97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""v4.9.24: reconciliation process review must be explainable and actionable."""
|
|
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
if "sqlalchemy" not in sys.modules:
|
|
sqlalchemy = types.ModuleType("sqlalchemy")
|
|
sqlalchemy.text = lambda sql: sql
|
|
sys.modules["sqlalchemy"] = sqlalchemy
|
|
|
|
if "app.db" not in sys.modules:
|
|
app_db = types.ModuleType("app.db")
|
|
app_db.engine = object()
|
|
sys.modules["app.db"] = app_db
|
|
|
|
if "app.action_catalog" not in sys.modules:
|
|
action_catalog = types.ModuleType("app.action_catalog")
|
|
action_catalog.get_action_config = lambda code: {"route": "rever", "action": code, "action_required": True, "safe_to_post": False}
|
|
sys.modules["app.action_catalog"] = action_catalog
|
|
|
|
from app import reconciliation_service as svc
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def read(path: str) -> str:
|
|
return (ROOT / path).read_text(encoding="utf-8")
|
|
|
|
|
|
def _item(id_, external_type, number, date, amount):
|
|
return {
|
|
"id": id_,
|
|
"source_system": "jasmin" if external_type.startswith("jasmin") else "odoo",
|
|
"external_type": external_type,
|
|
"title": f"{external_type} · {number}",
|
|
"customer_name": "ACZCO BRAGA ENERGY, LDA",
|
|
"customer_tax_id": "517249200",
|
|
"document_number": number,
|
|
"document_date": date,
|
|
"amount": amount,
|
|
"currency": "EUR",
|
|
"payload": {"record": {"name": number, "invoice_status": "to invoice"}} if external_type == "odoo_sale_order" else {},
|
|
"operation_suggestions": [],
|
|
}
|
|
|
|
|
|
def test_process_candidates_explain_reasons_and_risks(monkeypatch):
|
|
monkeypatch.setattr(
|
|
svc,
|
|
"list_reconciliation_items",
|
|
lambda *, status, limit, days: [
|
|
_item("quote-154", "jasmin_quotation", "ORC.ORC2026.154", "2026-06-03", "638.60"),
|
|
_item("sale-279", "odoo_sale_order", "S00279", "2026-06-05", "638.60"),
|
|
],
|
|
)
|
|
|
|
candidates = svc.list_reconciliation_process_candidates(status="open", days=7, limit=10)
|
|
|
|
assert len(candidates) == 1
|
|
candidate = candidates[0]
|
|
assert candidate["review_status"] == "needs_review"
|
|
assert "cliente fiscal por NIF exato" in candidate["reasons"]
|
|
assert "valor igual entre documentos" in candidate["reasons"]
|
|
assert "datas próximas" in candidate["reasons"]
|
|
assert "venda Odoo sem fatura Jasmin associada" in candidate["risks"]
|
|
|
|
|
|
def test_process_candidates_flag_conflict_when_amounts_differ(monkeypatch):
|
|
monkeypatch.setattr(
|
|
svc,
|
|
"list_reconciliation_items",
|
|
lambda *, status, limit, days: [
|
|
_item("quote-154", "jasmin_quotation", "ORC.ORC2026.154", "2026-06-03", "638.60"),
|
|
_item("invoice-80", "jasmin_invoice", "FA2026.80", "2026-06-05", "190.00"),
|
|
],
|
|
)
|
|
|
|
candidates = svc.list_reconciliation_process_candidates(status="open", days=7, limit=10)
|
|
|
|
assert len(candidates) == 2 # different amounts keep separate purchase anchors
|
|
assert all("reasons" in candidate and "risks" in candidate for candidate in candidates)
|
|
|
|
|
|
def test_v4924_schema_and_ui_have_decisions_review_and_history_actions():
|
|
service = read("app/reconciliation_service.py")
|
|
page = read("app/admin_ui/pages/reconciliation.py")
|
|
styles = read("app/admin_ui/styles.py")
|
|
|
|
assert "CREATE TABLE IF NOT EXISTS reconciliation_decisions" in service
|
|
assert "def _candidate_reasons_and_risks" in service
|
|
assert "reasons" in service and "risks" in service and "review_status" in service
|
|
assert "/reconciliation/processes/needs-review" in page
|
|
assert "/reconciliation/processes/historical" in page
|
|
assert "Motivos" in page and "Riscos" in page
|
|
assert "cf-reconcile-explain" in styles
|