83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""
|
|
Reconciliation of the month-end close.
|
|
|
|
Internal identity (always holds and proves nothing was silently dropped):
|
|
uploaded_total = receivable_orders + paid_orders + transfers_total
|
|
|
|
Closing receivable:
|
|
final_receivable_usd = Σ_marketplace ROUND(reserve + additional_sales) x fx
|
|
+ manual_adjustments
|
|
|
|
Status is "Reconciled" when |identity difference| and |vs-expected difference|
|
|
are within the rounding tolerance (default USD 0.01).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from .settlements import AggregationResult, Classification, RECEIVABLE_ACCOUNT_TYPES
|
|
from .receivable import ReceivableResult
|
|
|
|
DEFAULT_TOLERANCE = 0.01
|
|
|
|
|
|
@dataclass
|
|
class Reconciliation:
|
|
uploaded_total: float = 0.0
|
|
receivable_orders: float = 0.0 # Σ order_total over receivable settlements (all accts)
|
|
paid_orders: float = 0.0 # Σ order_total over paid settlements
|
|
transfers_total: float = 0.0 # Σ total over transfer rows
|
|
reserve_total: float = 0.0
|
|
manual_adjustments: float = 0.0
|
|
final_receivable_usd: float = 0.0
|
|
identity_difference: float = 0.0
|
|
expected_receivable: float | None = None
|
|
expected_difference: float | None = None
|
|
tolerance: float = DEFAULT_TOLERANCE
|
|
status: str = "Reconciled"
|
|
notes: list[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def reconciled(self) -> bool:
|
|
return self.status == "Reconciled"
|
|
|
|
|
|
def reconcile(
|
|
agg: AggregationResult,
|
|
cls: Classification,
|
|
receivable: ReceivableResult,
|
|
manual_adjustments: float = 0.0,
|
|
expected_receivable: float | None = None,
|
|
tolerance: float = DEFAULT_TOLERANCE,
|
|
) -> Reconciliation:
|
|
r = Reconciliation(tolerance=tolerance, manual_adjustments=manual_adjustments,
|
|
expected_receivable=expected_receivable)
|
|
r.uploaded_total = agg.uploaded_total
|
|
for key, st in agg.settlements.items():
|
|
if st.status == "receivable":
|
|
r.receivable_orders += st.order_total
|
|
else:
|
|
r.paid_orders += st.order_total
|
|
r.transfers_total += st.transfer_total
|
|
r.reserve_total = sum(m.reserve for m in receivable.marketplaces.values())
|
|
r.final_receivable_usd = receivable.total_usd + manual_adjustments
|
|
|
|
reconstructed = r.receivable_orders + r.paid_orders + r.transfers_total
|
|
r.identity_difference = r.uploaded_total - reconstructed
|
|
if abs(r.identity_difference) > tolerance:
|
|
r.status = "Unreconciled"
|
|
r.notes.append(
|
|
f"Uploaded total {r.uploaded_total:,.2f} != "
|
|
f"receivable+paid+transfers {reconstructed:,.2f} "
|
|
f"(diff {r.identity_difference:,.2f})."
|
|
)
|
|
if expected_receivable is not None:
|
|
r.expected_difference = r.final_receivable_usd - expected_receivable
|
|
if abs(r.expected_difference) > tolerance:
|
|
r.status = "Unreconciled"
|
|
r.notes.append(
|
|
f"Computed receivable {r.final_receivable_usd:,.2f} vs expected "
|
|
f"{expected_receivable:,.2f} (diff {r.expected_difference:,.2f})."
|
|
)
|
|
return r
|