From dcf8329b04087f4a94b38495810bfaeba68a71ca Mon Sep 17 00:00:00 2001 From: carlopav <47068848+carlopav@users.noreply.github.com> Date: Mon, 21 Jul 2025 08:34:00 +0200 Subject: [PATCH] Pdf ifc cost schedule export improvements (#6884) * Export_IfcCostSchedule_to_PDF_improvements * IfcCostSchedule CSV export: Added ItemIsASum column New column in the ifc export that tracks if IfcCostItem is a sum, also added a new static method to the IfcDataGetter class. * IfcCostSchedule CSV export: Added cost quantities column Cost quantities are a serialsed list containing the name of the quantity and the quantity value. * IfcCostScheduel PDF export: add options to fine tune export New options include nested_structure_depth, should_print_cover, should_print_description, should_print_rates, should_print_summary, should_print_cost_ids. Also pass project currency to typst (still not used). Added footer with "proudly created with IfcOpenShell". Updated Cover with formatting and IfcCostSchedule Description --- src/bonsai/bonsai/bim/module/cost/operator.py | 105 ++++++- src/bonsai/bonsai/core/cost.py | 8 +- src/bonsai/bonsai/tool/cost.py | 12 +- src/ifc5d/ifc5d/ifc5Dspreadsheet.py | 116 ++++++- .../typst_template_ifc_cost_schedule.typ | 287 ++++++++++++++---- 5 files changed, 441 insertions(+), 87 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index daa8cbb898..bf43ee8956 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -871,11 +871,82 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper): items=get_cost_schedules_enum_items, ) + nested_structure_depth: bpy.props.IntProperty( + name="Nested structure depth: ", + description="Define till which level of the structure the parent cost items are displayed.\n0: display the full structure.", + default=0, + min=0, + max=9, + ) + parent_to_new_page_up_to_depth: bpy.props.IntProperty( + name="Parents to new page up to depth: ", + description="Define till which level of the structure the parent is printed to a new page.\n0: no parent is split to a new page.", + default=0, + min=0, + max=9, + ) + show_only_parents: bpy.props.BoolProperty( + name="Show only parent cost items", + description="Hide cost items and show only container costs", + default=False, + ) + should_print_cover: bpy.props.BoolProperty( + name="Cover page", + description="Create a cover page with project data", + default=False, + ) + should_print_description: bpy.props.BoolProperty( + name="Full Cost Items Description", + description="Export the full description if present", + default=True, + ) + should_print_cost_ids: bpy.props.BoolProperty( + name="Print Cost Identification", + description="Print Cost Identification under Cost Name if present", + default=True, + ) + should_print_each_quantity: bpy.props.BoolProperty( + name="Show each quantity", + description="Export the full list of quantities", + default=False, + ) + should_print_each_cost_value: bpy.props.BoolProperty( + name="Show each cost value", + description="Export the full list of cost values\nassociated with each cost item\nin the schedule of rates", + default=False, + ) + should_print_rates: bpy.props.BoolProperty( + name="Rates and totals", + description="Print rates and totals for each voice", + default=True, + ) should_print_summary: bpy.props.BoolProperty( - name="Should print summary", + name="Final Summary", description="Print summary at the end of the document", default=True, ) + force_schedule_type: bpy.props.EnumProperty( + name="Force output type", + description="Force the output to this type\nalso if it is not coincident with the cost schedule Predefined Type", + items=[ + ( + "OFF", + "Off", + "Uses Cost Schedule Predefined Type", + ), + ( + "PRICEDBILLOFQUANTITIES", + "Priced Bill of Quantities", + "Forces the output as a priced bill of quantities", + ), + ( + "SCHEDULEOFRATES", + "Schedule of Rates", + "Forces the output as a schedule of rates", + ), + ], + default="OFF", + ) def draw(self, context): layout = self.layout @@ -884,8 +955,18 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper): box.prop(self, "cost_schedules_enum", text="") layout.separator() box = layout.box() - box.label(text="Export properties:") + box.label(text="Nested cost structure:") + box.prop(self, "nested_structure_depth") + layout.separator() + box = layout.box() + box.label(text="PDF Document properties:") + box.prop(self, "should_print_cover") + box.prop(self, "should_print_cost_ids") + box.prop(self, "should_print_description") + box.prop(self, "should_print_each_quantity") + box.prop(self, "should_print_rates") box.prop(self, "should_print_summary") + box.prop(self, "force_schedule_type") @classmethod def poll(cls, context): @@ -920,9 +1001,25 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper): file = tool.Ifc.get() self.props = tool.Cost.get_cost_props() cost_schedule = file.by_id(int(self.cost_schedules_enum)) - options = {"should_print_summary": self.should_print_summary} + options = { + "nested_structure_depth": self.nested_structure_depth, + "parent_to_new_page_up_to_depth": self.parent_to_new_page_up_to_depth, + "show_only_parents": self.show_only_parents, + "should_print_cover": self.should_print_cover, + "should_print_cost_ids": self.should_print_cost_ids, + "should_print_description": self.should_print_description, + "should_print_each_quantity": self.should_print_each_quantity, + "should_print_each_cost_value": self.should_print_each_cost_value, + "should_print_rates": self.should_print_rates, + "should_print_summary": self.should_print_summary, + } + core.export_cost_schedules_to_pdf( - tool.Cost, filepath=self.filepath, cost_schedule=cost_schedule, options=options + tool.Cost, + filepath=self.filepath, + cost_schedule=cost_schedule, + options=options, + force_schedule_type=self.force_schedule_type, ) return {"FINISHED"} diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index d640ba8083..d2eedbb40d 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -404,10 +404,14 @@ def export_cost_schedules( def export_cost_schedules_to_pdf( - cost: type[tool.Cost], filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict + cost: type[tool.Cost], + filepath: str, + cost_schedule: ifcopenshell.entity_instance, + options: dict, + force_schedule_type: str = "", ): cost.play_sound() - return cost.export_cost_schedules_to_pdf(filepath, cost_schedule, options) + return cost.export_cost_schedules_to_pdf(filepath, cost_schedule, options, force_schedule_type) def clear_cost_item_assignments( diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index ba34d33fa7..42aa38dbd6 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -846,10 +846,18 @@ class Cost(bonsai.core.tool.Cost): return "Could not open file location" @classmethod - def export_cost_schedules_to_pdf(cls, filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict): + def export_cost_schedules_to_pdf( + cls, filepath: str, cost_schedule: ifcopenshell.entity_instance, options: dict, force_schedule_type: str = "" + ): from ifc5d.ifc5Dspreadsheet import Ifc5DPdfWriter - writer = Ifc5DPdfWriter(file=tool.Ifc.get(), output=filepath, cost_schedule=cost_schedule, options=options) + writer = Ifc5DPdfWriter( + file=tool.Ifc.get(), + output=filepath, + cost_schedule=cost_schedule, + options=options, + force_schedule_type=force_schedule_type, + ) writer.write() @classmethod diff --git a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py index 4cc3b3aa34..0aa9f7ee7c 100644 --- a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py +++ b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py @@ -36,10 +36,13 @@ class CostItem(TypedDict): # Exported columns. Index: int Hierarchy: str + ItemIsASum: int Id: int Identification: Union[str, None] Name: Union[str, None] + Description: Union[str, None] Unit: str + Quantities: str Quantity: Union[float, None] RateSubtotal: float TotalPrice: float @@ -101,6 +104,13 @@ class IfcDataGetter: ) return values + @staticmethod + def cost_item_is_a_sum(cost_item: ifcopenshell.entity_instance) -> bool: + for cost_value in cost_item.CostValues or []: + if cost_value.Category == "*": + return True + return False + @staticmethod def get_cost_items_data( file: ifcopenshell.file, @@ -138,12 +148,14 @@ class IfcDataGetter: data: CostItem = { "Index": index, + "ItemIsASum": IfcDataGetter.cost_item_is_a_sum(cost_item), "Hierarchy": hierarchy, "Id": cost_item.id(), "Identification": cost_item.Identification, "Name": cost_item.Name, "Description": cost_item.Description, "Unit": unit, + "Quantities": IfcDataGetter.serialise_cost_quantities(file, cost_item), "Quantity": quantity_data["quantity"], "RateSubtotal": rate_subtotal, "TotalPrice": total_price, @@ -236,6 +248,33 @@ class IfcDataGetter: "unit": unit, } + @staticmethod + def serialise_cost_quantities(file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance) -> str: + if not cost_item.is_a("IfcCostItem"): + return "" + if cost_item.CostQuantities is None: + return "" + string = "[" + for quantity in cost_item.CostQuantities: + string += '["' + for rel in file.get_inverse(quantity): + if rel.is_a("IfcPropertySet") or rel.is_a("IfcElementQuantity"): + prop_set = rel + # Find elements that have this property set + for prop_rel in file.get_inverse(prop_set): + if prop_rel.is_a("IfcRelDefinesByProperties"): + for obj in prop_rel.RelatedObjects: + if obj.is_a("IfcElement"): + string += obj.Name + " - " + string += quantity.Name + if quantity.is_a("IfcPhysicalSimpleQuantity"): + string += '", ' + str(quantity[3]) + "]," + else: + string += ' ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0],' + string = string.removesuffix(",") + string += "]" + return string + class SheetData(TypedDict): headers: list[str] @@ -301,6 +340,7 @@ class Ifc5Dwriter: cost_items = IfcDataGetter.get_schedule_cost_items_data(self.file, cost_schedule) headers: list[str] = [ "Id", + "ItemIsASum", "Hierarchy", "Index", "Identification", @@ -309,6 +349,7 @@ class Ifc5Dwriter: "Unit", ] if cost_schedule.PredefinedType != "SCHEDULEOFRATES": + headers.insert(-1, "Quantities") headers.insert(-1, "Quantity") headers.extend(["RateSubtotal", "TotalPrice"]) @@ -529,13 +570,15 @@ class Ifc5DPdfWriter(Ifc5Dwriter): file: Union[str, ifcopenshell.file], output: str, options: dict, - cost_schedule: Optional[ifcopenshell.entity_instance] = None, + cost_schedule: ifcopenshell.entity_instance, + force_schedule_type: Optional[str] = None, ): """ PDF Writer is based on typst library, be sure it is available. :param file: IFC file to exprot cost schedules from. :param output: Output file path including name and .pdf extension. - :param cost_schedule: exported cost schedule. If not provided, will export all available schedules. Output will be different accoding to Cost Schedule PredefinedType. + :param cost_schedule: exported cost schedule. Output will be different accoding to Cost Schedule PredefinedType. + :param force_schedule_type: optional parameter to force the output to a specific Schedule Type (suports "PRICEDBILLOFQUANTITIES", "UNPRICEDBILLOFQUANTITIES", "SCHEDULEOFRATES",). """ self.output = output if isinstance(file, str): @@ -543,6 +586,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter): else: self.file = file self.cost_schedule = cost_schedule + self.force_schedule_type = force_schedule_type self.options = options def write(self) -> None: @@ -552,15 +596,54 @@ class Ifc5DPdfWriter(Ifc5Dwriter): import typst import tempfile + DEFAULT_OPTIONS = { + "nested_structure_depth": 0, + "parent_to_new_page_up_to_depth": 0, + "show_only_parents": False, + "should_print_cover": False, + "should_print_cost_ids": True, + "should_print_description": False, + "should_print_each_quantity": True, + "should_print_each_cost_value": False, + "should_print_rates": True, + "should_print_summary": True, + } + + HANDLED_COST_SCHEDULE_TYPES = ( + # Commented predefined types are not handled at the moment + # "BUDGET", + # "COSTPLAN", + # "ESTIMATE", + # "TENDER", + "PRICEDBILLOFQUANTITIES", + "UNPRICEDBILLOFQUANTITIES", + "SCHEDULEOFRATES", + # "USERDEFINED", + # "NOTDEFINED" + ) + with tempfile.TemporaryDirectory() as temp_dir: - project_name = self.file.by_type("IfcProject")[0].Name - schedule_name = getattr(self.cost_schedule, "Name", None) or "Unnamed" - schedule_type = getattr(self.cost_schedule, "PredefinedType", None) or "UNTYPED" + cost_schedule_name = self.cost_schedule.Name or "Unnamed" + if self.force_schedule_type in ("PRICEDBILLOFQUANTITIES", "SCHEDULEOFRATES"): + schedule_type = self.force_schedule_type + elif self.force_schedule_type is None or self.force_schedule_type == "OFF": + schedule_type = self.cost_schedule.PredefinedType + if schedule_type not in HANDLED_COST_SCHEDULE_TYPES: + schedule_type = "PRICEDBILLOFQUANTITIES" + else: + raise ValueError( + "force_schedule_type can be set to OFF, PRICEDBILLOFQUANTITIES, SCHEDULEOFRATES values only." + ) + project_monetary_unit = self.file.by_type("IfcMonetaryUnit") + if project_monetary_unit: + project_currency = project_monetary_unit[0].Currency + else: + project_currency = '""' # export csv file csv_file_writer = Ifc5DCsvWriter(file=self.file, output=temp_dir, cost_schedule=self.cost_schedule) csv_file_writer.write() - csv_file_name = schedule_name + ".csv" + csv_file_name = cost_schedule_name + ".csv" # locate typst template file typst_template_file_path = os.path.join( @@ -572,13 +655,20 @@ class Ifc5DPdfWriter(Ifc5Dwriter): typst_main_content = "" typst_main_content += '#import "{}": *\n'.format("typst_template_ifc_cost_schedule.typ") typst_main_content += "#show: project.with(\n" - typst_main_content += 'schedule_path: "{}",\n'.format(csv_file_name) - typst_main_content += 'title: "{}",\n'.format(project_name) - typst_main_content += 'schedule_name: "{}",\n'.format(schedule_name) - typst_main_content += 'schedule_type: "{}",\n'.format(schedule_type) - typst_main_content += "cover_page: {},\n".format("false") - typst_main_content += "root_items_to_new_page: {},\n".format("false") - typst_main_content += "summary: {},\n".format(str(self.options.get("should_print_summary", False)).lower()) + typst_main_content += ' schedule_path: "{}",\n'.format(csv_file_name) + typst_main_content += ' title: "{}",\n'.format(self.file.by_type("IfcProject")[0].Name) + typst_main_content += ' schedule_name: "{}",\n'.format(cost_schedule_name) + typst_main_content += ' schedule_description: "{}",\n'.format(self.cost_schedule.Description or "") + typst_main_content += ' schedule_type: "{}",\n'.format(schedule_type) + typst_main_content += " project_currency: {},\n".format(project_currency) + for option_name, default_value in DEFAULT_OPTIONS.items(): + value = self.options.get(option_name, default_value) + if isinstance(value, bool): + formatted_value = str(value).lower() + else: + formatted_value = str(value) + typst_main_content += f" {option_name}: {formatted_value},\n" + typst_main_content += ")" typst_main_path = os.path.join(temp_dir, "main.typ") with open(typst_main_path, "w") as typ_file: diff --git a/src/ifc5d/ifc5d/typst_template_ifc_cost_schedule.typ b/src/ifc5d/ifc5d/typst_template_ifc_cost_schedule.typ index 6bd35a1b1f..bdaf68f901 100644 --- a/src/ifc5d/ifc5d/typst_template_ifc_cost_schedule.typ +++ b/src/ifc5d/ifc5d/typst_template_ifc_cost_schedule.typ @@ -124,59 +124,93 @@ -#let arrange_summary_row(row) = { +#let arrange_summary_row(row, options) = { let name = strong(upper(row.at("Name"))) let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))] let total = if row.at("RateSubtotal") == "" {0.0} else {float(row.at("RateSubtotal"))} - if row.at("TotalPrice") != "0.0" { + if row.at("ItemIsASum") == "True" { if row.at("Index") == "1" { // ROOT COST ( - row.at("Hierarchy"), + strong[#row.at("Hierarchy")], name, [], - strong[#format-decimal(float(row.at("TotalPrice")), places: 2)] + if options.at("should_print_rates") { + strong[#format-decimal(float(row.at("TotalPrice")), places: 2)] + } else { + } ) } else { - // SUB CATEGORY + // SUB-SECTION ( row.at("Hierarchy"), table.cell(inset: (left: int(row.at("Index"))*2.5mm))[#upper(row.at("Name"))], - format-decimal(float(row.at("TotalPrice")), places: 2), + if options.at("should_print_rates") { + format-decimal(float(row.at("TotalPrice")), places: 2) + } else { + [] + } + , [], ) } + } else { + () } } -#let arrange_bill_of_quantity_row(row) = { - if row.at("TotalPrice") != "0.0" { - // CATEGORY - let name = strong(upper(row.at("Name"))) - let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))] - let total_price = format-decimal(float(row.at("TotalPrice", default: "0.0")), places: 2) - - ( - [], [], [], [], [], [], [], [], [], - ) - ( - table.cell(..root-cost-cell-style)[#row.at("Hierarchy")], - table.cell(..root-cost-cell-style)[#strong(upper(row.at("Name"))) #linebreak() #row.at("Description", default:"")], - table.cell(..root-cost-cell-style)[], - table.cell(..root-cost-cell-style)[], - table.cell(..root-cost-cell-style)[], - table.cell(..root-cost-cell-style)[], - table.cell(..root-cost-cell-style)[], - table.cell(..root-cost-cell-style)[], - table.cell(..root-cost-cell-style)[#strong(total_price)], - ) +#let arrange_bill_of_quantity_row(row, options) = { + if row.at("ItemIsASum") == "True" { + // SECTION (Parent Cost Item) + if options.at("nested_structure_depth") == 0 or int(row.at("Index")) <= options.at("nested_structure_depth") { + let name = strong(upper(row.at("Name"))) + let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))] + let total_price = format-decimal(float(row.at("TotalPrice", default: "0.0")), places: 2) + + ( + [], [], [], [], [], [], [], [], [], + ) + ( + table.cell(..root-cost-cell-style)[#row.at("Hierarchy")], + table.cell(..root-cost-cell-style)[#strong(upper(row.at("Name"))) #linebreak() #row.at("Description", default:"")], + table.cell(..root-cost-cell-style)[], + table.cell(..root-cost-cell-style)[], + table.cell(..root-cost-cell-style)[], + table.cell(..root-cost-cell-style)[], + table.cell(..root-cost-cell-style)[], + table.cell(..root-cost-cell-style)[], + if options.at("should_print_rates") == true { + table.cell(..root-cost-cell-style)[#strong(total_price)] + } else{ + table.cell(..root-cost-cell-style)[] + }, + ) + } else { + () + } } else { // COST ITEM - let name = strong(upper(row.at("Name"))) - let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))] + let name = "" + if row.at("Name") == "" { + name = strong(upper("Unnamed Cost Item")) + } else { + name = strong(upper(row.at("Name"))) + } + let identification = "" + if options.at("should_print_cost_ids") == true and row.at("Identification") != "" { + identification = linebreak() + row.at("Identification") + } else { + identification = "" + } + let description = "" + if options.at("should_print_description") == true and row.at("Description") != "" { + description = [#par(justify: true, text(8pt, row.at("Description", default: "")))] + } else { + description = "" + } let unit = table.cell(align: right)[Sum #unit_map.at(row.at("Unit"), default: "")] let quant = if row.at("Quantity") == "" {0.0} else { format-decimal(float(row.at("Quantity")))} @@ -187,7 +221,7 @@ ( row.at("Hierarchy"), - if row.at("Identification") == "" {name + linebreak() + description} else {name + linebreak() + row.at("Identification") + linebreak() + description}, + name + identification + description, [], [], [], @@ -196,30 +230,61 @@ [], [], ) - ( - [], - unit, - [], - [], - [], - [], - table.cell(..total-cell-style, align: right + bottom)[#quant], - table.cell(..total-cell-style, align: right + bottom)[#rate], - table.cell(..total-cell-style, align: right + bottom)[#total], - ) - + if row.at("Quantities") != "" and options.at("should_print_each_quantity") { + let json_str = row.at("Quantities") + let quantites = json.decode(json_str) + for quantity in quantites { + ( + [], + if quantity.at(0) == "Unnamed" {[quantity]} else {quantity.at(0)}, + [], + [], + [], + [], + format-decimal(quantity.at(1)), + [], + [], + ) + } + } + if options.at("should_print_rates") == true { + ( + [], + unit, + [], + [], + [], + [], + table.cell(..total-cell-style, align: right + bottom)[#quant], + table.cell(..total-cell-style, align: right + bottom)[#rate], + table.cell(..total-cell-style, align: right + bottom)[#total], + ) + } else { + ( + [], + unit, + [], + [], + [], + [], + table.cell(..total-cell-style, align: right + bottom)[#quant], + [.................], + [.......................], + ) + } } } -#let arrange_schedule_of_rates_row(row) = { +#let arrange_schedule_of_rates_row(row, options) = { let name = strong(upper(row.at("Name"))) let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))] let unit = table.cell(align: right)[#unit_map.at(row.at("Unit"), default: "")] let rate = if row.at("RateSubtotal") == "" {0.0} else { format-decimal(float(row.at("RateSubtotal")))} + if row.at("ItemIsASum") == "True" {return ()} //skip sections in schedule of rates ( row.at("Identification"), if row.at("Identification") == "" {name + linebreak() + description} else {name + linebreak() + description}, @@ -241,11 +306,11 @@ path, delimiter: ",", type: "PRICEDBILLOFQUANTITIES", - should_hide_rates: false + options: () ) = { if type == "PRICEDBILLOFQUANTITIES" or type == "UNPRICEDBILLOFQUANTITIES"{ let data = csv(path, delimiter: delimiter, row-type: dictionary) - let new_rows = data.map(arrange_bill_of_quantity_row) + let new_rows = data.map(item => arrange_bill_of_quantity_row(item, options)) table( columns: (18mm,1fr, 12mm,12mm,12mm,12mm, 20mm, 20mm, 25mm), @@ -253,10 +318,11 @@ stroke: none, ..new_rows.flatten() ) + } else if type == "SCHEDULEOFRATES" { // REMEMBER TO CHECK IF THE ROW IS UNIQUE let data = csv(path, delimiter: delimiter, row-type: dictionary) - let new_rows = data.map(arrange_schedule_of_rates_row) + let new_rows = data.map(item => arrange_schedule_of_rates_row(item, options)) table( columns: (30mm,130mm, 25mm), @@ -271,10 +337,11 @@ #let create-summary( path, - delimiter: "," + delimiter: ",", + options ) = { let data = csv(path, delimiter: delimiter, row-type: dictionary) - let new_rows = data.map(arrange_summary_row) + let new_rows = data.map(item => arrange_summary_row(item, options)) let general_total = data.filter(row => row.at("Index") == "1") .map(row => float(row.at("TotalPrice"))) .sum(default: 0.00) @@ -301,25 +368,92 @@ align: (center, right, center, right), inset: 1mm, fill: gray.transparentize(70%), - [], strong[GENERAL TOTAL:], [],[#strong(format-decimal(general_total, places: 2))] + [], strong[GENERAL TOTAL:], [], + if options.at("should_print_rates"){[#strong(format-decimal(general_total, places: 2))] + } else { + [] + } ) } +#let create-cover( + title, + schedule_name, + schedule_description, + schedule_type, +) = { + set page( + numbering: none, + margin: (top: 35mm, left: 20mm, right: 10mm), + background: place( top + left, dx: 15mm, dy: 25mm, + table( + columns: 185mm, + rows: 254mm, + align: (center, left, center), + stroke: 1pt + ) + ), + footer: [ + #set text(size: 7pt, fill: gray) + #align(right)[#linebreak()powered by IfcOpenShell] + ] + ) + set text(font: template_fonts, size: 12pt) + place( bottom + left, dx: 0mm, dy: -10mm, + grid( + columns: (30mm, 135mm), + gutter: 2em, + align: top + left, + [Title:], [*#title*], + [Schedule:],[*#schedule_name*], + [Schedule Type:], [#schedule_type], + if schedule_description != "" {[Description:]}, + if schedule_description != "" {[#schedule_description]}, + [],[], + [#datetime.today().display("[day]/[month]/[year]")],[Signed], + [],[.................................], + ) + ) + + +} + + + #let project( + schedule_path: "", title: "", - schedule_name: "", + schedule_name: "", + schedule_description: "", schedule_type: "", - cover_page: bool, - root_items_to_new_page: bool, - should_hide_rates: false, - summary: bool, - body) = { - // Set the document's basic properties. - //set document(schedule: schedule_name, title: title) - + project_currency: str, + nested_structure_depth: int, + parent_to_new_page_up_to_depth: int, + show_only_parents: bool, + should_print_cover: bool, + should_print_cost_ids: bool, + should_print_description: bool, + should_print_each_quantity: bool, + should_print_each_cost_value: bool, + should_print_rates: bool, + should_print_summary: bool, + + body) = { + + if should_print_cover { + create-cover( + title, + schedule_name, + schedule_description, + schedule_type + ) + pagebreak() + counter(page).update(n => n - 1) + } + set page( margin: (left: 15mm, right: 10mm, top: 35mm, bottom: 20mm), numbering: "1/1", @@ -350,18 +484,39 @@ ) set text(font: template_fonts, size: 8pt, lang: "en"); + + let options = ( + "nested_structure_depth": nested_structure_depth, + "parent_to_new_page_up_to_depth": parent_to_new_page_up_to_depth, + "show_only_parents": show_only_parents, + "should_print_cost_ids": should_print_cost_ids, + "should_print_description": should_print_description, + "should_print_each_quantity": should_print_each_quantity, + "should_print_each_cost_value": should_print_each_cost_value, + "should_print_rates": should_print_rates, + ) - if schedule_type == "PRICEDBILLOFQUANTITIES" { - create-schedule(schedule_path, type: "PRICEDBILLOFQUANTITIES", should_hide_rates: should_hide_rates) - } else if schedule_type == "UNPRICEDBILLOFQUANTITIES" { - create-schedule(schedule_path, type: "PRICEDBILLOFQUANTITIES", should_hide_rates: true) + if schedule_type == "UNPRICEDBILLOFQUANTITIES" { + create-schedule( + schedule_path, + type: "PRICEDBILLOFQUANTITIES", + options: options + ) } else if schedule_type == "SCHEDULEOFRATES" { - create-schedule(schedule_path, type: "SCHEDULEOFRATES", should_hide_rates: true) + create-schedule( + schedule_path, + type: "SCHEDULEOFRATES", + options: options + ) } else { - create-schedule(schedule_path, type: "PRICEDBILLOFQUANTITIES", should_hide_rates: should_hide_rates) + create-schedule( + schedule_path, + type: "PRICEDBILLOFQUANTITIES", + options: options + ) } - if summary == true { + if should_print_summary and schedule_type != "SCHEDULEOFRATES"{ pagebreak() set text(font: template_fonts, size: 8pt, lang: "en"); set page( @@ -370,6 +525,6 @@ format_table.at("SUMMARY") ) ) - create-summary(schedule_path) + create-summary(schedule_path, options) } } \ No newline at end of file