62 lines
2.6 KiB
Python
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")
|