""" Create / update the custom Print Format "Soumission TARGO" on ERPNext. Design ------ - Same CSS as "Facture TARGO" (invoice) — consistent branding. - Doctype: Quotation. - Right column: total estimé + QR code → DocuSeal signing link + "Signer" CTA. No payment section, no SOMMAIRE DU COMPTE, no CPRST notice. - Language-aware: SOUMISSION (fr) / QUOTE (en) driven by Customer.language. - Items grouped by Quotation Item.service_location (custom field, optional — falls back gracefully if the field is absent on the child table). - QR code links to DocuSeal signing URL stored in custom field `custom_docuseal_signing_url` on the Quotation. If blank, a placeholder is shown (URL will be populated by n8n after envelope creation). Custom fields required on Quotation (create via ERPNext Customize Form): - custom_docuseal_signing_url (Data, read-only) — populated by n8n - custom_quote_type (Select: residential/commercial) - custom_docuseal_envelope_id (Data, read-only) Run (inside erpnext-backend-1) ------------------------------ docker cp scripts/migration/setup_quote_print_format.py \\ erpnext-backend-1:/tmp/setup_quote_print_format.py docker exec erpnext-backend-1 \\ /home/frappe/frappe-bench/env/bin/python /tmp/setup_quote_print_format.py """ import os os.chdir("/home/frappe/frappe-bench/sites") import frappe frappe.init(site="erp.gigafibre.ca", sites_path=".") frappe.connect() print("Connected:", frappe.local.site) PRINT_FORMAT_NAME = "Soumission TARGO" html_template = r"""{#- ══════════════════════════════════════════════════════════════════════════ Soumission TARGO — ERPNext Print Format (Quotation) Same CSS as Facture TARGO; right column replaced with DocuSeal signing CTA. Depends on: - custom app `gigafibre_utils` (QR + logo + short_item_name helpers) - PDF generator set to `chrome` - Custom fields on Quotation: custom_docuseal_signing_url, custom_quote_type ══════════════════════════════════════════════════════════════════════════ -#} {%- macro money(v) -%}{{ "%.2f"|format((v or 0)|float) }}{%- endmacro -%} {%- macro _clean(s) -%}{{ (s or "") | replace("'","'") | replace("&","&") | replace("<","<") | replace(">",">") | replace(""",'"') }}{%- endmacro -%} {%- set mois_fr = {"January":"janvier","February":"février","March":"mars","April":"avril","May":"mai","June":"juin","July":"juillet","August":"août","September":"septembre","October":"octobre","November":"novembre","December":"décembre"} -%} {%- macro date_short(d) -%} {%- if d -%}{%- set dt = frappe.utils.getdate(d) -%}{{ "%02d/%02d/%04d" | format(dt.day, dt.month, dt.year) }}{%- else -%}—{%- endif -%} {%- endmacro -%} {%- macro date_fr(d) -%} {%- if d -%}{%- set dt = frappe.utils.getdate(d) -%}{{ dt.day }} {{ mois_fr.get(dt.strftime("%B"), dt.strftime("%B")) }} {{ dt.year }}{%- else -%}—{%- endif -%} {%- endmacro -%} {#- ── Core variables ── -#} {%- set quote_number = doc.name -%} {%- set quote_date = date_short(doc.transaction_date) -%} {%- set customer_id = (doc.party_name if doc.quotation_to == "Customer" else "") or "" -%} {%- set account_number = customer_id -%} {%- set client_name = doc.customer_name -%} {%- set valid_till = date_fr(doc.valid_till) if doc.valid_till else "—" -%} {%- set signing_url = doc.get("custom_docuseal_signing_url") or "" -%} {#- ── Client address ── -#} {%- set client_address_line1 = "" -%} {%- set client_address_line2 = "" -%} {%- if doc.customer_address -%} {%- set a = frappe.get_doc("Address", doc.customer_address) -%} {%- set client_address_line1 = a.address_line1 or "" -%} {%- set _city = (a.city or "") -%} {%- set _state = (a.state or "") -%} {%- set _pin = (a.pincode or "") -%} {%- set client_address_line2 = (_city ~ (", " if _city and _state else "") ~ _state ~ (" " if _pin else "") ~ _pin) | trim -%} {%- endif -%} {#- ── Language detection ── -#} {%- set _cust_lang = ((frappe.db.get_value("Customer", customer_id, "language") if customer_id else None) or "fr") | lower -%} {%- set _is_en = _cust_lang[:2] == "en" -%} {%- set doc_title = "QUOTE" if _is_en else "SOUMISSION" -%} {#- ── TPS / TVQ split ── -#} {%- set ns = namespace(tps=0, tvq=0) -%} {%- for t in doc.taxes or [] -%} {%- set _desc = ((t.description or "") ~ " " ~ (t.account_head or "")) | upper -%} {%- if "TPS" in _desc or "GST" in _desc or "HST" in _desc -%} {%- set ns.tps = ns.tps + (t.tax_amount or 0) -%} {%- elif "TVQ" in _desc or "QST" in _desc or "PST" in _desc -%} {%- set ns.tvq = ns.tvq + (t.tax_amount or 0) -%} {%- endif -%} {%- endfor -%} {%- if (ns.tps + ns.tvq) == 0 and (doc.total_taxes_and_charges or 0) > 0 -%} {%- set _tot = doc.total_taxes_and_charges -%} {%- set ns.tps = (_tot * 5.0 / 14.975) | round(2) -%} {%- set ns.tvq = _tot - ns.tps -%} {%- endif -%} {%- set tps_amount = money(ns.tps) -%} {%- set tvq_amount = money(ns.tvq) -%} {%- set total_taxes = money(doc.total_taxes_and_charges) -%} {%- set subtotal_before_taxes = money(doc.net_total) -%} {%- set grand_total = money(doc.grand_total) -%} {#- ── Split items by billing frequency (Monthly / Annual / One-time) ── -#} {%- set _monthly = [] -%} {%- set _annual = [] -%} {%- set _onetime = [] -%} {%- set _sums = namespace(monthly=0, annual=0, onetime=0) -%} {%- for it in doc.items -%} {%- set _freq = (it.get("custom_billing_frequency") or "One-time") -%} {%- set _short = frappe.call("gigafibre_utils.api.short_item_name", name=(it.item_name or it.description), max_len=56) -%} {%- set _desc = _short or _clean(it.item_name or it.description) -%} {%- set _row = { "description": _desc, "qty": (it.qty | int if it.qty == it.qty|int else it.qty), "unit_price": money(it.rate), "amount": money(it.amount), "amount_val": (it.amount or 0), "frequency": _freq, } -%} {%- if _freq == "Monthly" -%} {%- set _ = _monthly.append(_row) -%} {%- set _sums.monthly = _sums.monthly + (it.amount or 0) -%} {%- elif _freq == "Annual" -%} {%- set _ = _annual.append(_row) -%} {%- set _sums.annual = _sums.annual + (it.amount or 0) -%} {%- else -%} {%- set _ = _onetime.append(_row) -%} {%- set _sums.onetime = _sums.onetime + (it.amount or 0) -%} {%- endif -%} {%- endfor -%} {%- set monthly_equiv_val = _sums.monthly + (_sums.annual / 12.0) -%} {%- set has_recurring = (_monthly|length + _annual|length) > 0 -%} {%- set has_onetime = _onetime|length > 0 -%} {#- ── QR code — DocuSeal signing URL if set, else portal URL ── -#} {%- if signing_url -%} {%- set qr_code_base64 = frappe.call("gigafibre_utils.api.url_qr_base64", url=signing_url) -%} {%- else -%} {%- set qr_code_base64 = "" -%} {%- endif -%} {#- ── Logo ── -#} {%- set logo_base64 = frappe.call("gigafibre_utils.api.logo_base64") -%} {{ doc_title }} {{ quote_number }}
{% if _is_en %}Bill to{% else %}Soumission pour{% endif %}
{{ client_name }}
{{ client_address_line1 }}
{{ client_address_line2 }}
{#- ── Recurring services ── -#} {% if has_recurring %}
{% if _is_en %}RECURRING SERVICES{% else %}SERVICES RÉCURRENTS{% endif %}
{% for item in _monthly %} {% endfor %} {% for item in _annual %} {% endfor %}
{{ item.description }}{% if item.qty and item.qty|string not in ('1', '1.0') %} (×{{ item.qty }}){% endif %} ({% if _is_en %}/mo{% else %}/mois{% endif %}) $ {{ item.amount }}
{{ item.description }}{% if item.qty and item.qty|string not in ('1', '1.0') %} (×{{ item.qty }}){% endif %} ({% if _is_en %}/yr{% else %}/an{% endif %}) $ {{ item.amount }}
{% if _is_en %}Recurring subtotal{% else %}Sous-total récurrent{% endif %}{% if _annual|length %} ({% if _is_en %}monthly equiv.{% else %}équiv. mensuel{% endif %}){% endif %} $ {{ money(monthly_equiv_val) }}
{% endif %} {#- ── One-time fees ── -#} {% if has_onetime %}
{% if _is_en %}ONE-TIME FEES{% else %}FRAIS UNIQUES{% endif %}
{% for item in _onetime %} {% endfor %}
{{ item.description }}{% if item.qty and item.qty|string not in ('1', '1.0') %} (×{{ item.qty }}){% endif %} $ {{ item.amount }}
{% if _is_en %}One-time subtotal{% else %}Sous-total frais uniques{% endif %} $ {{ money(_sums.onetime) }}
{% endif %}
{% if _is_en %}Prices exclude applicable taxes (GST/QST).{% else %}Les prix excluent les taxes applicables (TPS/TVQ).{% endif %}
TPS: 834975559RT0001  |  TVQ: 1213765929TQ0001
{{ doc_title }}
{% if _is_en %}Quote #{% else %}Nº soumission{% endif %}{{ quote_number }}
Date{{ quote_date }}
{% if _is_en %}Valid until{% else %}Valide jusqu'au{% endif %}{{ valid_till }}
{% if _is_en %}Account #{% else %}Nº compte{% endif %}{{ account_number }}
{#- Split totals — recurring + one-time shown separately -#} {% if has_recurring %}
{% if _is_en %}Recurring total{% else %}Total récurrent{% endif %}
{% if _is_en %}Valid until{% else %}Valide jusqu'au{% endif %} {{ valid_till }}
$ {{ money(monthly_equiv_val) }}
{% endif %} {% if has_onetime %}
{% if _is_en %}Initial fees{% else %}Frais initiaux{% endif %}
{% if _is_en %}Billed on first invoice{% else %}Facturés à la 1re facture{% endif %}
$ {{ money(_sums.onetime) }}
{% endif %}
{% if qr_code_base64 %} {% else %}
QR
{% endif %}
{% if _is_en %}Sign online{% else %}Signez en ligne{% endif %}
{% if _is_en %}Scan the QR code or click below{% else %}Scannez le code QR ou cliquez ci-dessous{% endif %}
sign.gigafibre.ca
{% if signing_url %} {% if _is_en %}✎  SIGN THIS QUOTE{% else %}✎  SIGNER CETTE SOUMISSION{% endif %} {% else %}
{% if _is_en %}Signing link will be sent by email / SMS{% else %}Lien de signature envoyé par courriel / SMS{% endif %}
{% endif %}
{% if _is_en %} This quote is valid until {{ valid_till }}. Services are subject to network availability at the service address. By signing, you accept our terms and conditions. {% else %} Cette soumission est valide jusqu'au {{ valid_till }}. Les services sont sujets à la disponibilité du réseau à l'adresse de service. En signant, vous acceptez nos termes et conditions. {% endif %}
{% if _is_en %}Contact us{% else %}Contactez-nous{% endif %}
TARGO Communications Inc.
1867 chemin de la Rivière
Sainte-Clotilde, QC J0L 1W0
855 888-2746 · {% if _is_en %}Mon-Fri 8am-5pm{% else %}Lun-Ven 8h-17h{% endif %}
info@targo.ca • www.targo.ca
""" _PF_OPTIONS = { "print_format_type": "Jinja", "standard": "No", "custom_format": 1, "pdf_generator": "chrome", "margin_top": 5, "margin_bottom": 5, "margin_left": 15, "margin_right": 15, "disabled": 0, } if frappe.db.exists("Print Format", PRINT_FORMAT_NAME): pf = frappe.get_doc("Print Format", PRINT_FORMAT_NAME) pf.html = html_template for k, v in _PF_OPTIONS.items(): setattr(pf, k, v) pf.save(ignore_permissions=True) print(f" Updated Print Format: {PRINT_FORMAT_NAME} ({len(html_template)} bytes)") else: pf = frappe.get_doc({ "doctype": "Print Format", "name": PRINT_FORMAT_NAME, "__newname": PRINT_FORMAT_NAME, "doc_type": "Quotation", "module": "CRM", **_PF_OPTIONS, "html": html_template, "default_print_language": "fr", }) pf.insert(ignore_permissions=True) print(f" Created Print Format: {PRINT_FORMAT_NAME} ({len(html_template)} bytes)") frappe.db.commit() print(f"\n Done. Print Format '{PRINT_FORMAT_NAME}' ready.") print(f" Preview: ERPNext → Quotation → Print → Select '{PRINT_FORMAT_NAME}'") print(f" Note: add gigafibre_utils.api.url_qr_base64(url) to generate QR for DocuSeal links.")