dodil/erp-brick-expenses
DODIL ERP brick: expenses
src/brick_expenses/domain/odoo.py
"""Odoo 17's hr_expense as our claims (EXP-24): the arithmetic of the mapping, no I/O.
Plain Python. History is carried read-only and never re-priced or recomputed: a sheet's amounts are Odoo's own, to the penny. What is decided here is only
the status a sheet arrives in and the keys it keeps:
state post -> posted (Odoo's journal entry is carried by gl's migration; this brick posts nothing for it)
state done -> paid (history)
state approve -> approved: Odoo booked nothing for it, so the normal path posts it (the journal row waits in the outbox) and it is paid here
a post sheet whose payment_state is paid is paid; draft, submit and cancel sheets are not carried (finished in Odoo, or re-entered)."""
from datetime import date
from decimal import Decimal, InvalidOperation
from typing import Optional
from brick_expenses.domain.errors import RuleBroken
from brick_expenses.domain.money import ZERO, money
CARRIED = ("approve", "post", "done")
MODELS = ("hr.expense.sheet", "hr.expense", "product.product", "ir.attachment")
def ref_id(v) -> Optional[int]:
"""An Odoo many2one: [id, name] or an id (or False)."""
if isinstance(v, (list, tuple)):
return int(v[0]) if v else None
return int(v) if v else None
def ref_name(v) -> str:
return str(v[1]) if isinstance(v, (list, tuple)) and len(v) > 1 else ""
def report_id(db: str, sheet_id) -> str:
return f"odoo:{db}:hr.expense.sheet:{int(sheet_id)}"
def status_of(state: str, payment_state: Optional[str]) -> str:
if state == "done" or (state == "post" and payment_state == "paid"):
return "paid"
if state == "post":
return "posted"
if state == "approve":
return "approved"
raise RuleBroken(f"an Odoo sheet in state {state!r} is not carried (draft, submitted and cancelled sheets are finished in Odoo)")
def iso(v) -> Optional[date]:
if not v:
return None
return v if isinstance(v, date) else date.fromisoformat(str(v)[:10])
def amount(v, what: str) -> Decimal:
try:
d = Decimal(str(v if v not in (None, False) else 0))
except InvalidOperation:
raise RuleBroken(f"Odoo's {what} {v!r} is not an amount") from None
return money(d)
def month_of(d: date) -> str:
return f"{d.year}-{d.month:02d}"
def sheet_day(sheet: dict) -> date:
"""The day a sheet is dated for the month check: Odoo's accounting date, else its approval date."""
d = iso(sheet.get("accounting_date")) or iso(sheet.get("approval_date"))
if d is None:
raise RuleBroken(f"Odoo sheet {sheet.get('id')} has neither an accounting date nor an approval date")
return d
def own_total(lines: list) -> Decimal:
"""What the employee is owed on a sheet: the own_account lines, in the company currency (Odoo's total_amount)."""
return money(sum((amount(x.get("total_amount"), "total_amount") for x in lines if x.get("payment_mode") != "company_account"), ZERO))