mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-08 13:26:34 +00:00
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
This commit is contained in:
@@ -871,11 +871,82 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
|
|||||||
items=get_cost_schedules_enum_items,
|
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(
|
should_print_summary: bpy.props.BoolProperty(
|
||||||
name="Should print summary",
|
name="Final Summary",
|
||||||
description="Print summary at the end of the document",
|
description="Print summary at the end of the document",
|
||||||
default=True,
|
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):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
@@ -884,8 +955,18 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
|
|||||||
box.prop(self, "cost_schedules_enum", text="")
|
box.prop(self, "cost_schedules_enum", text="")
|
||||||
layout.separator()
|
layout.separator()
|
||||||
box = layout.box()
|
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, "should_print_summary")
|
||||||
|
box.prop(self, "force_schedule_type")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -920,9 +1001,25 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
|
|||||||
file = tool.Ifc.get()
|
file = tool.Ifc.get()
|
||||||
self.props = tool.Cost.get_cost_props()
|
self.props = tool.Cost.get_cost_props()
|
||||||
cost_schedule = file.by_id(int(self.cost_schedules_enum))
|
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(
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -404,10 +404,14 @@ def export_cost_schedules(
|
|||||||
|
|
||||||
|
|
||||||
def export_cost_schedules_to_pdf(
|
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()
|
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(
|
def clear_cost_item_assignments(
|
||||||
|
|||||||
@@ -846,10 +846,18 @@ class Cost(bonsai.core.tool.Cost):
|
|||||||
return "Could not open file location"
|
return "Could not open file location"
|
||||||
|
|
||||||
@classmethod
|
@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
|
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()
|
writer.write()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -36,10 +36,13 @@ class CostItem(TypedDict):
|
|||||||
# Exported columns.
|
# Exported columns.
|
||||||
Index: int
|
Index: int
|
||||||
Hierarchy: str
|
Hierarchy: str
|
||||||
|
ItemIsASum: int
|
||||||
Id: int
|
Id: int
|
||||||
Identification: Union[str, None]
|
Identification: Union[str, None]
|
||||||
Name: Union[str, None]
|
Name: Union[str, None]
|
||||||
|
Description: Union[str, None]
|
||||||
Unit: str
|
Unit: str
|
||||||
|
Quantities: str
|
||||||
Quantity: Union[float, None]
|
Quantity: Union[float, None]
|
||||||
RateSubtotal: float
|
RateSubtotal: float
|
||||||
TotalPrice: float
|
TotalPrice: float
|
||||||
@@ -101,6 +104,13 @@ class IfcDataGetter:
|
|||||||
)
|
)
|
||||||
return values
|
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
|
@staticmethod
|
||||||
def get_cost_items_data(
|
def get_cost_items_data(
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
@@ -138,12 +148,14 @@ class IfcDataGetter:
|
|||||||
|
|
||||||
data: CostItem = {
|
data: CostItem = {
|
||||||
"Index": index,
|
"Index": index,
|
||||||
|
"ItemIsASum": IfcDataGetter.cost_item_is_a_sum(cost_item),
|
||||||
"Hierarchy": hierarchy,
|
"Hierarchy": hierarchy,
|
||||||
"Id": cost_item.id(),
|
"Id": cost_item.id(),
|
||||||
"Identification": cost_item.Identification,
|
"Identification": cost_item.Identification,
|
||||||
"Name": cost_item.Name,
|
"Name": cost_item.Name,
|
||||||
"Description": cost_item.Description,
|
"Description": cost_item.Description,
|
||||||
"Unit": unit,
|
"Unit": unit,
|
||||||
|
"Quantities": IfcDataGetter.serialise_cost_quantities(file, cost_item),
|
||||||
"Quantity": quantity_data["quantity"],
|
"Quantity": quantity_data["quantity"],
|
||||||
"RateSubtotal": rate_subtotal,
|
"RateSubtotal": rate_subtotal,
|
||||||
"TotalPrice": total_price,
|
"TotalPrice": total_price,
|
||||||
@@ -236,6 +248,33 @@ class IfcDataGetter:
|
|||||||
"unit": unit,
|
"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):
|
class SheetData(TypedDict):
|
||||||
headers: list[str]
|
headers: list[str]
|
||||||
@@ -301,6 +340,7 @@ class Ifc5Dwriter:
|
|||||||
cost_items = IfcDataGetter.get_schedule_cost_items_data(self.file, cost_schedule)
|
cost_items = IfcDataGetter.get_schedule_cost_items_data(self.file, cost_schedule)
|
||||||
headers: list[str] = [
|
headers: list[str] = [
|
||||||
"Id",
|
"Id",
|
||||||
|
"ItemIsASum",
|
||||||
"Hierarchy",
|
"Hierarchy",
|
||||||
"Index",
|
"Index",
|
||||||
"Identification",
|
"Identification",
|
||||||
@@ -309,6 +349,7 @@ class Ifc5Dwriter:
|
|||||||
"Unit",
|
"Unit",
|
||||||
]
|
]
|
||||||
if cost_schedule.PredefinedType != "SCHEDULEOFRATES":
|
if cost_schedule.PredefinedType != "SCHEDULEOFRATES":
|
||||||
|
headers.insert(-1, "Quantities")
|
||||||
headers.insert(-1, "Quantity")
|
headers.insert(-1, "Quantity")
|
||||||
headers.extend(["RateSubtotal", "TotalPrice"])
|
headers.extend(["RateSubtotal", "TotalPrice"])
|
||||||
|
|
||||||
@@ -529,13 +570,15 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
|
|||||||
file: Union[str, ifcopenshell.file],
|
file: Union[str, ifcopenshell.file],
|
||||||
output: str,
|
output: str,
|
||||||
options: dict,
|
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.
|
PDF Writer is based on typst library, be sure it is available.
|
||||||
:param file: IFC file to exprot cost schedules from.
|
:param file: IFC file to exprot cost schedules from.
|
||||||
:param output: Output file path including name and .pdf extension.
|
: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
|
self.output = output
|
||||||
if isinstance(file, str):
|
if isinstance(file, str):
|
||||||
@@ -543,6 +586,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
|
|||||||
else:
|
else:
|
||||||
self.file = file
|
self.file = file
|
||||||
self.cost_schedule = cost_schedule
|
self.cost_schedule = cost_schedule
|
||||||
|
self.force_schedule_type = force_schedule_type
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
def write(self) -> None:
|
def write(self) -> None:
|
||||||
@@ -552,15 +596,54 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
|
|||||||
import typst
|
import typst
|
||||||
import tempfile
|
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:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
project_name = self.file.by_type("IfcProject")[0].Name
|
cost_schedule_name = self.cost_schedule.Name or "Unnamed"
|
||||||
schedule_name = getattr(self.cost_schedule, "Name", None) or "Unnamed"
|
if self.force_schedule_type in ("PRICEDBILLOFQUANTITIES", "SCHEDULEOFRATES"):
|
||||||
schedule_type = getattr(self.cost_schedule, "PredefinedType", None) or "UNTYPED"
|
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
|
# export csv file
|
||||||
csv_file_writer = Ifc5DCsvWriter(file=self.file, output=temp_dir, cost_schedule=self.cost_schedule)
|
csv_file_writer = Ifc5DCsvWriter(file=self.file, output=temp_dir, cost_schedule=self.cost_schedule)
|
||||||
csv_file_writer.write()
|
csv_file_writer.write()
|
||||||
csv_file_name = schedule_name + ".csv"
|
csv_file_name = cost_schedule_name + ".csv"
|
||||||
|
|
||||||
# locate typst template file
|
# locate typst template file
|
||||||
typst_template_file_path = os.path.join(
|
typst_template_file_path = os.path.join(
|
||||||
@@ -572,13 +655,20 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
|
|||||||
typst_main_content = ""
|
typst_main_content = ""
|
||||||
typst_main_content += '#import "{}": *\n'.format("typst_template_ifc_cost_schedule.typ")
|
typst_main_content += '#import "{}": *\n'.format("typst_template_ifc_cost_schedule.typ")
|
||||||
typst_main_content += "#show: project.with(\n"
|
typst_main_content += "#show: project.with(\n"
|
||||||
typst_main_content += 'schedule_path: "{}",\n'.format(csv_file_name)
|
typst_main_content += ' schedule_path: "{}",\n'.format(csv_file_name)
|
||||||
typst_main_content += 'title: "{}",\n'.format(project_name)
|
typst_main_content += ' title: "{}",\n'.format(self.file.by_type("IfcProject")[0].Name)
|
||||||
typst_main_content += 'schedule_name: "{}",\n'.format(schedule_name)
|
typst_main_content += ' schedule_name: "{}",\n'.format(cost_schedule_name)
|
||||||
typst_main_content += 'schedule_type: "{}",\n'.format(schedule_type)
|
typst_main_content += ' schedule_description: "{}",\n'.format(self.cost_schedule.Description or "")
|
||||||
typst_main_content += "cover_page: {},\n".format("false")
|
typst_main_content += ' schedule_type: "{}",\n'.format(schedule_type)
|
||||||
typst_main_content += "root_items_to_new_page: {},\n".format("false")
|
typst_main_content += " project_currency: {},\n".format(project_currency)
|
||||||
typst_main_content += "summary: {},\n".format(str(self.options.get("should_print_summary", False)).lower())
|
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_content += ")"
|
||||||
typst_main_path = os.path.join(temp_dir, "main.typ")
|
typst_main_path = os.path.join(temp_dir, "main.typ")
|
||||||
with open(typst_main_path, "w") as typ_file:
|
with open(typst_main_path, "w") as typ_file:
|
||||||
|
|||||||
@@ -124,59 +124,93 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
#let arrange_summary_row(row) = {
|
#let arrange_summary_row(row, options) = {
|
||||||
let name = strong(upper(row.at("Name")))
|
let name = strong(upper(row.at("Name")))
|
||||||
let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))]
|
let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))]
|
||||||
let total = if row.at("RateSubtotal") == "" {0.0} else {float(row.at("RateSubtotal"))}
|
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" {
|
if row.at("Index") == "1" {
|
||||||
// ROOT COST
|
// ROOT COST
|
||||||
(
|
(
|
||||||
row.at("Hierarchy"),
|
strong[#row.at("Hierarchy")],
|
||||||
name,
|
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 {
|
} else {
|
||||||
// SUB CATEGORY
|
// SUB-SECTION
|
||||||
(
|
(
|
||||||
row.at("Hierarchy"),
|
row.at("Hierarchy"),
|
||||||
table.cell(inset: (left: int(row.at("Index"))*2.5mm))[#upper(row.at("Name"))],
|
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) = {
|
#let arrange_bill_of_quantity_row(row, options) = {
|
||||||
if row.at("TotalPrice") != "0.0" {
|
if row.at("ItemIsASum") == "True" {
|
||||||
// CATEGORY
|
// SECTION (Parent Cost Item)
|
||||||
let name = strong(upper(row.at("Name")))
|
if options.at("nested_structure_depth") == 0 or int(row.at("Index")) <= options.at("nested_structure_depth") {
|
||||||
let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))]
|
let name = strong(upper(row.at("Name")))
|
||||||
let total_price = format-decimal(float(row.at("TotalPrice", default: "0.0")), places: 2)
|
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)[#row.at("Hierarchy")],
|
||||||
table.cell(..root-cost-cell-style)[],
|
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)[],
|
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)],
|
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 {
|
} else {
|
||||||
// COST ITEM
|
// COST ITEM
|
||||||
let name = strong(upper(row.at("Name")))
|
let name = ""
|
||||||
let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))]
|
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 unit = table.cell(align: right)[Sum #unit_map.at(row.at("Unit"), default: "")]
|
||||||
let quant = if row.at("Quantity") == "" {0.0} else {
|
let quant = if row.at("Quantity") == "" {0.0} else {
|
||||||
format-decimal(float(row.at("Quantity")))}
|
format-decimal(float(row.at("Quantity")))}
|
||||||
@@ -187,7 +221,7 @@
|
|||||||
|
|
||||||
(
|
(
|
||||||
row.at("Hierarchy"),
|
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 @@
|
|||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
(
|
if row.at("Quantities") != "" and options.at("should_print_each_quantity") {
|
||||||
[],
|
let json_str = row.at("Quantities")
|
||||||
unit,
|
let quantites = json.decode(json_str)
|
||||||
[],
|
for quantity in quantites {
|
||||||
[],
|
(
|
||||||
[],
|
[],
|
||||||
[],
|
if quantity.at(0) == "Unnamed" {[quantity]} else {quantity.at(0)},
|
||||||
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],
|
[],
|
||||||
)
|
[],
|
||||||
|
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 name = strong(upper(row.at("Name")))
|
||||||
let description = [#par(justify: true, text(8pt, row.at("Description", default: "")))]
|
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 unit = table.cell(align: right)[#unit_map.at(row.at("Unit"), default: "")]
|
||||||
let rate = if row.at("RateSubtotal") == "" {0.0} else {
|
let rate = if row.at("RateSubtotal") == "" {0.0} else {
|
||||||
format-decimal(float(row.at("RateSubtotal")))}
|
format-decimal(float(row.at("RateSubtotal")))}
|
||||||
|
if row.at("ItemIsASum") == "True" {return ()} //skip sections in schedule of rates
|
||||||
(
|
(
|
||||||
row.at("Identification"),
|
row.at("Identification"),
|
||||||
if row.at("Identification") == "" {name + linebreak() + description} else {name + linebreak() + description},
|
if row.at("Identification") == "" {name + linebreak() + description} else {name + linebreak() + description},
|
||||||
@@ -241,11 +306,11 @@
|
|||||||
path,
|
path,
|
||||||
delimiter: ",",
|
delimiter: ",",
|
||||||
type: "PRICEDBILLOFQUANTITIES",
|
type: "PRICEDBILLOFQUANTITIES",
|
||||||
should_hide_rates: false
|
options: ()
|
||||||
) = {
|
) = {
|
||||||
if type == "PRICEDBILLOFQUANTITIES" or type == "UNPRICEDBILLOFQUANTITIES"{
|
if type == "PRICEDBILLOFQUANTITIES" or type == "UNPRICEDBILLOFQUANTITIES"{
|
||||||
let data = csv(path, delimiter: delimiter, row-type: dictionary)
|
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(
|
table(
|
||||||
columns: (18mm,1fr, 12mm,12mm,12mm,12mm, 20mm, 20mm, 25mm),
|
columns: (18mm,1fr, 12mm,12mm,12mm,12mm, 20mm, 20mm, 25mm),
|
||||||
@@ -253,10 +318,11 @@
|
|||||||
stroke: none,
|
stroke: none,
|
||||||
..new_rows.flatten()
|
..new_rows.flatten()
|
||||||
)
|
)
|
||||||
|
|
||||||
} else if type == "SCHEDULEOFRATES" {
|
} else if type == "SCHEDULEOFRATES" {
|
||||||
// REMEMBER TO CHECK IF THE ROW IS UNIQUE
|
// REMEMBER TO CHECK IF THE ROW IS UNIQUE
|
||||||
let data = csv(path, delimiter: delimiter, row-type: dictionary)
|
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(
|
table(
|
||||||
columns: (30mm,130mm, 25mm),
|
columns: (30mm,130mm, 25mm),
|
||||||
@@ -271,10 +337,11 @@
|
|||||||
|
|
||||||
#let create-summary(
|
#let create-summary(
|
||||||
path,
|
path,
|
||||||
delimiter: ","
|
delimiter: ",",
|
||||||
|
options
|
||||||
) = {
|
) = {
|
||||||
let data = csv(path, delimiter: delimiter, row-type: dictionary)
|
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")
|
let general_total = data.filter(row => row.at("Index") == "1")
|
||||||
.map(row => float(row.at("TotalPrice")))
|
.map(row => float(row.at("TotalPrice")))
|
||||||
.sum(default: 0.00)
|
.sum(default: 0.00)
|
||||||
@@ -301,25 +368,92 @@
|
|||||||
align: (center, right, center, right),
|
align: (center, right, center, right),
|
||||||
inset: 1mm,
|
inset: 1mm,
|
||||||
fill: gray.transparentize(70%),
|
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(
|
#let project(
|
||||||
|
|
||||||
schedule_path: "",
|
schedule_path: "",
|
||||||
title: "",
|
title: "",
|
||||||
schedule_name: "",
|
schedule_name: "",
|
||||||
|
schedule_description: "",
|
||||||
schedule_type: "",
|
schedule_type: "",
|
||||||
cover_page: bool,
|
project_currency: str,
|
||||||
root_items_to_new_page: bool,
|
nested_structure_depth: int,
|
||||||
should_hide_rates: false,
|
parent_to_new_page_up_to_depth: int,
|
||||||
summary: bool,
|
show_only_parents: bool,
|
||||||
body) = {
|
should_print_cover: bool,
|
||||||
// Set the document's basic properties.
|
should_print_cost_ids: bool,
|
||||||
//set document(schedule: schedule_name, title: title)
|
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(
|
set page(
|
||||||
margin: (left: 15mm, right: 10mm, top: 35mm, bottom: 20mm),
|
margin: (left: 15mm, right: 10mm, top: 35mm, bottom: 20mm),
|
||||||
numbering: "1/1",
|
numbering: "1/1",
|
||||||
@@ -350,18 +484,39 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
set text(font: template_fonts, size: 8pt, lang: "en");
|
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" {
|
if schedule_type == "UNPRICEDBILLOFQUANTITIES" {
|
||||||
create-schedule(schedule_path, type: "PRICEDBILLOFQUANTITIES", should_hide_rates: should_hide_rates)
|
create-schedule(
|
||||||
} else if schedule_type == "UNPRICEDBILLOFQUANTITIES" {
|
schedule_path,
|
||||||
create-schedule(schedule_path, type: "PRICEDBILLOFQUANTITIES", should_hide_rates: true)
|
type: "PRICEDBILLOFQUANTITIES",
|
||||||
|
options: options
|
||||||
|
)
|
||||||
} else if schedule_type == "SCHEDULEOFRATES" {
|
} else if schedule_type == "SCHEDULEOFRATES" {
|
||||||
create-schedule(schedule_path, type: "SCHEDULEOFRATES", should_hide_rates: true)
|
create-schedule(
|
||||||
|
schedule_path,
|
||||||
|
type: "SCHEDULEOFRATES",
|
||||||
|
options: options
|
||||||
|
)
|
||||||
} else {
|
} 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()
|
pagebreak()
|
||||||
set text(font: template_fonts, size: 8pt, lang: "en");
|
set text(font: template_fonts, size: 8pt, lang: "en");
|
||||||
set page(
|
set page(
|
||||||
@@ -370,6 +525,6 @@
|
|||||||
format_table.at("SUMMARY")
|
format_table.at("SUMMARY")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
create-summary(schedule_path)
|
create-summary(schedule_path, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user