About 15 % of contracts do. The template has an 'additional clauses' section for free-text paragraphs that legal has pre-approved; they are appended after the standard clauses and marked as addenda. When the same addendum keeps appearing, legal promotes it to a standard clause in the base. The base stays small and the exceptions stay visible.
Services
Backend Engineering, Product
Industry
B2B Marketplace
Year
2022-2024
Sixteen contract types, generated from verified data
A contract left a B2B marketplace with '[COMPANY NAME]' still in the header. It had taken the usual route: one of sixteen Word templates on a network drive, filled in by hand, mailed to legal, corrected, exported, mailed again for signature. Three business days per contract. One in five with a wrong name, an old payment term or a missing clause.
THE CHALLENGE
Sixteen templates, four liability caps
Each Word template carried yellow placeholder fields that an account manager overwrote by hand, usually starting from the previous contract. That is how a company name travelled from one client's agreement into the next, and how a placeholder reached a header unnoticed. The clauses drifted the same way, only slower. Four of the sixteen templates stated different liability caps. When legal changed a clause, someone had to open all sixteen files and paste the new text; three months after one such update, two templates still carried the old wording, and the first to notice was a client's lawyer. Nothing was versioned, so nobody could say which text a signed contract had come from.
The fastest way to remove errors from a contract is to remove the step where a person types the data.
THE SOLUTION
Templates in the database, data from the record
The obvious purchase was a contract platform such as PandaDoc or DocuSign CLM. They solve approval chains, e-signatures and CRM sync; the problem here was a human typing data that already existed. I rejected them for the monthly cost, for contracts leaving the server, and for the API mapping between their fields and the Django model. Instead, each contract type became a Django template stored as a database record with typed variables. Generate reads company name, address, agreed terms and pricing from the listing record, validates every variable, renders HTML and writes the PDF with WeasyPrint. The trade-off is ownership: legal edits templates in the Django admin instead of Word, and the team maintains the engine.
The generation path: verified data in, validated context, HTML to PDF, and a record of which clause versions and which hash the document has:
Python
class ContractGenerator:
def generate(self, listing, template_type):
"""Generate a contract PDF from verified data."""
template = ContractTemplate.objects.get(
type=template_type,
is_active=True,
)
# Pull variables from verified database records
context = {
'company': listing.company,
'address': listing.company.billing_address,
'terms': listing.agreed_terms,
'pricing': listing.pricing_schedule,
'clauses': template.get_active_clauses(),
'generated_at': timezone.now(),
}
# Validate all variables before rendering
template.validate_context(context)
# Render HTML → PDF
html = render_to_string(
template.django_template_path,
context,
)
pdf = weasyprint.HTML(string=html).write_pdf()
# Store with audit trail
return Contract.objects.create(
listing=listing,
template_version=template.version,
clause_versions=template.clause_version_map(),
pdf_hash=hashlib.sha256(pdf).hexdigest(),
pdf_file=ContentFile(pdf, name=f'{listing.slug}.pdf'),
)Generate a contract, then change the base
Pick a contract type and generate. Variables come from the fields, not from typing into the document; clear one and the preview shows the placeholder that once shipped, and generation halts. Then press the legal update: §7 changes in the base and every type in the strip picks it up, whichever one is selected.
Service Agreement
Between Halden Logistics GmbH and the Marketplace.
§1Scope of servicestype · v1
The Marketplace provides Listing management for Halden Logistics GmbH for a term of 12 months.
§2Feestype · v1
Halden Logistics GmbH pays a monthly fee of 1,500 €, payable Net 30.
§3Termbase · v1
This agreement runs for twelve months and renews unless terminated.
§4Confidentialitybase · v1
Both parties keep the other party's business information confidential.
§5Data protectionbase · v1
Personal data is processed under the data processing agreement in Annex 1.
§6Intellectual propertybase · v1
Content uploaded by Halden Logistics GmbH remains the property of Halden Logistics GmbH.
§7Liabilitybase · v1
Liability is capped at the fees paid in the twelve months before the claim.
§8Warrantybase · v1
Services are provided with the care of a prudent business.
§9Terminationbase · v1
Either party may terminate with three months' notice to the end of a term.
§10Force majeurebase · v1
Neither party is liable for delays caused by events outside its control.
§11Governing lawbase · v1
This agreement is governed by Austrian law.
§12Final provisionsbase · v1
Amendments require written form. Invalid clauses do not affect the rest.
Not generated yet. Generate to record the clause versions and the document hash.
16 contract types extend the base
- Service Agreement§7 v1
- Non-Disclosure Agreement§7 v1
- Maintenance Contract§7 v1
- Type 04§7 v1
- Type 05§7 v1
- Type 06§7 v1
- Type 07§7 v1
- Type 08§7 v1
- Type 09§7 v1
- Type 10§7 v1
- Type 11§7 v1
- Type 12§7 v1
- Type 13§7 v1
- Type 14§7 v1
- Type 15§7 v1
- Type 16§7 v1
- Variables injected
- 5 / 5
- Clauses from base
- 10 / 12
- Overridden by type
- 2
- Types on §7 v2
- 0 / 16
The base template. Every contract type extends it and overrides a block or two; §3 to §12 come from the shared clause set:
HTML
{# base_service_agreement.html #}
{# All 16 contract types inherit from this base #}
<p class="doc-title">Service Agreement</p>
<p>Between {{ company.name }} and the Marketplace.</p>
{% block scope %}
<h2>§1 Scope of Services</h2>
<p>{{ terms.scope_description }}</p>
{% endblock %}
{% block compensation %}
<h2>§2 Compensation</h2>
<p>Monthly rate: €{{ pricing.monthly_rate|floatformat:2 }}</p>
{% endblock %}
{# §3-§12: shared standard clauses, one file for all types #}
{% for clause in clauses %}
<h2>§{{ clause.number }} {{ clause.title }}</h2>
{{ clause.body|safe }}
{% endfor %}THE RESULT
Contracts became a button in the listing
Account managers now generate contracts from the listing they are working on, and go back to the client instead of to Word. Legal reviews a template change once and knows it is live everywhere; the four liability caps are one clause again. A placeholder cannot reach a header, because there are no placeholders, only variables that either validate or stop the run. Every PDF carries its clause versions and a hash, so which wording a client signed six months ago is a lookup, not an email search. Turnaround went from days to seconds.
KEY METRICS
< 2sGeneration time per contract
0%Field errors since launch
40+Contracts generated per week
CLIENT FEEDBACK
Contracts used to eat a good part of my Monday. Now I open the listing, pick the type, check the fields the system pulled in, and the PDF is there. I have not typed a company name into a contract since, and I could not if I tried.
Senior Account Manager
B2B marketplace, sales
FOR YOUR PROJECT
- When it applies
You issue the same document type again and again from data you already store, and the errors come from retyping it: contracts, offers, order confirmations. The signal is a placeholder or a stale value reaching a customer.
- What to check
Where the data lives today. If company, terms and prices sit in one record, templates can read them; if they live in the account manager's head, that is the first thing to fix. Then count the copies of each clause across your templates. Here it was sixteen.
- What it needs
A backend developer for the engine, a legal owner who edits templates in an admin instead of Word, and a review step for template versions. No licence fees and no data leaving the server, but the code is yours to maintain.
FAQ
TECHNOLOGY STACK
Django
Python
PostgreSQL
Dietmar Eglhofer - CEO, Solvster & VIARESDaniel made a decisive contribution to the founding and implementation of the companies Solvster and VIARES. He was responsible for the entire technical setup of the online presence, including server setups, developing and optimizing the website, and ensuring functionality. I was particularly impressed by how proactively and solution-oriented he always worked on improvements. Daniel was always very responsive and reliable - it was a pleasure working with him. His expertise and commitment contributed significantly to the success of both projects!
Join Dietmar and launch your next project with confidence.
Open to new ProjectsGet In Touch