Created
June 18, 2025 17:26
-
-
Save tibicen/f349330c231e5cf0ab0a6cd7d9d375c8 to your computer and use it in GitHub Desktop.
Typst IFC documents
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// PRICED BILL OF QUANTITIES TEMPLATE | |
// author: carlo pavan | |
// year: 2025 | |
#let euro(num) = { | |
str(calc.round(float(num), digits: 2)) + " €" | |
} | |
#let unit_map = ( | |
"METRE": "m", | |
"SQUARE_METRE": "m²", | |
"m2": "m²", | |
"CUBIC_METRE": "m³", | |
"m3": "m³", | |
"VOLUMEUNIT / CUBIC_METRE": "m³", | |
"KILOGRAM": "kg", | |
// add more mappings as needed | |
) | |
#let format-decimal(num, places: 2) = { | |
let rounded = calc.round(num, digits: places) | |
let str-num = str(rounded) | |
// Split into integer and decimal parts | |
let parts = str-num.split(".") | |
let integer-part = parts.at(0) | |
let decimal-part = parts.at(1, default: "") | |
// Add thousand separators to integer part | |
let formatted-integer = "" | |
let chars = integer-part.clusters().rev() | |
for (i, char) in chars.enumerate() { | |
if i > 0 and calc.rem(i, 3) == 0 { | |
formatted-integer = "'" + formatted-integer | |
} | |
formatted-integer = char + formatted-integer | |
} | |
// Ensure decimal part has correct number of places | |
decimal-part = decimal-part + "0" * (places - decimal-part.len()) | |
formatted-integer + "." + decimal-part | |
} | |
#let read-csv(path, delimiter: ",") = { | |
let lines = read(path).split("\n").filter(l => l != "") | |
let header = lines.at(0).split(delimiter).map(f => f.trim()) | |
let rows = lines.slice(1).map(line => { | |
let values = line.split(delimiter).map(f => f.trim()) | |
let row-dict = (:) | |
for (i, col) in header.enumerate() { | |
row-dict.insert(col, values.at(i, default: "")) | |
} | |
row-dict | |
}) | |
(header: header, rows: rows) | |
} | |
#let arrange_row(row) = { | |
let description = [#text(8pt,weight: "bold", row.at(4)) \ #text(8pt, row.at(3)) \ #text(8pt, lorem(25)) \ #align(right)[Sum]] | |
let quant = if row.at(5) == "" {0.0} else {float(row.at(5))} | |
let rate = if row.at(7) == "" {0.0} else {float(row.at(7))} | |
let total = if row.at(8) == "" {0.0} else {float(row.at(8))} | |
( | |
row.at(1), | |
description, | |
[], | |
[], | |
[], | |
[], | |
align(right + bottom)[#format-decimal(quant)], | |
align(right + bottom)[#format-decimal(rate)], | |
align(right + bottom)[#format-decimal(total)], | |
) | |
} | |
#let csv-table-schedule(path, delimiter: ",") = { | |
// let data = read-csv(path, delimiter: delimiter) | |
// let new_rows = () | |
let data = csv(path, delimiter: delimiter) | |
let header = data.remove(0) | |
let new_rows = data.map(arrange_row) // TODO: Do transformation on data to fit your needs | |
table( | |
columns: (auto,1fr, 12mm,12mm,12mm,12mm, auto, auto, auto), | |
align: (left, left, center, center, center, center, right, right, right), | |
stroke: (x, y) => ( | |
left: if x == 0 { 1pt } else { 0.25pt }, | |
right: 1pt, | |
top: if y < 2 {1pt} else {0pt}, | |
bottom: 1pt | |
), | |
table.header([Hierarchy], [Description], [n°],[l],[w],[h/w], [Quantity], [Rate], [Total]), | |
..new_rows.flatten() | |
) | |
} | |
#let csv-table-summary(path, delimiter: ",") = { | |
let data = read-csv(path, delimiter: delimiter) | |
let new_rows = () | |
for row in data.rows { | |
let new-cell = () | |
if row.at("TotalPrice") != "0.0" { | |
new-cell.push(row.at("Hierarchy")) | |
new-cell.push(row.at("Name")) | |
new-cell.push(row.at("General Cost")) | |
new-cell.push(format-decimal(float(row.at("TotalPrice")), places: 2)) | |
} | |
new_rows.push(new-cell) | |
} | |
set text(size: 10pt) | |
pad(left: 2cm)[SUMMARY:] | |
set text(size: 8pt) | |
table( | |
columns: (18mm,107mm, 30mm, 30mm), | |
align: (center, left, right, right), | |
stroke: (x, y) => ( | |
left: none, | |
right: none, | |
top: (dash: "dotted"), | |
bottom: (dash: "dotted") | |
), | |
..new_rows.flatten() | |
) | |
} | |
#let project( | |
title: "", | |
schedule_name: "", | |
cover_page: bool, | |
root_items_to_new_page: bool, | |
summary: bool, | |
body) = { | |
// Set the document's basic properties. | |
//set document(schedule: schedule_name, title: title) | |
set page( | |
margin: (left: 15mm, right: 10mm, top: 35mm, bottom: 20mm), | |
numbering: "1/1", | |
number-align: end, | |
header:[ | |
#set text(font: "Liberation Sans", size: 9pt, lang: "en"); | |
#title | |
#h(1fr) | |
#schedule_name | |
], | |
footer: context [ | |
#datetime.today().display("[day]/[month]/[year]") | |
#h(1fr) | |
#counter(page).display("1/1", both: true) | |
], | |
) | |
set text(font: "Liberation Sans", size: 8pt, lang: "en"); | |
csv-table-schedule("schedule.csv") | |
if summary == true { | |
pagebreak() | |
set text(font: "Liberation Sans", size: 8pt, lang: "en"); | |
set page( | |
background: | |
place( top + left, dx: 15mm, dy: 25mm, | |
table(columns: (18mm,107mm, 30mm, 30mm), | |
rows: (6mm, 245mm), | |
align: (center, left, center, center, center, center, center, center, center), | |
stroke: (x, y) => ( | |
left: if x == 0 { 1pt } else { 0.25pt }, | |
right: 1pt, | |
top: 1pt, | |
bottom: 1pt | |
), | |
text(size: 8pt)[Hierarchy], | |
text(size: 8pt)[Description], | |
text(size: 8pt)[Sub Total], | |
text(size: 8pt)[Total] | |
) | |
) | |
) | |
set text(font: "Liberation Sans", size: 8pt, lang: "en"); | |
csv-table-summary("schedule.csv") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment