dodil/erp-brick-payments
DODIL ERP brick: payments
tests/odoo_fake.py
"""A small Odoo database in memory - the reader port's fake (infrastructure/odoo.py) - and the ar documents its
invoices became. One scenario of everything PAY-44 maps: partial and full reconciliations, a credit note applied, an
overpayment refunded, a cross-currency payment, three unreconciled bank credits (one naming no partner), a credit
limit and a follow-up level.
Odoo db "prod1", company currency GBP. Partners 11 -> bp 7311, 12 -> bp 7312.
invoice 101 1000.00 p11 paid 600.00 (payment 1) and 100.00 (credit note 103) -> 300.00 open, follow-up level 2
invoice 102 500.00 p11 paid in full (payment 2)
credit note 103 100.00 p11 applied to invoice 101
invoice 104 850.00 p12 paid by payment 3 of 1,000.00 EUR (the cross-currency payment)
invoice 105 200.00 p11 paid by payment 4 of 250.00; 50.00 of it refunded (outbound payment 5)
statement lines: 1 (p12, 80.00), 2 (p11, 25.00), 3 (no partner, 40.00) - none reconciled
"""
from copy import deepcopy
DB = "prod1"
CUTOVER = "2026-11-15"
PARTNERS = {11: 7311, 12: 7312}
SUSPENSE = 9001
def line(id, move_id, move_type, partner, residual, *, amount=0, currency="GBP", payment_id=None, statement_line_id=None, followup=0, name=""):
return {"id": id, "move_id": move_id, "move_type": move_type, "move_name": name or str(move_id), "partner": partner, "date": "2026-10-01",
"date_maturity": "2026-10-31", "amount_currency": amount, "currency": currency, "amount_residual": residual,
"amount_residual_currency": residual, "reconciled": residual == 0, "payment_id": payment_id,
"statement_line_id": statement_line_id, "followup_level": followup, "followup_date": "2026-11-01" if followup else None}
def partial(id, debit, credit, amount, *, credit_amount=None, credit_currency="GBP", when="2026-10-10"):
return {"id": id, "debit_line": debit, "credit_line": credit, "amount": amount, "debit_amount_currency": amount,
"credit_amount_currency": credit_amount if credit_amount is not None else amount, "debit_currency": "GBP",
"credit_currency": credit_currency, "max_date": when, "full_reconcile_id": None, "exchange_move_id": None}
def payment(id, partner, amount, ptype="inbound", currency="GBP", date="2026-10-10"):
return {"id": id, "date": date, "amount": amount, "currency": currency, "partner": partner, "journal_code": "BNK1", "payment_type": ptype,
"partner_type": "customer", "state": "posted", "ref": f"REF-{id}", "method_code": "manual", "move_id": 9000 + id}
def stmt(id, partner, amount, date="2026-11-10"):
return {"id": id, "date": date, "amount": amount, "currency": "GBP", "payment_ref": f"transfer {id}", "partner": partner, "journal_code": "BNK1",
"unique_import_id": f"UIID-{id}", "is_reconciled": False, "amount_residual": amount, "move_id": 8000 + id, "payment_id": None}
class FakeOdoo:
"""The reader port: `read(model)` returns copies of the rows below."""
def __init__(self):
self.db, self.company_currency = DB, "GBP"
self.rows = {
"account.journal": [{"id": 1, "code": "BNK1", "name": "Bank", "type": "bank", "currency": "", "acc_number": "GB00 1234 5678", "gl_account": "1200"}],
"account.payment": [payment(1, 11, 600), payment(2, 11, 500), payment(3, 12, 1000, currency="EUR", date="2026-10-20"),
payment(4, 11, 250), payment(5, 11, 50, "outbound", date="2026-10-25")],
"account.bank.statement.line": [stmt(1, 12, 80), stmt(2, 11, 25), stmt(3, None, 40)],
"account.move.line": [
line(1001, 101, "out_invoice", 11, 300, amount=1000, followup=2, name="INV/101"),
line(1002, 102, "out_invoice", 11, 0, amount=500), line(1003, 103, "out_refund", 11, 0, amount=-100),
line(1004, 104, "out_invoice", 12, 0, amount=850), line(1005, 105, "out_invoice", 11, 0, amount=200),
line(2001, 9001, "entry", 11, 0, amount=-600, payment_id=1), line(2002, 9002, "entry", 11, 0, amount=-500, payment_id=2),
line(2003, 9003, "entry", 12, 0, amount=-1000, currency="EUR", payment_id=3),
line(2004, 9004, "entry", 11, 0, amount=-250, payment_id=4), line(2005, 9005, "entry", 11, 0, amount=50, payment_id=5)],
"account.partial.reconcile": [
partial(1, 1001, 2001, 600), partial(2, 1001, 1003, 100, when="2026-10-12"), partial(3, 1002, 2002, 500, when="2026-10-15"),
partial(4, 1004, 2003, 850, credit_amount=1000, credit_currency="EUR", when="2026-10-20"),
partial(5, 1005, 2004, 200, when="2026-10-11"), partial(6, 2005, 2004, 50, when="2026-10-25")],
"account.full.reconcile": [{"id": 1}],
"res.partner": [{"id": 11, "commercial_partner_id": 11, "credit_limit": 5000, "use_partner_credit_limit": True},
{"id": 12, "commercial_partner_id": 12, "credit_limit": 0, "use_partner_credit_limit": False}],
}
def read(self, model):
return deepcopy(self.rows[model])
class FakePartners:
def __init__(self, mapping=None):
self.mapping = dict(PARTNERS if mapping is None else mapping)
def resolve(self, ids):
return {i: self.mapping[i] for i in ids if i in self.mapping}
def load_ar(nb):
"""The invoices and the credit note ar's migration made (ids odoo:<db>:<account.move id>)."""
a = lambda m: f"odoo:{DB}:{m}" # noqa: E731
nb.invoice(a(101), 7311, "1000.00", number="INV/101", issued_on="2026-09-01", due_date="2026-10-01")
nb.invoice(a(102), 7311, "500.00", number="INV/102", issued_on="2026-09-05", due_date="2026-10-05")
nb.credit_note(a(103), 7311, "100.00", issued_on="2026-10-12")
nb.invoice(a(104), 7312, "850.00", number="INV/104", issued_on="2026-09-10", due_date="2026-10-10")
nb.invoice(a(105), 7311, "200.00", number="INV/105", issued_on="2026-09-11", due_date="2026-10-11")
# ar's own reconcile figure per customer and currency (invoices less credit notes; its migration worked it from Odoo)
nb.ar_balances = {(7311, "GBP"): "300.00", (7312, "GBP"): "0.00"}