Add Phase 21 stock item master mapping intelligence

This commit is contained in:
A R R R Associates
2026-08-24 22:07:16 +05:30
parent b8fbaf55ec
commit 0fab925e71
11 changed files with 1777 additions and 10 deletions
@@ -7,6 +7,7 @@ from sqlalchemy import select
from app.modules.accounting.chart_models import AccountingChartLedger
from app.modules.accounting.chart_service import effective_role
from app.modules.accounting.stock_service import resolve_stock_item, resolve_unit
from app.modules.accounting.voucher_engine_models import (
AccountingVoucherDraft,
AccountingVoucherItemLine,
@@ -299,6 +300,58 @@ def create_eway_invoice_draft(
cess = round(float(invoice.get("cess") or 0), 2)
gross = round(taxable + cgst + sgst + igst + cess, 2)
# Phase 21 resolves every source item against the synchronized ERP mirror
# before a Purchase/Sales draft is created. Loose fuzzy-only candidates do
# not silently pass through to Tally.
resolved_items = []
unresolved = []
for source_item in invoice.get("items") or []:
source_description = _s(source_item.get("item_name"))
source_hsn = _s(source_item.get("hsn"))
stock = resolve_stock_item(
db,
tenant_id=tenant_id,
client_id=client_id,
tally_guid=tally_guid,
source_type="ewaybill",
party_key=party_name,
source_description=source_description,
hsn_sac=source_hsn,
)
unit = resolve_unit(
db,
tenant_id=tenant_id,
client_id=client_id,
tally_guid=tally_guid,
source_unit=_s(source_item.get("unit")),
)
if not stock.get("matched"):
unresolved.append(
f"{source_description or '(blank item)'}: {stock.get('reason') or 'stock item not mapped'}"
)
continue
if not unit.get("matched"):
unresolved.append(
f"{source_description or '(blank item)'}: {unit.get('reason') or 'unit not mapped'}"
)
continue
resolved_items.append(
{
"source": source_item,
"stock": stock,
"unit": unit,
}
)
if unresolved:
raise ValueError(
"Stock Item Intelligence review is required before this item-wise "
f"{voucher_type} draft can be created. "
+ " | ".join(unresolved[:8])
)
draft = AccountingVoucherDraft(
tenant_id=int(tenant_id),
client_id=int(client_id),
@@ -340,26 +393,48 @@ def create_eway_invoice_draft(
line_no = _append_line(draft, line_no, input_igst, "CR", igst, "Output IGST")
_append_line(draft, line_no, cess_ledger, "CR", cess, "Output Cess")
for index, item in enumerate(invoice.get("items") or [], 1):
for index, resolved in enumerate(resolved_items, 1):
item = resolved["source"]
mapped_stock = resolved["stock"]["stock_item"]
unit_result = resolved["unit"]
item_amount = round(float(item.get("taxable_value") or 0), 2)
qty = float(item.get("quantity") or 0)
rate = float(item.get("rate") or 0)
source_qty = float(item.get("quantity") or 0)
source_rate = float(item.get("rate") or 0)
factor = float(unit_result.get("quantity_factor") or 1.0)
qty = source_qty * factor
rate = source_rate / factor if source_rate and factor else source_rate
if not rate and qty:
rate = item_amount / qty
evidence = {
**item,
"source_type": "ewaybill",
"party_name": party_name,
"source_stock_item_name": _s(item.get("item_name")),
"mapped_stock_item_id": mapped_stock.id,
"mapped_stock_item_name": mapped_stock.name,
"stock_mapping_confidence": int(resolved["stock"].get("confidence") or 0),
"stock_mapping_reason": resolved["stock"].get("reason") or "",
"source_unit": _s(item.get("unit")),
"mapped_unit": unit_result.get("target_unit") or "",
"quantity_factor": factor,
}
draft.items.append(
AccountingVoucherItemLine(
line_no=index,
stock_item_name=_s(item.get("item_name")),
hsn_sac=_s(item.get("hsn")),
stock_item_name=mapped_stock.name,
hsn_sac=_s(item.get("hsn") or mapped_stock.hsn_code),
quantity=qty,
unit=_s(item.get("unit")),
unit=unit_result.get("target_unit") or mapped_stock.base_units,
rate=round(rate, 4),
amount=item_amount,
dr_cr=item_side,
accounting_ledger_name=main.name,
godown_name="",
source_item_json=json.dumps(item, ensure_ascii=False, default=str),
source_item_json=json.dumps(evidence, ensure_ascii=False, default=str),
)
)