Assembly: Implement Bill Of Materials (#14198)
* Assembly: Implementation of BOM * Assembly: BOM: make it possible for BOM to be made without an assembly.
This commit is contained in:
303
src/Mod/Assembly/App/BomObject.cpp
Normal file
303
src/Mod/Assembly/App/BomObject.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2023 Ondsel <development@ondsel.com> *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU Lesser General Public License as *
|
||||
* published by the Free Software Foundation, either version 2.1 of the *
|
||||
* License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with FreeCAD. If not, see *
|
||||
* <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
#ifndef _PreComp_
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
#include <App/DocumentObjectGroup.h>
|
||||
#include <App/FeaturePythonPyImp.h>
|
||||
#include <App/Link.h>
|
||||
#include <App/PropertyPythonObject.h>
|
||||
#include <App/Range.h>
|
||||
#include <Base/Console.h>
|
||||
#include <Base/Placement.h>
|
||||
#include <Base/Rotation.h>
|
||||
#include <Base/Tools.h>
|
||||
#include <Base/Interpreter.h>
|
||||
|
||||
#include <Mod/Part/App/PartFeature.h>
|
||||
#include <Mod/PartDesign/App/Body.h>
|
||||
#include <Mod/Spreadsheet/App/Cell.h>
|
||||
|
||||
|
||||
#include "AssemblyObject.h"
|
||||
#include "BomGroup.h"
|
||||
#include "JointGroup.h"
|
||||
#include "ViewGroup.h"
|
||||
#include "BomObject.h"
|
||||
#include "BomObjectPy.h"
|
||||
|
||||
|
||||
using namespace Assembly;
|
||||
|
||||
// ================================ Assembly Object ============================
|
||||
|
||||
PROPERTY_SOURCE(Assembly::BomObject, Spreadsheet::Sheet)
|
||||
|
||||
BomObject::BomObject()
|
||||
: Spreadsheet::Sheet()
|
||||
{
|
||||
ADD_PROPERTY_TYPE(columnsNames,
|
||||
("Index"),
|
||||
"Bom",
|
||||
(App::PropertyType)(App::Prop_None),
|
||||
"List of the columns of the Bill of Materials.");
|
||||
|
||||
ADD_PROPERTY_TYPE(detailSubAssemblies,
|
||||
(true),
|
||||
"Bom",
|
||||
(App::PropertyType)(App::Prop_None),
|
||||
"Detail sub-assemblies components.");
|
||||
|
||||
ADD_PROPERTY_TYPE(detailParts,
|
||||
(true),
|
||||
"Bom",
|
||||
(App::PropertyType)(App::Prop_None),
|
||||
"Detail Parts sub-components.");
|
||||
|
||||
ADD_PROPERTY_TYPE(
|
||||
onlyParts,
|
||||
(false),
|
||||
"Bom",
|
||||
(App::PropertyType)(App::Prop_None),
|
||||
"Only Part containers will be added. Solids like PartDesign Bodies will be ignored.");
|
||||
}
|
||||
BomObject::~BomObject() = default;
|
||||
|
||||
PyObject* BomObject::getPyObject()
|
||||
{
|
||||
if (PythonObject.is(Py::_None())) {
|
||||
// ref counter is set to 1
|
||||
PythonObject = Py::Object(new BomObjectPy(this), true);
|
||||
}
|
||||
return Py::new_reference_to(PythonObject);
|
||||
}
|
||||
|
||||
App::DocumentObjectExecReturn* BomObject::execute()
|
||||
{
|
||||
generateBOM();
|
||||
|
||||
return Spreadsheet::Sheet::execute();
|
||||
}
|
||||
|
||||
void BomObject::saveCustomColumnData()
|
||||
{
|
||||
// This function saves data that is not automatically generated.
|
||||
dataElements.clear();
|
||||
std::tuple<App::CellAddress, App::CellAddress> res = getUsedRange();
|
||||
int maxRow = std::get<1>(res).row();
|
||||
int nameColIndex = getColumnIndex("Name");
|
||||
|
||||
for (int row = 1; row <= maxRow; ++row) {
|
||||
|
||||
size_t col = 0;
|
||||
// Here we do not iterate columnName : columnsNames.getValues() because they may have
|
||||
// changed
|
||||
for (size_t i = 0; i < columnsNames.getValues().size(); ++i) {
|
||||
std::string columnName = getText(0, i);
|
||||
if (columnName != "Index" && columnName != "Name" && columnName != "Quantity"
|
||||
&& columnName != "File Name") {
|
||||
// Base::Console().Warning("row col %d %d\n", row, col);
|
||||
// save custom data if any.
|
||||
std::string text = getText(row, col);
|
||||
if (text != "") {
|
||||
std::string objName = getText(row, nameColIndex);
|
||||
BomDataElement el(objName, columnName, text);
|
||||
dataElements.push_back(el);
|
||||
}
|
||||
}
|
||||
|
||||
++col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BomObject::generateBOM()
|
||||
{
|
||||
saveCustomColumnData();
|
||||
clearAll();
|
||||
size_t row = 0;
|
||||
size_t col = 0;
|
||||
|
||||
// Populate headers
|
||||
for (auto& columnName : columnsNames.getValues()) {
|
||||
setCell(App::CellAddress(row, col), columnName.c_str());
|
||||
++col;
|
||||
}
|
||||
++row;
|
||||
|
||||
auto* assembly = getAssembly();
|
||||
if (assembly) {
|
||||
addObjectChildrenToBom(assembly->getOutList(), row, "");
|
||||
}
|
||||
else {
|
||||
addObjectChildrenToBom(getDocument()->getRootObjectsIgnoreLinks(), row, "");
|
||||
}
|
||||
}
|
||||
|
||||
void BomObject::addObjectChildrenToBom(std::vector<App::DocumentObject*> objs,
|
||||
size_t& row,
|
||||
std::string index)
|
||||
{
|
||||
int nameColIndex = getColumnIndex("Name");
|
||||
int quantityColIndex = getColumnIndex("Quantity");
|
||||
bool hasQuantityCol = hasQuantityColumn();
|
||||
|
||||
int siblingsInitialRow = row;
|
||||
|
||||
if (index != "") {
|
||||
index = index + ".";
|
||||
}
|
||||
|
||||
size_t sub_i = 1;
|
||||
|
||||
for (auto* child : objs) {
|
||||
if (child->isDerivedFrom<App::Link>()) {
|
||||
child = static_cast<App::Link*>(child)->getLinkedObject();
|
||||
}
|
||||
|
||||
if (child->isDerivedFrom<BomGroup>() || child->isDerivedFrom<JointGroup>()
|
||||
|| child->isDerivedFrom<ViewGroup>()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasQuantityCol) {
|
||||
// Check if the object is not already in (case of links). And if so just increment.
|
||||
// Note: an object can be used in several parts. In which case we do no want to blindly
|
||||
// increment.
|
||||
bool found = false;
|
||||
for (size_t i = siblingsInitialRow; i <= row; ++i) {
|
||||
std::string childName = child->Label.getValue();
|
||||
|
||||
if (childName == getText(i, nameColIndex) && childName != "") {
|
||||
int qty = std::stoi(getText(i, quantityColIndex)) + 1;
|
||||
setCell(App::CellAddress(i, quantityColIndex), std::to_string(qty).c_str());
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (child->isDerivedFrom<App::DocumentObjectGroup>()
|
||||
|| child->isDerivedFrom<AssemblyObject>() || child->isDerivedFrom<App::Part>()
|
||||
|| (child->isDerivedFrom<Part::Feature>() && !onlyParts.getValue())) {
|
||||
|
||||
std::string sub_index = index + std::to_string(sub_i);
|
||||
++sub_i;
|
||||
|
||||
addObjectToBom(child, row, sub_index);
|
||||
++row;
|
||||
|
||||
if (child->isDerivedFrom<App::DocumentObjectGroup>()
|
||||
|| (child->isDerivedFrom<AssemblyObject>() && detailSubAssemblies.getValue())
|
||||
|| (child->isDerivedFrom<App::Part>() && detailParts.getValue())) {
|
||||
addObjectChildrenToBom(child->getOutList(), row, sub_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BomObject::addObjectToBom(App::DocumentObject* obj, size_t row, std::string index)
|
||||
{
|
||||
size_t col = 0;
|
||||
for (auto& columnName : columnsNames.getValues()) {
|
||||
if (columnName == "Index") {
|
||||
setCell(App::CellAddress(row, col), index.c_str());
|
||||
}
|
||||
else if (columnName == "Name") {
|
||||
setCell(App::CellAddress(row, col), obj->Label.getValue());
|
||||
}
|
||||
else if (columnName == "File Name") {
|
||||
setCell(App::CellAddress(row, col), obj->getDocument()->getFileName());
|
||||
}
|
||||
else if (columnName == "Quantity") {
|
||||
setCell(App::CellAddress(row, col), std::to_string(1).c_str());
|
||||
}
|
||||
else {
|
||||
// load custom data if any.
|
||||
for (auto& el : dataElements) {
|
||||
if (el.objName == obj->Label.getValue() && el.columnName == columnName) {
|
||||
setCell(App::CellAddress(row, col), el.value.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
++col;
|
||||
}
|
||||
}
|
||||
|
||||
AssemblyObject* BomObject::getAssembly()
|
||||
{
|
||||
for (auto& obj : getInList()) {
|
||||
if (obj->isDerivedFrom<AssemblyObject>()) {
|
||||
return static_cast<AssemblyObject*>(obj);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool BomObject::hasQuantityColumn()
|
||||
{
|
||||
for (auto& columnName : columnsNames.getValues()) {
|
||||
if (columnName == "Quantity") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Assembly::BomObject::getText(size_t row, size_t col)
|
||||
{
|
||||
Spreadsheet::Cell* cell = getCell(App::CellAddress(row, col));
|
||||
std::string cellName;
|
||||
if (cell) {
|
||||
cell->getStringContent(cellName);
|
||||
|
||||
// getStringContent is addind a ' before the string for whatever reason.
|
||||
if (!cellName.empty() && cellName.front() == '\'') {
|
||||
cellName.erase(0, 1); // Remove the first character if it's a '
|
||||
}
|
||||
}
|
||||
|
||||
return cellName;
|
||||
}
|
||||
|
||||
int BomObject::getColumnIndex(std::string name)
|
||||
{
|
||||
int col = 0;
|
||||
for (auto& columnName : columnsNames.getValues()) {
|
||||
if (columnName == name) {
|
||||
return col;
|
||||
}
|
||||
++col;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
Reference in New Issue
Block a user