diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css
index c0ad9a8e16..eac13d1945 100644
--- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css
+++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css
@@ -20,6 +20,22 @@ body {
box-sizing: border-box;
}
+.floating-form {
+ width: 50vw;
+ max-height: 80vh;
+ position: absolute;
+ background-color: var(--background-color);
+ border: 1px solid black;
+ z-index: 9999;
+ cursor: move;
+ user-select: none;
+ padding-bottom: var(--padding-medium);
+}
+
+[id^="enable-editing-quantities"].floating-form {
+ height: 70vh;
+}
+
.form-header {
display: flex;
justify-content: flex-start;
@@ -30,36 +46,44 @@ body {
font-size: large;
border-bottom: 1px solid #ddd;
border-radius: 6px 6px 0 0;
+ height: 5%;
}
-.floating-form span {
- padding: var(--padding-small);
- margin-left: var(--margin-small);
+.form-container {
+ height: 95%;
+ padding-left: var(--padding-medium);
+ padding-right: var(--padding-medium);
}
-.floating-form {
- width: 50vw;
- max-height: 80vh;
- position: absolute;
- background-color: var(--background-color);
- border: 1px solid black;
- z-index: 9999;
- cursor: move;
- user-select: none;
- resize: both;
+.form-section {
+ margin-bottom: 1em;
+ height: 65%;
+ overflow-y: auto;
+ overflow-x: hidden;
}
+.form-section h3 {
+ margin-bottom: 0.5em;
+}
+
+.summary-section {
+ margin-top: 1em;
+}
+
+.summary-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 14px;
+ text-align: left;
+}
+
+
.floating-form table tr {
margin-bottom: 10px;
}
-.form-container {
- padding: var(--padding-medium);
- overflow-y: auto;
- overflow-x: hidden;
-}
-
.action-button {
+ position:relative;
background-color: var(--button-background);
border: none;
color: #fff;
@@ -72,6 +96,32 @@ body {
transition: background-color 0.3s ease;
width: fit-content;
margin: 0.5em;
+ z-index: 11000;
+}
+
+.action-button::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: -30px;
+ background-color: #333;
+ color: #fff;
+ padding: 5px 10px;
+ border-radius: 5px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s;
+ font-size: 12px;
+}
+
+.action-button:hover::after {
+ opacity: 1;
+}
+
+.active-btn {
+ background-color: #887821;
+ /* make a nic shadow */
+ box-shadow: 0 0 10px #887821;
}
.action-button:hover {
@@ -141,7 +191,7 @@ body {
}
.floating-form .table-container {
- max-height: 70%;
+ max-height: 80%;
}
#cost-items {
@@ -149,7 +199,6 @@ body {
}
[id^="cost-values-form"] table {
- /* fixed */
max-height: 50%;
table-layout: fixed;
}
@@ -186,6 +235,7 @@ td,
th {
border-right: 1px solid #ddd;
text-align: center;
+ height: 100%;
}
th {
@@ -221,15 +271,7 @@ select {
color: var(--blender-button-text, var(--text-color));
border-color: var(--blender-button-border, var(--border-color));
transition: filter 0.2s ease;
- border-radius: 6px;
- padding: 6px 8px;
- font-size: 14px;
- line-height: 20px;
transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
- margin-left: 0.5em;
- margin-right: 0.5em;
- font-size: var(--fontSize);
- min-width: 50px;
}
input:focus {
@@ -239,6 +281,31 @@ input:focus {
color: #ddd;
}
+#cost-items input, #cost-items
+select {
+ border-radius: 6px;
+ padding: 6px 8px;
+ font-size: 14px;
+ line-height: 20px;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ font-size: var(--fontSize);
+ min-width: 50px;
+}
+
+form input {
+ border-radius: 6px;
+ padding: 3px 4px;
+ font-size: 12px;
+ line-height: 10px;
+ margin-left: 0.2em;
+ margin-right: 0.2em;
+ font-size: inherit;
+ height: inherit;
+ width: inherit;
+ box-sizing: border-box;
+}
+
#cost-items tr:hover,
.highlighted {
background-color: #28a74657;
@@ -332,7 +399,7 @@ form table {
}
.clickable-cell:hover {
- border: 1px solid #28a746;
+ border: 1px solid #94a728;
border-radius: 6px;
cursor: pointer;
-}
+}
\ No newline at end of file
diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js
index 74943c9426..be078a8d3c 100644
--- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js
+++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js
@@ -36,19 +36,51 @@ function handleEditQuantities(data) {
const products = data.data["quantities"]["selected_products"];
const assigned_products = data.data["quantities"]["assigned_products"];
const quantityNames = data.data["quantities"]["product_quantity_names"];
+ const costQuantities = data.data["quantities"]["cost_quantities"];
- CostUI.editQuantities({
+ CostUI.enableEditingQuantities({
costItemId: costItemId,
selectedProducts: products,
assignedProducts: assigned_products,
quantityNames: quantityNames,
+ costQuantities: costQuantities,
callbacks: {
addProductAssignments: addProductAssignments,
enableEditingQuantities: enableEditingQuantities,
+ addQuantity: addQuantity,
+ editQuantity: editQuantity,
+ deleteQuantity: deleteQuantity,
},
});
}
+function addQuantity(costItemId, ifcClass) {
+ executeOperator({
+ type: "AddCostItemQuantity",
+ costItemId: costItemId,
+ ifcClass: ifcClass,
+ });
+
+ //CostUI.addQuantity(costItemId, quantityName);
+}
+
+function editQuantity(costItemId, quantityId, attributes) {
+ executeOperator({
+ type: "editCostItemQuantity",
+ costItemId: costItemId,
+ quantityId: quantityId,
+ attributes: attributes,
+ });
+}
+
+function deleteQuantity(costItemId, quantityId) {
+ executeOperator({
+ type: "deleteCostItemQuantity",
+ costItemId: costItemId,
+ quantityId: quantityId,
+ });
+}
+
function handlePredefinedTypes(data) {
CostUI.enableAddingCostSchedule(
data.data["predefined_types"],
diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js
index 0a94fb4d50..7a99b57c8a 100644
--- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js
+++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js
@@ -793,11 +793,15 @@ export class CostUI {
}
static getRowNameCell(costItemId) {
- return document.getElementById(costItemId).querySelector("td input");
+ return document.getElementById(costItemId)
+ ? document.getElementById(costItemId).querySelector("td input")
+ : null;
}
static getCostItemName(costItemId) {
- return CostUI.getRowNameCell(costItemId).value;
+ return CostUI.getRowNameCell(costItemId)
+ ? CostUI.getRowNameCell(costItemId).value
+ : "Unnamed";
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
@@ -840,7 +844,6 @@ export class CostUI {
static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) {
const table = CostUI.getCostValuesTable(costItemId);
if (!table) {
- console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
const tr = CostUI.createCostvaluesRow(
@@ -908,8 +911,8 @@ export class CostUI {
} else if (costValue.applied_value) {
costType = "FIXED";
}
-
- const typeCell = CostUI.createTableDropdown("type", costType);
+ const options = ["FIXED", "CATEGORY", "SUM"];
+ const typeCell = CostUI.createTableDropdown("type", options, costType);
const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function () {
const selectedType = this.value;
@@ -1012,12 +1015,11 @@ export class CostUI {
}
}
- static createTableDropdown(name, value = "FIXED") {
+ static createTableDropdown(name, options, value = "FIXED") {
const cell = document.createElement("td");
const dropdown = document.createElement("select");
dropdown.name = name;
- const options = ["FIXED", "CATEGORY", "SUM"];
options.forEach((optionValue) => {
const option = document.createElement("option");
option.value = optionValue;
@@ -1306,7 +1308,7 @@ export class CostUI {
}
static createProductTable({
- form,
+ container,
products,
quantityNames,
costItemId,
@@ -1316,7 +1318,7 @@ export class CostUI {
const noProductsMessage = document.createElement("p");
noProductsMessage.textContent =
"Your Blender Selection is empty! Select objects first.";
- form.appendChild(noProductsMessage);
+ container.appendChild(noProductsMessage);
return;
}
const { tableContainer, table } = CostUI.addTable({
@@ -1324,7 +1326,7 @@ export class CostUI {
className: "",
id: "cost-values-table-" + costItemId,
});
- form.appendChild(tableContainer);
+ container.appendChild(tableContainer);
const tbody = document.createElement("tbody");
table.appendChild(tbody);
@@ -1432,7 +1434,7 @@ export class CostUI {
const productIds = products.map((product) => product.info.id);
callbacks.emptyForm = () => {
- form.innerHTML = "";
+ container.innerHTML = "";
};
const addProductAssignmentsButton = CostUI.addProductAssignmentsButton(
costItemId,
@@ -1441,13 +1443,13 @@ export class CostUI {
callbacks
);
- form.appendChild(quantitySelect);
- form.appendChild(addProductAssignmentsButton);
+ container.appendChild(quantitySelect);
+ container.appendChild(addProductAssignmentsButton);
return table;
}
static createAssignmentsTable({
- form,
+ container,
products,
costItemId,
quantityNames,
@@ -1459,7 +1461,7 @@ export class CostUI {
id: "cost-values-table-" + costItemId,
});
- form.appendChild(tableContainer);
+ container.appendChild(tableContainer);
const tbody = document.createElement("tbody");
table.appendChild(tbody);
@@ -1512,8 +1514,10 @@ export class CostUI {
quantitySelect,
callbacks
) {
- const addProductAssignmentsButton = document.createElement("button");
- addProductAssignmentsButton.textContent = "Add Product Assignments";
+ const addProductAssignmentsButton = CostUI.createButton(
+ "Add Product Assignments",
+ "fa-solid fa-plus"
+ );
addProductAssignmentsButton.addEventListener("click", (event) => {
event.preventDefault();
let propName = quantitySelect.value;
@@ -1528,7 +1532,7 @@ export class CostUI {
})
: null;
callbacks.emptyForm ? callbacks.emptyForm() : null;
- callbacks.getSelectedProducts(costItemId);
+ callbacks.enableEditingQuantities(costItemId);
});
addProductAssignmentsButton.classList.add("action-button");
return addProductAssignmentsButton;
@@ -1730,49 +1734,424 @@ export class CostUI {
});
}
- static editQuantities({
+ static enableEditingQuantities({
costItemId,
selectedProducts,
- quantityNames,
assignedProducts,
+ quantityNames,
+ costQuantities,
callbacks,
}) {
- const formId = "selected-products-" + costItemId;
+ CostUI.highlightElement(costItemId);
const form = CostUI.Form({
- id: formId,
- name:
- "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId),
+ id: "enable-editing-quantities-" + costItemId,
+ name: "Editing Quantities for: " + CostUI.getCostItemName(costItemId),
icon: "fa-solid fa-box",
});
- const numberOfProducts = CostUI.Text(
- "Selection basket : " + selectedProducts.length + " products",
- "fa-solid fa-cart-shopping",
- "large"
- );
- form.appendChild(numberOfProducts);
- CostUI.highlightElement(costItemId);
- const selectedProductsTable = CostUI.createProductTable({
- form,
+
+ const ribbonBar = CostUI.createRibbonBar();
+ form.appendChild(ribbonBar);
+
+ const selectedProductsSection = CostUI.selectedProductsSection({
products: selectedProducts,
quantityNames,
costItemId,
callbacks,
});
- if (assignedProducts.length > 0) {
+ const assignedProductsSection = CostUI.assignedProductsSection({
+ products: assignedProducts,
+ quantityNames,
+ costItemId,
+ callbacks,
+ });
+
+ const manualQuantitiesSection = CostUI.manualQuantitiesSection({
+ costItemId,
+ costQuantities,
+ has_assigned_products: assignedProducts.length > 0,
+ callbacks,
+ });
+
+ form.appendChild(selectedProductsSection);
+ form.appendChild(assignedProductsSection);
+ form.appendChild(manualQuantitiesSection);
+
+ const summarySection = CostUI.createSummarySection({
+ selectedProducts,
+ assignedProducts,
+ costQuantities,
+ });
+ form.appendChild(summarySection);
+
+ CostUI.addEventListeners({
+ switchBar: ribbonBar,
+ costItemId,
+ selectedProductsSection,
+ assignedProductsSection,
+ manualQuantitiesSection,
+ });
+
+ const lastActiveSection =
+ localStorage.getItem("lastActiveSection") || "selected-products";
+ const lastActiveButton = document.getElementById(
+ `${lastActiveSection}-btn`
+ );
+ if (lastActiveButton) {
+ lastActiveButton.click();
+ }
+ }
+
+ static createRibbonBar() {
+ const ribbonBar = document.createElement("div");
+ ribbonBar.className = "switch-bar";
+ ribbonBar.innerHTML = `
+
+
+
+ `;
+ return ribbonBar;
+ }
+
+ static createSummarySection({
+ selectedProducts,
+ assignedProducts,
+ costQuantities,
+ }) {
+ const summarySection = document.createElement("div");
+ summarySection.classList.add("summary-section");
+
+ const manualQuantities = costQuantities.quantities.filter(
+ (quantity) => !quantity.fromProduct
+ );
+ const paramaterQuantities = costQuantities.quantities.filter(
+ (quantity) => quantity.fromProduct
+ );
+
+ const table = document.createElement("table");
+ table.classList.add("summary-table");
+
+ const thead = document.createElement("thead");
+ const headerRow = document.createElement("tr");
+ const headers = ["Category", "Count"];
+ headers.forEach((headerText) => {
+ const th = document.createElement("th");
+ th.textContent = headerText;
+ headerRow.appendChild(th);
+ });
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+ const tbody = document.createElement("tbody");
+
+ const data = [
+ { category: "Assigned Products", count: assignedProducts.length },
+ { category: "Manual Quantities", count: manualQuantities.length },
+ {
+ category: "Product derived Quantities",
+ count: paramaterQuantities.length,
+ },
+ ];
+
+ data.forEach((item) => {
+ const row = document.createElement("tr");
+ const categoryCell = document.createElement("td");
+ categoryCell.textContent = item.category;
+ const countCell = document.createElement("td");
+ countCell.textContent = item.count;
+ row.appendChild(categoryCell);
+ row.appendChild(countCell);
+ tbody.appendChild(row);
+ });
+
+ table.appendChild(tbody);
+ summarySection.appendChild(table);
+
+ return summarySection;
+ }
+
+ static addEventListeners({
+ switchBar,
+ costItemId,
+ selectedProductsSection,
+ assignedProductsSection,
+ manualQuantitiesSection,
+ }) {
+ const buttons = switchBar.querySelectorAll(".action-button");
+
+ buttons.forEach((button) => {
+ button.addEventListener("click", (e) => {
+ e.preventDefault();
+ const sectionName = button.id.replace("-btn", "");
+ const sectionId = sectionName + "-section-" + costItemId;
+
+ if (selectedProductsSection)
+ selectedProductsSection.style.display = "none";
+ if (assignedProductsSection)
+ assignedProductsSection.style.display = "none";
+ if (manualQuantitiesSection)
+ manualQuantitiesSection.style.display = "none";
+
+ const section = document.getElementById(sectionId);
+ if (section) {
+ section.style.display = "block";
+ }
+ buttons.forEach((btn) => btn.classList.remove("active-btn"));
+ button.classList.add("active-btn");
+
+ localStorage.setItem("lastActiveSection", sectionName);
+ });
+ });
+ }
+
+ static getSection(costItemId, sectionName) {
+ return document.getElementById(`${sectionName}-${costItemId}`);
+ }
+
+ static selectedProductsSection({
+ products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ }) {
+ const selectedProductsSection = document.createElement("div");
+ selectedProductsSection.id = "selected-products-section-" + costItemId;
+ selectedProductsSection.classList.add("form-section");
+ const numberOfProducts = CostUI.Text(
+ "Selection basket : " + products.length + " products",
+ "fa-solid fa-cart-shopping",
+ "medium"
+ );
+ selectedProductsSection.appendChild(numberOfProducts);
+
+ const selectedProductsTable = CostUI.createProductTable({
+ container: selectedProductsSection,
+ products: products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ });
+ return selectedProductsSection;
+ }
+
+ static assignedProductsSection({
+ products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ }) {
+ const assignedProductsSection = document.createElement("div");
+ assignedProductsSection.id = "assigned-products-section-" + costItemId;
+ assignedProductsSection.style.display = "none";
+ assignedProductsSection.classList.add("form-section");
+ if (products.length > 0) {
+ let text = " Assigned Products : " + products.length;
const assignedProductsText = CostUI.Text(
- "Assigned Products",
+ text,
"fa-solid fa-solid fa-paperclip",
- "large"
+ "medium"
);
- form.appendChild(assignedProductsText);
+ assignedProductsSection.appendChild(assignedProductsText);
const assignmentsTable = CostUI.createAssignmentsTable({
- form,
- products: assignedProducts,
+ container: assignedProductsSection,
+ products: products,
quantityNames,
costItemId,
callbacks,
});
}
+
+ return assignedProductsSection;
+ }
+
+ static manualQuantitiesSection({
+ costItemId,
+ costQuantities,
+ has_assigned_products,
+ callbacks,
+ }) {
+ const manualQuantitiesSection = document.createElement("div");
+ manualQuantitiesSection.classList.add("form-section");
+ manualQuantitiesSection.id = "manual-quantities-section-" + costItemId;
+ manualQuantitiesSection.style.display = "none";
+
+ const title = CostUI.Text(
+ "Manual Quantities",
+ "fa-solid fa-ruler",
+ "medium"
+ );
+ manualQuantitiesSection.appendChild(title);
+ const unitSymbol = costQuantities.unit_symbol;
+
+ let quantityType = costQuantities.quantity_type;
+
+ if (quantityType) {
+ const text = CostUI.Text(
+ quantityType + " (" + unitSymbol + " )",
+ "fa-solid fa-ruler",
+ "small"
+ );
+ manualQuantitiesSection.appendChild(text);
+ } else {
+ // add dropdown to chose quantity type , from "IfcQuantityArea", "IfcQuantityLength", "IfcQuantityVolume", "IfcQuantityCount", "IfcQuantityWeight "
+ const quantityTypes = [
+ "IfcQuantityArea",
+ "IfcQuantityLength",
+ "IfcQuantityVolume",
+ "IfcQuantityCount",
+ "IfcQuantityWeight",
+ ];
+ const dropdown = CostUI.createTableDropdown(
+ "type",
+ quantityTypes,
+ "IfcQuantityArea"
+ );
+ dropdown.id = "quantity-type-dropdown-" + costItemId;
+ manualQuantitiesSection.appendChild(dropdown);
+ }
+
+ // if quantity is ty IfcQuantityCount, and the costQuantities
+ if (quantityType === "IfcQuantityCount" && has_assigned_products) {
+ // write text that one of the quantities is assigned to the product selection
+ const text = CostUI.Text(
+ " One of the quantities is assigned to the product selection",
+ "fa-solid fa-warning",
+ "small"
+ );
+ manualQuantitiesSection.appendChild(text);
+ }
+
+ let paramaterQuantities = costQuantities.quantities.filter(
+ (quantity) => quantity.fromProduct
+ );
+ let manualQuantities = costQuantities.quantities.filter(
+ (quantity) => !quantity.fromProduct
+ );
+
+ let headerNames = ["Name", "Value" + " (" + unitSymbol + " )", "Actions"];
+
+ if (manualQuantities.length > 0) {
+ headerNames = [];
+ Object.entries(manualQuantities[0]).forEach(([key, value]) => {
+ if (key !== "id" && key !== "fromProduct") {
+ headerNames.push(key);
+ }
+ });
+ headerNames.push("Actions");
+ }
+
+ const { tableContainer, table } = CostUI.addTable({
+ headers: headerNames,
+ className: "manual-quantities-table",
+ id: "manual-quantities-table-" + costItemId,
+ });
+
+ manualQuantities.forEach((quantity) => {
+ const tr = CostUI.createManualQuantityRow(
+ costItemId,
+ quantity,
+ callbacks
+ );
+ table.appendChild(tr);
+ });
+ const addButton = CostUI.createAddManualQuantityButton(
+ costItemId,
+ quantityType,
+ callbacks
+ );
+
+ manualQuantitiesSection.appendChild(tableContainer);
+ // manualQuantitiesSection.appendChild(tableContainer2);
+ manualQuantitiesSection.appendChild(addButton);
+ return manualQuantitiesSection;
+ }
+
+ static createManualQuantityRow(costItemId, quantity, callbacks) {
+ const tr = document.createElement("tr");
+ tr.id = quantity.id;
+
+ // get quantity keys and values to create the table row cells
+ // Label cell
+
+ Object.entries(quantity).forEach(([key, value]) => {
+ if (key !== "id" && key !== "fromProduct") {
+ if (key === "Name") {
+ // create input
+ const cell = document.createElement("td");
+ const input = document.createElement("input");
+ input.type = "text";
+ input.value = value;
+ cell.appendChild(input);
+ tr.appendChild(cell);
+
+ input.addEventListener("keydown", function (e) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ callbacks.editQuantity(costItemId, quantity.id, {
+ [key]: input.value,
+ });
+ }
+ });
+ }
+
+ // if key contains value
+ else if (key.toLowerCase().includes("value")) {
+ const cell = document.createElement("td");
+ const input = document.createElement("input");
+ input.type = "number";
+ input.value = value;
+ cell.appendChild(input);
+
+ input.addEventListener("keydown", function (e) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ callbacks.editQuantity(costItemId, quantity.id, {
+ [key]: parseFloat(input.value), // Dynamically set the key and parse the value as a double
+ });
+ }
+ });
+ tr.appendChild(cell);
+ } else {
+ const cell = document.createElement("td");
+ cell.textContent = value;
+ tr.appendChild(cell);
+ }
+ }
+ });
+
+ // Add delete button
+ const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash");
+ const deleteCell = document.createElement("td");
+ deleteButton.addEventListener("click", function (e) {
+ e.preventDefault();
+ callbacks.deleteQuantity(costItemId, quantity.id);
+ tr.remove();
+ });
+ deleteCell.appendChild(deleteButton);
+ tr.appendChild(deleteCell);
+
+ return tr;
+ }
+
+ static createAddManualQuantityButton(costItemId, quantityType, callbacks) {
+ const addButton = CostUI.createButton(
+ "Add Manual Quantity",
+ "fa-solid fa-plus"
+ );
+
+ addButton.addEventListener("click", function (e) {
+ e.preventDefault();
+ let type = quantityType;
+ if (!type) {
+ // get quantityType from dropdown
+ const qtoSection = document.getElementById(
+ "manual-quantities-section-" + costItemId
+ );
+ const dropdown = qtoSection.querySelector("select");
+ type = dropdown ? dropdown.value : null;
+ }
+ callbacks.addQuantity(costItemId, type);
+ });
+
+ return addButton;
}
}
diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py
index 8d9945a4ad..4c68d31d12 100644
--- a/src/bonsai/bonsai/tool/cost.py
+++ b/src/bonsai/bonsai/tool/cost.py
@@ -295,6 +295,15 @@ class Cost(bonsai.core.tool.Cost):
unit = cls.get_quantity_unit_symbol(quantity)
return selected_quantitites, unit
+
+ @classmethod
+ def get_assigned_product(cls, cost_item, quantity):
+ assigned_products = cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=False)
+ for product in assigned_products:
+ assigned_quantities, _ = cls.get_assigned_quantities(cost_item, product)
+ if quantity in assigned_quantities:
+ return product
+
@classmethod
def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]:
if related_object_type == "PRODUCT":
@@ -859,7 +868,33 @@ class Cost(bonsai.core.tool.Cost):
bpy.ops.bim.connect_websocket_server(page="costing")
cost_schedule_data = cls.create_cost_schedule_json(cost_chedule)
tool.Web.send_webui_data(
- data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id()},
+ data={
+ "cost_items": cost_schedule_data,
+ "cost_schedule_id": cost_chedule.id(),
+ "currency": cls.currency()
+ },
data_key="cost_items",
event="cost_items",
)
+
+ @classmethod
+ def get_cost_quantities(cls, cost_item: ifcopenshell.entity_instance) -> dict:
+ results = {
+ "quantities": [],
+ "unit_symbol": None,
+ }
+ if not cost_item:
+ return results
+ results["quantity_type"] = cost_item.CostQuantities[0].is_a() if cost_item.CostQuantities else None
+ unit = ifcopenshell.util.unit.get_property_unit(cost_item.CostQuantities[0], tool.Ifc.get()) if cost_item.CostQuantities else None
+ if unit:
+ results["unit_symbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
+ for quantity in cost_item.CostQuantities or []:
+ assigned_product = cls.get_assigned_product(cost_item, quantity)
+ info = quantity.get_info()
+ info["fromProduct"] = assigned_product.get_info(recursive=True) if assigned_product else None
+ results["quantities"].append(info)
+ if results["quantity_type"] == "IfcQuantityCount":
+ results["unit_symbol"] = "U"
+ return results
+
diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py
index 858c2d2c5f..c8d3f1fb82 100644
--- a/src/bonsai/bonsai/tool/web.py
+++ b/src/bonsai/bonsai/tool/web.py
@@ -344,52 +344,6 @@ class Web(bonsai.core.tool.Web):
operator_data (dict): A dictionary containing the operator data.
"""
- def selection_data(cost_item, elements):
- print(cost_item, elements)
- if not elements:
- return []
- if not cost_item:
- return [
- {
- "info": {
- "id": element.id(),
- "name": element.Name,
- "class": element.is_a(),
- "type": get_type(element).Name if get_type(element) else None,
- },
- "qtos": get_psets(element, qtos_only=True),
- }
- for element in elements or []
- ]
- data = []
- for element in elements or []:
- psets = get_psets(element, qtos_only=True)
- element_type = get_type(element)
- quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element)
- data.append(
- {
- "info": {
- "id": element.id(),
- "name": element.Name,
- "class": element.is_a(),
- "type": element_type.Name if element_type else None,
- },
- "qtos": psets,
- "assigned_quantities": [
- {
- "cost_item_id": cost_item.id(),
- "product_id": element.id(),
- "name": q.Name,
- "value": q[3],
- "type": q.Unit,
- }
- for q in quantities or []
- ],
- "unit": unit,
- }
- )
- return data
-
ifc_file = tool.Ifc.get()
if operator_data["type"] == "getPredefinedTypes":
print("getting predefined types")
@@ -425,31 +379,6 @@ class Web(bonsai.core.tool.Web):
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
tool.Spatial.select_products(products, unhide=True)
- if operator_data["type"] == "enableEditingQuantities":
- cost_item_id = operator_data["costItemId"]
- cost_item = ifc_file.by_id(cost_item_id)
- if not cost_item:
- print("---> cost item not found")
- return
-
- selected_products = list(tool.Spatial.get_selected_products())
- selected_products_data = selection_data(cost_item, selected_products)
-
- assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False)
- assigned_products_data = selection_data(cost_item, assigned_products)
-
- names = ifcopenshell.util.cost.get_product_quantity_names(selected_products)
- cls.send_webui_data(
- data={
- "selected_products": selected_products_data,
- "assigned_products": assigned_products_data,
- "product_quantity_names": names,
- "cost_item_id": cost_item_id,
- },
- data_key="quantities",
- event="quantities",
- )
-
if operator_data["type"] == "addSummaryCostItem":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule)
@@ -520,12 +449,109 @@ class Web(bonsai.core.tool.Web):
prop_name = operator_data["propName"]
if prop_name == "count":
prop_name = ""
- print(prop_name)
bpy.ops.bim.assign_cost_item_quantity(
cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name
)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
cls.load_cost_schedule_web_ui(cost_schedule)
+ if operator_data["type"] == "enableEditingQuantities":
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cls.enableEditingCostItemQuantities(cost_item)
+ if operator_data["type"] == "AddCostItemQuantity":
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cost_schedule = tool.Cost.get_cost_schedule(cost_item)
+ bpy.ops.bim.add_cost_item_quantity(cost_item=cost_item_id, ifc_class=operator_data["ifcClass"])
+ cls.load_cost_schedule_web_ui(cost_schedule)
+ cls.enableEditingCostItemQuantities(cost_item)
+ if operator_data["type"] == "editCostItemQuantity":
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cost_schedule = tool.Cost.get_cost_schedule(cost_item)
+ physical_quantity = tool.Ifc.get().by_id(operator_data["quantityId"])
+ attributes = operator_data["attributes"]
+ tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes)
+ tool.Cost.load_cost_item_quantities(ifc_file.by_id(operator_data["costItemId"]))
+ cls.load_cost_schedule_web_ui(cost_schedule)
+ cls.enableEditingCostItemQuantities(cost_item)
+ if operator_data["type"] == "deleteCostItemQuantity":
+ bpy.ops.bim.remove_cost_item_quantity(cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"])
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cost_schedule = tool.Cost.get_cost_schedule(cost_item)
+ cls.load_cost_schedule_web_ui(cost_schedule)
+ cls.enableEditingCostItemQuantities(cost_item)
+
+ @classmethod
+ def enableEditingCostItemQuantities(cls, cost_item):
+ if not cost_item:
+ return
+ selected_products = list(tool.Spatial.get_selected_products())
+ selected_products_data = cls.selection_data(cost_item, selected_products)
+
+ assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False)
+ assigned_products_data = cls.selection_data(cost_item, assigned_products)
+
+ names = ifcopenshell.util.cost.get_product_quantity_names(selected_products)
+ cls.send_webui_data(
+ data={
+ "selected_products": selected_products_data,
+ "assigned_products": assigned_products_data,
+ "product_quantity_names": names,
+ "cost_item_id": cost_item.id(),
+ "cost_quantities": tool.Cost.get_cost_quantities(cost_item),
+ },
+ data_key="quantities",
+ event="quantities",
+ )
+
+
+ @classmethod
+ def selection_data(cls, cost_item, elements):
+ if not elements:
+ return []
+ if not cost_item:
+ return [
+ {
+ "info": {
+ "id": element.id(),
+ "name": element.Name,
+ "class": element.is_a(),
+ "type": get_type(element).Name if get_type(element) else None,
+ },
+ "qtos": get_psets(element, qtos_only=True),
+ }
+ for element in elements or []
+ ]
+ data = []
+ for element in elements or []:
+ psets = get_psets(element, qtos_only=True)
+ element_type = get_type(element)
+ quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element)
+ data.append(
+ {
+ "info": {
+ "id": element.id(),
+ "name": element.Name,
+ "class": element.is_a(),
+ "type": element_type.Name if element_type else None,
+ },
+ "qtos": psets,
+ "assigned_quantities": [
+ {
+ "cost_item_id": cost_item.id(),
+ "product_id": element.id(),
+ "name": q.Name,
+ "value": q[3],
+ "type": q.Unit,
+ }
+ for q in quantities or []
+ ],
+ "unit": unit,
+ }
+ )
+ return data
@classmethod
def load_cost_schedule_web_ui(cls, cost_schedule):