Skip to content

fix(SEPA Payment Order): avoid overpaying for PIs with relevant returns#364

Open
PatrickDEissler wants to merge 1 commit into
version-16-hotfixfrom
no-overpaying-with-returns
Open

fix(SEPA Payment Order): avoid overpaying for PIs with relevant returns#364
PatrickDEissler wants to merge 1 commit into
version-16-hotfixfrom
no-overpaying-with-returns

Conversation

@PatrickDEissler
Copy link
Copy Markdown
Collaborator

Case

  • Purchase Invoice 1 is submitted.
  • A return Purchase Invoice is submitted against PI 1 (with update_outstanding_for_self deactivated) -> Result: The Purchase Invoice has a reduced outstanding_amount.
  • Now a SEPA Payment Order is created that includes Purchase Invoice 1

See example of a Purchase Invoice 1:
image
-> Previous outstanding of 39.000€ (see grand_total) was reduced by 10k (see outstanding_amount)

Before

image -> 39k was used from row in `payment_schedule`.

Cause

Only outstanding_amount from payment_schedule is used to determine the default payment amount.
Problem is, that this field is not (and can not be) updated by returns.

After fix

image -> The minimum of `payment_schedule` and `doc.outstanding_amount` is used as default.

Also for discounted payments the minimum will be considered as base (before discounting).

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 13, 2026

Greptile Summary

This PR fixes SEPA Payment Order overpayment when a Purchase Invoice has associated return PIs that reduce outstanding_amount but cannot update the per-row outstanding in payment_schedule. The fix takes min(scheduled_payment.outstanding, doc.outstanding_amount) as the effective base and applies it to both the default and discounted payment paths.

  • The fix correctly prevents overpayment for the common single-row payment schedule case described in the PR.
  • For Purchase Invoices with multiple payment schedule rows (installment payments), the per-row cap using the document-level doc.outstanding_amount does not guarantee the total payment stays within the actual outstanding — if the return is smaller than any individual row's outstanding, none of the rows are capped and the total paid can still exceed doc.outstanding_amount.

Confidence Score: 3/5

The fix solves the stated single-row case but leaves a logic gap for invoices with multiple payment schedule rows that could still result in the same overpayment it aims to prevent.

The min(scheduled_payment.outstanding, doc.outstanding_amount) cap is applied independently to each payment schedule row. Because doc.outstanding_amount is a document-level total rather than a per-row budget, a return smaller than any individual row leaves all rows uncapped while their combined total still exceeds the actual outstanding. Installment-payment invoices (two or more rows) hit this path, so the issue is not confined to an exotic configuration.

banking/custom/purchase_invoice.py — specifically the get_sepa_payment_amount function and how its result interacts with multi-row payment schedules in make_sepa_payment_order.

Important Files Changed

Filename Overview
banking/custom/purchase_invoice.py Adds min(scheduled_payment.outstanding, doc.outstanding_amount) cap to fix overpayment caused by return PIs; cap is applied per-row and does not correctly handle multi-row payment schedules where the return is smaller than any individual row's outstanding.

Sequence Diagram

sequenceDiagram
    participant UI as User / Bulk Action
    participant PI as Purchase Invoice
    participant fn as get_sepa_payment_amount()
    participant PS as payment_schedule row

    UI->>PI: make_sepa_payment_order(source_name)
    PI->>PS: "iterate rows where outstanding > 0"
    loop each PaymentSchedule row
        PI->>fn: get_sepa_payment_amount(doc, row, execution_date)
        fn->>PS: scheduled_payment.outstanding
        fn->>PI: doc.outstanding_amount
        Note over fn: outstanding = min(row.outstanding, doc.outstanding_amount)
        alt discount_date valid and not expired
            fn-->>PI: "round(outstanding * discount_factor)"
        else no discount or expired
            fn-->>PI: outstanding
        end
    end
    PI-->>UI: SEPA Payment Order (with per-row amounts)
Loading

Fix All in Cursor

Reviews (1): Last reviewed commit: "fix(SEPA Payment Order): avoid overpayin..." | Re-trigger Greptile

return 0

# Also consider doc.outstanding_amount to avoid overpaying (typical case: Return PIs reduced the outstanding amount)
outstanding = min(scheduled_payment.outstanding, doc.outstanding_amount)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Multi-row payment schedule: cap is per-row, not per-document

When a Purchase Invoice has multiple payment schedule rows (installment payments), this cap can fail to prevent overpayment. doc.outstanding_amount is the total remaining balance for the entire document, not for an individual row. If the return amount is smaller than any individual row's outstanding, the min() leaves every row unchanged, yet their sum still exceeds doc.outstanding_amount.

Concrete example: two rows with outstanding = [15_000, 24_000], a 10 000 return leaves doc.outstanding_amount = 29_000. min(15_000, 29_000) and min(24_000, 29_000) are both uncapped, so the total paid is 39 000 instead of 29 000.

A correct cap would subtract the outstanding of the other rows from doc.outstanding_amount before capping, or use a proportional allocation across rows.

Fix in Cursor

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.

1 participant