jey_erp/jey_erp/custom/sales_invoice_tax_articles.py

62 lines
2.6 KiB
Python

# Sales Invoice multi tax-article — server-side foundation.
#
# Source of truth for the (possibly multiple) tax articles of an invoice line is
# the parent table `custom_item_tax_articles` (child DocType "Sales Invoice Tax
# Article"), whose rows are linked to an item row by `custom_si_row_key`.
#
# This validate hook:
# 1. guarantees every item row has a stable `custom_si_row_key` (so SQL joins
# from `Sales Invoice Item` to `Sales Invoice Tax Article` are reliable),
# 2. drops ONLY orphaned article rows (whose parent_row matches no item row) —
# the user's selection is otherwise kept regardless of tax template,
# 3. refreshes the `custom_tax_articles_display` text (the grid cell summary).
#
# It deliberately does NOT write the legacy single `tax_article` field: after
# Phase 2 the consumers (VAT Allocation, tax reports) JOIN the child table, so the
# single field is unused on Sales Invoice — and writing it fought vat_calculator.js
# (which clears it on refresh for non-0% lines), leaving the form perpetually dirty.
import frappe
def sync_item_tax_articles(doc, method=None):
if doc.doctype != "Sales Invoice":
return
# 1) ensure each item row has a unique, stable row key (links it to its articles)
seen = set()
for item in doc.get("items", []):
key = item.get("custom_si_row_key")
if not key or key in seen:
key = "r" + frappe.generate_hash(length=10)
item.custom_si_row_key = key
seen.add(key)
# 2) keep every article row that points at a real item row; drop only orphans.
# NOTE: we intentionally do NOT drop by item_tax_template — the user's
# selection must survive saving even if the template is empty/non-0%.
item_keys = {item.custom_si_row_key for item in doc.get("items", [])}
rows = [
r
for r in (doc.get("custom_item_tax_articles") or [])
if r.get("parent_row") in item_keys and r.get("tax_article")
]
doc.custom_item_tax_articles = rows
# 3) refresh the per-row "Tax Articles" display label
by_row = {}
for r in rows:
by_row.setdefault(r.parent_row, []).append(r.tax_article)
for item in doc.get("items", []):
articles = by_row.get(item.custom_si_row_key, [])
if hasattr(item, "custom_tax_articles_display"):
item.custom_tax_articles_display = _display_label(len(articles))
def _display_label(n: int) -> str:
"""Short summary shown in the grid cell (avoids the Data field length limit)."""
if n:
return frappe._("{0} selected").format(n)
return frappe._("Click to select")