diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11f136d..763a0db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,7 +258,7 @@ template (the next template still runs). | c | render | the template is copied to a temp dir (without `template.toml`), every `{{token}}` is replaced with its `default` (or a `Sample …` stand-in), and a `project.toml` is written (`[document]` + `[build] entry = "main.tex"`, plus `bibliography = "bib/references.bib"` when that file exists) | | d | texforge | in the rendered copy: `texforge check` exits 0 **and** prints no line matching `WARNING\|ERROR`; `texforge fmt --check` exits 0; `texforge build` exits 0; `texforge pdf check` exits 0 | | e | previews | `texforge preview --scale 1.5` writes PNG pages into `/template-previews//` (the directory is emptied first), and the page count is read from `texforge pdf info` | -| f | content | *(requirements skipped for `letter`, `cv` and `contract`)* `main.tex` or a file it `\input`s contains a `\begin{code}[lang=…]` block, and at least one `\begin{mermaid}` / `\begin{graphviz}` / `\begin{d2}` whose options contain `style=`. For **every** template: no `\usepackage[utf8]{inputenc}`, no `\lstdefinestyle`, no `\begin{lstlisting}`, no `\usepackage{minted}` | +| f | content | *(requirements skipped for `letter`, `cv`, `contract` and `quotation`)* `main.tex` or a file it `\input`s contains a `\begin{code}[lang=…]` block, and at least one `\begin{mermaid}` / `\begin{graphviz}` / `\begin{d2}` whose options contain `style=`. For **every** template: no `\usepackage[utf8]{inputenc}`, no `\lstdefinestyle`, no `\begin{lstlisting}`, no `\usepackage{minted}` | ### Where previews land diff --git a/README.md b/README.md index c8797da..8dac3a7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ All templates support **multi-language output** via the `{{language}}` placehold | `ieee` | IEEE journal format for technical papers | 0.2.1 | | `internal-docs` | Handbook for internal policies, procedures, playbooks and decision records, with a cover, document control and callouts | 0.1.0 | | `letter` | Formal letter template for professional correspondence | 0.3.0 | +| `quotation` | Service quotation with itemised prices, scope, 50/50 payment terms and acceptance through the signed service order | 0.1.0 | | `report` | Structured university report with front matter, contents and appendices | 0.4.0 | | `slides` | 16:9 presentation slides with code and diagrams | 0.2.0 | | `technical-brief` | Concise technical update with callouts, diagrams, code and results | 0.3.0 | diff --git a/check-templates.sh b/check-templates.sh index e940b80..e663ee6 100644 --- a/check-templates.sh +++ b/check-templates.sh @@ -232,8 +232,8 @@ for p, text in files: if pat in text: print("forbidden pattern " + pat + " in " + str(p.relative_to(dest))) raise SystemExit(1) -if name in ("letter", "cv", "contract"): - raise SystemExit(0) # content requirements are skipped for letter/cv/contract +if name in ("letter", "cv", "contract", "quotation"): + raise SystemExit(0) # content requirements are skipped for letter/cv/contract/quotation entry = dest / "main.tex" if not entry.is_file(): print("main.tex not found"); raise SystemExit(1) diff --git a/quotation/main.tex b/quotation/main.tex new file mode 100644 index 0000000..45c13e0 --- /dev/null +++ b/quotation/main.tex @@ -0,0 +1,237 @@ +% Service quotation sent to a client before a service order. It is not a +% contract: the client accepts it by signing the service order electronically. +% Text in double braces is filled in when the project is created. +\documentclass[11pt,a4paper]{article} + +\usepackage[{{language}}]{babel} +\usepackage[T1]{fontenc} +\usepackage[lining]{ebgaramond} +\usepackage[scale=0.92]{sourcesanspro} +\usepackage{microtype} +\usepackage[a4paper,left=28mm,right=28mm,top=26mm,bottom=30mm, + headheight=14pt,headsep=10mm,footskip=12mm]{geometry} +\usepackage{xcolor} +\usepackage{enumitem} +\usepackage{fancyhdr} +\usepackage{booktabs} +\usepackage{tabularx} +\usepackage{lastpage} +\usepackage{needspace} +\usepackage{xfp} +\usepackage{siunitx} +\usepackage{etoolbox} +\usepackage{hyperref} + +% Palette: garnet accent (placeholder, garnet by default) on ink and sepia +% greys. No gold, no teal, plain paper. +\definecolor{conink}{HTML}{1D1510} +\definecolor{conmuted}{HTML}{6B5A43} +\colorlet{conrule}{conmuted!35!white} +\definecolor{conaccent}{HTML}{{{accent}}} +\hypersetup{colorlinks=true,linkcolor=conaccent,urlcolor=conaccent, + pdftitle={Quotation}} + +% Numbers: English by default; Spanish overrides the separators below. +\sisetup{group-separator={,},output-decimal-marker={.},group-minimum-digits=4} + +% Fixed labels and phrases. English defaults, then Spanish overrides. +\newcommand{\lblTitle}{Quotation} +\newcommand{\lblQuoteNo}{Quote no.} +\newcommand{\lblIssued}{Issue date} +\newcommand{\lblValidity}{Validity (days)} +\newcommand{\lblCurrency}{Currency} +\newcommand{\lblIssuer}{Issuer} +\newcommand{\lblClient}{Client} +\newcommand{\lblName}{Name} +\newcommand{\lblId}{ID or tax number} +\newcommand{\lblEmail}{Email} +\newcommand{\lblSecNeed}{Need} +\newcommand{\lblSecScope}{Scope} +\newcommand{\lblIn}{Included} +\newcommand{\lblOut}{Not included} +\newcommand{\lblSecItems}{Itemised prices} +\newcommand{\lblItem}{Item} +\newcommand{\lblDesc}{Description} +\newcommand{\lblQty}{Qty} +\newcommand{\lblUnit}{Unit price} +\newcommand{\lblLineTotal}{Total} +\newcommand{\lblSubtotal}{Subtotal} +\newcommand{\lblTotalAmt}{Total} +\newcommand{\lblSecPay}{Payment terms} +\newcommand{\lblWhen}{When} +\newcommand{\lblPct}{Share} +\newcommand{\lblAmount}{Amount} +\newcommand{\lblAtStart}{At signing of the service order} +\newcommand{\lblOnDelivery}{On delivery} +\newcommand{\lblMethods}{Payment methods} +\newcommand{\feesnote}{Payment gateway and bank transfer fees (for example PayPal) are paid by the client.} +\newcommand{\lblSecTax}{Tax} +\newcommand{\lblSecTime}{Estimated timeline} +\newcommand{\lblSecAccept}{How to accept} +\newcommand{\acceptext}[1]{This quotation is accepted by signing the service order electronically at #1. This quotation alone is not a contract.} +\newcommand{\lblQuoteNoFoot}{Quotation} +\addto\captionsspanish{% + \renewcommand{\lblTitle}{Cotización} + \renewcommand{\lblQuoteNo}{Cotización n.º} + \renewcommand{\lblIssued}{Fecha de emisión} + \renewcommand{\lblValidity}{Vigencia (días)} + \renewcommand{\lblCurrency}{Moneda} + \renewcommand{\lblIssuer}{Emisor} + \renewcommand{\lblClient}{Cliente} + \renewcommand{\lblName}{Nombre} + \renewcommand{\lblId}{Identificación o NIT} + \renewcommand{\lblEmail}{Correo electrónico} + \renewcommand{\lblSecNeed}{Necesidad} + \renewcommand{\lblSecScope}{Alcance} + \renewcommand{\lblIn}{Incluye} + \renewcommand{\lblOut}{No incluye} + \renewcommand{\lblSecItems}{Detalle económico} + \renewcommand{\lblItem}{Ítem} + \renewcommand{\lblDesc}{Descripción} + \renewcommand{\lblQty}{Cant.} + \renewcommand{\lblUnit}{Precio unitario} + \renewcommand{\lblLineTotal}{Total} + \renewcommand{\lblSubtotal}{Subtotal} + \renewcommand{\lblTotalAmt}{Total} + \renewcommand{\lblSecPay}{Condiciones de pago} + \renewcommand{\lblWhen}{Momento} + \renewcommand{\lblPct}{Porcentaje} + \renewcommand{\lblAmount}{Valor} + \renewcommand{\lblAtStart}{Al firmar la orden de servicio} + \renewcommand{\lblOnDelivery}{Contra entrega} + \renewcommand{\lblMethods}{Medios de pago} + \renewcommand{\feesnote}{Los cargos de la pasarela y de las transferencias (por ejemplo, PayPal) corren por cuenta del cliente.} + \renewcommand{\lblSecTax}{Impuestos} + \renewcommand{\lblSecTime}{Cronograma estimado} + \renewcommand{\lblSecAccept}{Cómo aceptar} + \renewcommand{\acceptext}[1]{Esta cotización se acepta firmando electrónicamente la orden de servicio en #1. La cotización, por sí sola, no constituye un contrato.} + \renewcommand{\lblQuoteNoFoot}{Cotización} + % Spanish groups thousands with a full stop; a bare full stop is dropped by siunitx, so it is braced. + \sisetup{group-separator={{.}},output-decimal-marker={,}}} + +% Values from the placeholders, read once by the totals below. +\newcommand{\QtyA}{{{item_1_qty}}} +\newcommand{\PriceA}{{{item_1_price}}} +\newcommand{\QtyB}{{{item_2_qty}}} +\newcommand{\PriceB}{{{item_2_price}}} +\newcommand{\QtyC}{{{item_3_qty}}} +\newcommand{\PriceC}{{{item_3_price}}} +\newcommand{\AdvPct}{{{advance_pct}}} +\newcommand{\QuoteNo}{{{quote_number}}} + +% Small helpers: section headings, metadata labels and the party blocks. +\newcommand{\metalabel}[1]{{\sffamily\scriptsize\scshape\color{conmuted}#1}} +\newcommand{\sechead}[1]{\par\vspace{1.2em}\needspace{6\baselineskip}% + {\sffamily\small\bfseries\scshape\color{conaccent}#1}\par + \vspace{2pt}{\color{conrule}\hrule height 0.4pt}\par\vspace{6pt}} +\newcommand{\subhead}[1]{\par\vspace{4pt}{\sffamily\small\bfseries\color{conink}#1}\par} +\newcommand{\party}[4]{\begin{minipage}[t]{0.47\textwidth} + {\sffamily\small\scshape\bfseries\color{conaccent}#1}\par\vspace{2pt} + {\color{conaccent}\hrule height 0.8pt}\par\vspace{6pt} + {\bfseries #2}\par\vspace{5pt} + \metalabel{\lblId}\par #3\par\vspace{5pt} + \metalabel{\lblEmail}\par #4 + \end{minipage}} + +% Page furniture: quote number and page count on every page. +\setlength{\parindent}{0pt} +\setlength{\parskip}{0.5\baselineskip} +\setlength{\emergencystretch}{2em} +\setlist[itemize]{leftmargin=1.2em,itemsep=2pt,topsep=2pt,parsep=0pt} +\renewcommand{\arraystretch}{1.3} +\fancypagestyle{quote}{% + \fancyhf{} + \renewcommand{\headrulewidth}{0pt} + \fancyfoot[L]{\small\sffamily {{lab_name}}\quad\textperiodcentered\quad\lblQuoteNoFoot\ \QuoteNo} + \fancyfoot[R]{\small\sffamily\thepage\ /\ \pageref*{LastPage}} + \renewcommand{\footrulewidth}{0pt}} +\pagestyle{quote} + +\begin{document} + + % Totals: each line is quantity times unit price; subtotal, advance and balance. + \edef\TotA{\fpeval{\QtyA*\PriceA}} + \edef\TotB{\fpeval{\QtyB*\PriceB}} + \edef\TotC{\fpeval{\QtyC*\PriceC}} + \edef\Sub{\fpeval{\TotA+\TotB+\TotC}} + \edef\Adv{\fpeval{\Sub*\AdvPct/100}} + \edef\Bal{\fpeval{\Sub-\Adv}} + \edef\BalPct{\fpeval{100-\AdvPct}} + + % Header: quote number, title, the issuer and the client. + {\sffamily\small\scshape\color{conaccent}\lblQuoteNo\ \QuoteNo\par} + \vspace{4pt} + {\normalfont\fontsize{30}{34}\selectfont\color{conink}\lblTitle\par} + \vspace{6pt} + {\color{conaccent}\rule{22mm}{2pt}\par} + \vspace{10pt} + + \noindent + \party{\lblIssuer}{{{lab_name}}\\{{owner_name}}}{{{owner_id}}}{{{owner_email}}\\{{lab_site}}}% + \hfill + \party{\lblClient}{{{client_name}}}{{{client_id}}}{{{client_contact}}} + + \vspace{12pt} + \noindent\begin{tabularx}{\textwidth}{@{}X X X@{}} + \metalabel{\lblIssued} & \metalabel{\lblValidity} & \metalabel{\lblCurrency}\\[2pt] + {{issue_date}} & {{validity_days}} & {{currency}} + \end{tabularx} + + \sechead{\lblSecNeed} + {{service_need}} + + \sechead{\lblSecScope} + \subhead{\lblIn} + \begin{itemize} + \item {{scope_in_1}} + \item {{scope_in_2}} + \item {{scope_in_3}} + \end{itemize} + \subhead{\lblOut} + \begin{itemize} + \item {{scope_out_1}} + \item {{scope_out_2}} + \end{itemize} + + \sechead{\lblSecItems} + \begin{tabularx}{\textwidth}{@{}l X r r r@{}} + \toprule + \textbf{\lblItem} & \textbf{\lblDesc} & \textbf{\lblQty} & \textbf{\lblUnit} & \textbf{\lblLineTotal}\\ + \midrule + {{item_1_name}} & {{item_1_desc}} & \num{\QtyA} & \num{\PriceA} & \num{\TotA}\\ + {{item_2_name}} & {{item_2_desc}} & \num{\QtyB} & \num{\PriceB} & \num{\TotB}\\ + {{item_3_name}} & {{item_3_desc}} & \num{\QtyC} & \num{\PriceC} & \num{\TotC}\\ + \midrule + \multicolumn{4}{r}{\lblSubtotal} & \num{\Sub}\\ + \multicolumn{4}{r}{\textbf{\lblTotalAmt}\ ({{currency}})} & \textbf{\num{\Sub}}\\ + \bottomrule + \end{tabularx} + + \sechead{\lblSecPay} + \begin{tabularx}{\textwidth}{@{}X r r@{}} + \toprule + \textbf{\lblWhen} & \textbf{\lblPct} & \textbf{\lblAmount}\\ + \midrule + \lblAtStart & \AdvPct\,\% & \num{\Adv}\\ + \lblOnDelivery & \BalPct\,\% & \num{\Bal}\\ + \bottomrule + \end{tabularx} + \par\vspace{6pt} + {\itshape\small\color{conmuted}\feesnote\par} + \vspace{4pt} + \metalabel{\lblMethods}\par {{payment_methods}} + + \sechead{\lblSecTax} + {{tax_note}} + + \sechead{\lblSecTime} + {{timeline}} + + \sechead{\lblSecAccept} + \acceptext{{{lab_site}}} + + \needspace{4\baselineskip} + \vspace{1.6em} + {\itshape\color{conmuted}{{closing}}\par} + +\end{document} diff --git a/quotation/spell-whitelist.txt b/quotation/spell-whitelist.txt new file mode 100644 index 0000000..8636a1b --- /dev/null +++ b/quotation/spell-whitelist.txt @@ -0,0 +1,7 @@ +# Words the spell checker accepts in this template. One word per line. +pt +cop +usd +iva +paypal +hosting diff --git a/quotation/template.toml b/quotation/template.toml new file mode 100644 index 0000000..6acb21d --- /dev/null +++ b/quotation/template.toml @@ -0,0 +1,274 @@ +id = "quotation" +version = "0.1.0" +display_name = "Quotation" +description = "Service quotation sent to a client before a service order: itemised prices, scope, 50/50 payment terms and how to accept through the signed service order" + +[files] +include = ["**/*.tex", "**/*.txt"] +exclude = [] + +[[placeholders]] +name = "lab_name" +type = "string" +description = "Lab or business name shown in the issuer block" +required = false +default = "UniverLab" + +[[placeholders]] +name = "owner_name" +type = "string" +description = "Owner or responsible person of the lab" +required = false +default = "{{user.name}}" + +[[placeholders]] +name = "owner_id" +type = "string" +description = "Owner identification or tax number" +required = false +default = "000000000-0" + +[[placeholders]] +name = "owner_email" +type = "string" +description = "Contact email of the issuer" +required = false +default = "{{user.email}}" + +[[placeholders]] +name = "lab_site" +type = "string" +description = "Website where the service order is signed" +required = false +default = "univerlab.org" + +[[placeholders]] +name = "quote_number" +type = "string" +description = "Quote number" +required = false +default = "COT-2026-001" + +[[placeholders]] +name = "issue_date" +type = "string" +description = "Issue date of the quote" +required = false +default = "\\today" + +[[placeholders]] +name = "validity_days" +type = "string" +description = "Number of days the quote stays valid from its issue date" +required = false +default = "15" + +[[placeholders]] +name = "client_name" +type = "string" +description = "Client name" +required = false +default = "Cliente de ejemplo S.A.S." + +[[placeholders]] +name = "client_id" +type = "string" +description = "Client identification or tax number" +required = false +default = "111111111-1" + +[[placeholders]] +name = "client_contact" +type = "string" +description = "Client contact (email and phone)" +required = false +default = "cliente@example.org" + +[[placeholders]] +name = "service_need" +type = "string" +description = "Short description of the client's need that this quote answers" +required = false +default = "Describa en una o dos frases la necesidad del cliente que esta cotización atiende." + +[[placeholders]] +name = "scope_in_1" +type = "string" +description = "Scope item included in the service (first)" +required = false +default = "Análisis de requisitos con el cliente." + +[[placeholders]] +name = "scope_in_2" +type = "string" +description = "Scope item included in the service (second)" +required = false +default = "Diseño e implementación de la solución acordada." + +[[placeholders]] +name = "scope_in_3" +type = "string" +description = "Scope item included in the service (third)" +required = false +default = "Entrega final con documentación básica." + +[[placeholders]] +name = "scope_out_1" +type = "string" +description = "Item explicitly not included in the service (first)" +required = false +default = "Hosting, dominio y licencias de terceros." + +[[placeholders]] +name = "scope_out_2" +type = "string" +description = "Item explicitly not included in the service (second)" +required = false +default = "Mantenimiento posterior a la entrega." + +[[placeholders]] +name = "item_1_name" +type = "string" +description = "Price line 1: item name" +required = false +default = "Diseño del servicio" + +[[placeholders]] +name = "item_1_desc" +type = "string" +description = "Price line 1: description" +required = false +default = "Análisis de requisitos y propuesta de solución." + +[[placeholders]] +name = "item_1_qty" +type = "string" +description = "Price line 1: quantity (a number; delete the line if unused)" +required = false +default = "1" + +[[placeholders]] +name = "item_1_price" +type = "string" +description = "Price line 1: unit price (a number, no separators)" +required = false +default = "2500000" + +[[placeholders]] +name = "item_2_name" +type = "string" +description = "Price line 2: item name" +required = false +default = "Desarrollo" + +[[placeholders]] +name = "item_2_desc" +type = "string" +description = "Price line 2: description" +required = false +default = "Implementación y pruebas de la solución." + +[[placeholders]] +name = "item_2_qty" +type = "string" +description = "Price line 2: quantity (a number; delete the line if unused)" +required = false +default = "2" + +[[placeholders]] +name = "item_2_price" +type = "string" +description = "Price line 2: unit price (a number, no separators)" +required = false +default = "600000" + +[[placeholders]] +name = "item_3_name" +type = "string" +description = "Price line 3: item name" +required = false +default = "Entrega y capacitación" + +[[placeholders]] +name = "item_3_desc" +type = "string" +description = "Price line 3: description" +required = false +default = "Puesta en marcha, documentación y sesión de capacitación." + +[[placeholders]] +name = "item_3_qty" +type = "string" +description = "Price line 3: quantity (a number; delete the line if unused)" +required = false +default = "1" + +[[placeholders]] +name = "item_3_price" +type = "string" +description = "Price line 3: unit price (a number, no separators)" +required = false +default = "800000" + +[[placeholders]] +name = "currency" +type = "string" +description = "Currency code of all amounts (COP or USD)" +required = false +default = "COP" + +[[placeholders]] +name = "advance_pct" +type = "string" +description = "Percentage paid to start, at signing of the service order; the balance is paid on delivery" +required = false +default = "50" + +[[placeholders]] +name = "payment_methods" +type = "string" +description = "Payment methods accepted (bank transfer, PayPal, others)" +required = false +default = "Transferencia bancaria o PayPal" + +[[placeholders]] +name = "tax_note" +type = "string" +description = "Tax note shown under the taxes heading" +required = false +default = "No responsable de IVA" + +[[placeholders]] +name = "timeline" +type = "string" +description = "Estimated timeline from signing to delivery" +required = false +default = "Entrega estimada en 4 semanas a partir de la firma de la orden de servicio." + +[[placeholders]] +name = "closing" +type = "string" +description = "Short closing line" +required = false +default = "Gracias por confiar en UniverLab." + +[[placeholders]] +name = "accent" +type = "string" +description = "Accent colour as a six-digit HTML hex value, without the hash" +required = false +default = "87222C" + +[[placeholders]] +name = "language" +type = "enum" +description = "Document language for babel; fixed labels are translated for English and Spanish" +required = true +choices = ["english", "spanish", "french", "german", "italian", "portuguese"] +default = "spanish" + +[[post_generate]] +name = "build" +description = "Compile the quotation to PDF" +command = "texforge build" +optional = true diff --git a/registry.toml b/registry.toml index ae58d61..bbb017b 100644 --- a/registry.toml +++ b/registry.toml @@ -58,6 +58,11 @@ nombre = "letter" descripcion = "Formal letter template for professional correspondence" version = "0.3.0" +[[templates]] +nombre = "quotation" +descripcion = "Service quotation with itemised prices, scope, 50/50 payment terms and acceptance through the signed service order" +version = "0.1.0" + [[templates]] nombre = "report" descripcion = "Structured university report with front matter, contents and appendices"