Back to Homepage
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

No. 260928-1947 · Date 2026-09-28

Between Halden Logistics GmbH and the Marketplace.

  1. §1Scope of servicestype · v1

    The Marketplace provides Listing management for Halden Logistics GmbH for a term of 12 months.

  2. §2Feestype · v1

    Halden Logistics GmbH pays a monthly fee of 1,500 €, payable Net 30.

  3. §3Termbase · v1

    This agreement runs for twelve months and renews unless terminated.

  4. §4Confidentialitybase · v1

    Both parties keep the other party's business information confidential.

  5. §5Data protectionbase · v1

    Personal data is processed under the data processing agreement in Annex 1.

  6. §6Intellectual propertybase · v1

    Content uploaded by Halden Logistics GmbH remains the property of Halden Logistics GmbH.

  7. §7Liabilitybase · v1

    Liability is capped at the fees paid in the twelve months before the claim.

  8. §8Warrantybase · v1

    Services are provided with the care of a prudent business.

  9. §9Terminationbase · v1

    Either party may terminate with three months' notice to the end of a term.

  10. §10Force majeurebase · v1

    Neither party is liable for delays caused by events outside its control.

  11. §11Governing lawbase · v1

    This agreement is governed by Austrian law.

  12. §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

Daniel 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!

Dietmar Eglhofer - CEO, Solvster & VIARES

Join Dietmar and launch your next project with confidence.

Get In TouchGet In Touch