Skip to content

fix(peppol): report recorded payments as PrepaidAmount in the UBL - #860

Merged
evilguy4000 merged 1 commit into
DRYTRIX:developfrom
5eBeKs:fix/peppol-prepaid-amount
Nov 8, 2026
Merged

evilguy4000 merged 1 commit into
DRYTRIX:developfrom
5eBeKs:fix/peppol-prepaid-amount

Conversation

@5eBeKs

@5eBeKs 5eBeKs commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

What is wrong

build_peppol_ubl_invoice_xml writes the invoice total into PayableAmount (BT-115) and never emits PrepaidAmount (BT-113). When payments are already recorded on the invoice (a deposit, a partial payment, or a resend after a payment), the UBL that is sent through the access point or downloaded from the UBL export (/invoices/<id>/export/ubl) asks the buyer to pay the full total.

The Factur-X/ZUGFeRD export (app/utils/cii_invoice.py) already handles this: it writes amount_paid as TotalPrepaidAmount and DuePayableAmount = total - amount_paid (see test_cii_prepaid_amount in tests/test_cii_invoice.py). So the same invoice gets two different payable amounts depending on the format:

invoice amount_paid Factur-X DuePayableAmount UBL PayableAmount (develop) UBL PayableAmount (this change)
242.00 EUR 0.00 242.00 242.00 242.00
242.00 EUR 50.00 192.00 242.00 192.00 (PrepaidAmount 50.00)
242.00 EUR 242.00 0.00 242.00 0.00 (PrepaidAmount 242.00)

EN 16931 rule BR-CO-16 defines the payable amount as TaxInclusiveAmount - PrepaidAmount + PayableRoundingAmount.

What this changes

  • app/integrations/peppol.py: when amount_paid is above zero, PrepaidAmount is added to LegalMonetaryTotal (before PayableAmount, as the UBL 2.1 sequence requires), and PayableAmount is the total minus the prepaid amount.
  • Overpaid invoices: as in the Factur-X export, the amount due is never negative (PayableAmount 0.00). The Factur-X export still writes the full payment as TotalPrepaidAmount; here the prepaid amount is capped at the total instead, so that BR-CO-16 holds in the UBL (300.00 paid on 242.00 gives PrepaidAmount 242.00 and PayableAmount 0.00). The other UBL option, the full payment as PrepaidAmount with a negative PayableAmount, also satisfies BR-CO-16; if you would rather have that, it is a one-line change and I can switch it.
  • Unpaid invoices produce the same XML as before (no PrepaidAmount, PayableAmount = total).

Unchanged: the Factur-X/CII export, the invoice totals, and the send/export routes.

Tests

New file tests/test_peppol_ubl_totals.py:

test develop this change
test_unpaid_invoice_payable_is_the_total_without_prepaid passes passes
test_missing_amount_paid_is_treated_as_unpaid passes passes
test_partially_paid_invoice_reports_prepaid_and_the_balance (50.00 of 242.00 → payable 192.00) fails (no PrepaidAmount) passes
test_prepaid_amount_precedes_payable_amount fails (no PrepaidAmount) passes
test_fully_paid_invoice_has_nothing_payable fails (no PrepaidAmount) passes
test_overpaid_invoice_has_nothing_payable_and_stays_balanced (300.00 of 242.00) fails (no PrepaidAmount) passes
  • Peppol, e-invoice and invoice test files (test_peppol_attachments.py, test_peppol_bridge.py, test_peppol_identifiers.py, test_peppol_service.py, test_peppol_ubl_totals.py, test_invoice_validators.py, test_invoice_pdf_postprocess.py, test_zugferd.py, test_cii_invoice.py, test_invoices.py): 173 passed, 1 skipped with the change; on develop the same files give 169 passed, 1 skipped and only the 4 new tests failing.
  • Full suite (Python 3.11, SQLite, no network), run on an earlier revision of this commit that differed only in the wording of a code comment, the CHANGELOG line and the commit message: 2682 passed, 5 failed, 83 skipped, 1 xfailed with the change; develop gives 2676 passed and the same 5 failures (test_time_format_display.py::test_parse_user_time_input_compact_hhmm and four tests in tests/test_utils/test_webhook_service.py), so nothing new fails.
  • black, isort and flake8 are clean on the changed Python files.

The Peppol UBL set PayableAmount (BT-115) to the invoice total and never
emitted PrepaidAmount (BT-113), so an invoice with a recorded payment was
sent or exported asking the buyer for the full total. The Factur-X/CII
export already subtracts amount_paid.

Emit PrepaidAmount when payments are recorded and set PayableAmount to
TaxInclusiveAmount - PrepaidAmount (BR-CO-16). As in the CII export, the
amount due is never negative; here the prepaid amount is capped at the
total so BR-CO-16 holds, and an overpayment is therefore not shown in
full in PrepaidAmount.
@evilguy4000
evilguy4000 merged commit b49a8a1 into DRYTRIX:develop Nov 8, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants