fix(sales-order): show Item Tax Template in the item grid

The column was on Sales Invoice and not on Sales Order, which reads like a Frappe
default but isn't: ERPNext ships item_tax_template with in_list_view = 0 on both
child doctypes. The asymmetry came from show_item_tax_template_in_sales_invoice.py,
which set the property setters for the invoice and — as the name says — only the
invoice. Generalised to both, and renamed, since it is no longer sales-invoice
specific.

The template is what decides the VAT split and which tax-article group the picker
offers, so hiding it from the line the accountant works on is backwards.

The property setter alone is not enough. A user with a grid layout already saved in
__UserSettings gets that list rendered verbatim — Frappe does not merge a
newly-in_list_view field into it — so every pre-existing Sales Order layout would
stay without the column. Hence the patch, same shape as the ones that added the tax
article and certificate cells.

Verified headless: the column now renders in the items grid on both forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-07-14 12:39:39 +00:00
parent df63b036e3
commit d6af639bd0
5 changed files with 101 additions and 61 deletions

View File

@ -0,0 +1,47 @@
"""Surface `item_tax_template` in the item grid of the selling documents.
ERPNext ships the field hidden and out of the grid (in_list_view = 0) on both
Sales Invoice Item and Sales Order Item. Here it drives everything downstream
the VAT split and which tax-article group the picker offers so the accountant
has to see it on the line, not dig into the row's detail view.
This used to cover Sales Invoice only, which is why the column showed up on the
invoice and not on the order.
A Property Setter is only half the story: a user who already has a saved grid
layout gets that layout rendered verbatim, new in_list_view field or not. The
patches (add_item_tax_template_to_so_grid_views) inject the column into the
layouts that predate this.
"""
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
DOCTYPES = ("Sales Invoice Item", "Sales Order Item")
PROPERTIES = {
"hidden": (0, "Check"),
"in_list_view": (1, "Check"),
"columns": (2, "Int"),
"insert_after": ("item_name", "Data"),
}
def show_item_tax_template():
for doctype in DOCTYPES:
for prop, (value, property_type) in PROPERTIES.items():
try:
make_property_setter(
doctype=doctype,
fieldname="item_tax_template",
property=prop,
value=value,
property_type=property_type,
)
except Exception as e:
frappe.log_error(
f"{doctype}.item_tax_template.{prop}: {e}",
"show_item_tax_template",
)
frappe.clear_cache()

View File

@ -1,59 +0,0 @@
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
def show_item_tax_template_in_sales_invoice():
"""
Создает Property Setters для показа item_tax_template в Sales Invoice Item
"""
property_setters = [
{
'doctype': 'Sales Invoice Item',
'fieldname': 'item_tax_template',
'property': 'hidden',
'value': 0
},
{
'doctype': 'Sales Invoice Item',
'fieldname': 'item_tax_template',
'property': 'in_list_view',
'value': 1
},
{
'doctype': 'Sales Invoice Item',
'fieldname': 'item_tax_template',
'property': 'columns',
'value': 2
},
{
'doctype': 'Sales Invoice Item',
'fieldname': 'item_tax_template',
'property': 'insert_after',
'value': 'item_name'
}
]
for ps in property_setters:
try:
make_property_setter(
doctype=ps['doctype'],
fieldname=ps['fieldname'],
property=ps['property'],
value=ps['value'],
property_type=get_property_type(ps['property'])
)
print(f"✓ Set {ps['fieldname']}.{ps['property']} = {ps['value']}")
except Exception as e:
print(f"✗ Error: {e}")
frappe.clear_cache()
def get_property_type(property_name):
"""Возвращает тип свойства"""
types = {
'hidden': 'Check',
'in_list_view': 'Check',
'columns': 'Int',
'insert_after': 'Data'
}
return types.get(property_name, 'Data')

View File

@ -168,8 +168,8 @@ def after_migrate_combined():
from jey_erp.custom_fields import create_custom_fields from jey_erp.custom_fields import create_custom_fields
create_custom_fields() create_custom_fields()
from jey_erp.custom.show_item_tax_template_in_sales_invoice import show_item_tax_template_in_sales_invoice from jey_erp.custom.show_item_tax_template import show_item_tax_template
show_item_tax_template_in_sales_invoice() show_item_tax_template()
from jey_erp.custom.vat_calculations import patch_sales_documents from jey_erp.custom.vat_calculations import patch_sales_documents
patch_sales_documents() patch_sales_documents()
from jey_erp.custom.rename_default_groups import rename_default_groups from jey_erp.custom.rename_default_groups import rename_default_groups

View File

@ -18,3 +18,4 @@ jey_erp.patches.backfill_sales_invoice_tax_articles_history
jey_erp.patches.add_tax_articles_to_si_grid_views jey_erp.patches.add_tax_articles_to_si_grid_views
jey_erp.patches.add_certificates_to_si_grid_views jey_erp.patches.add_certificates_to_si_grid_views
jey_erp.patches.add_tax_articles_to_so_grid_views jey_erp.patches.add_tax_articles_to_so_grid_views
jey_erp.patches.add_item_tax_template_to_so_grid_views

View File

@ -0,0 +1,51 @@
import json
import frappe
def execute():
"""Inject the "Item Tax Template" column into saved Sales Order Item grid layouts.
show_item_tax_template() flips in_list_view on for the field, but a user with a grid
layout saved in `__UserSettings` gets that list rendered verbatim Frappe does not
merge newly-in_list_view fields into it. Sales Invoice had the property setter (and
the column) for a while; Sales Order never did, so every existing Sales Order layout
is missing it.
Insert it just before the Tax Articles cell you pick the template, then the articles
it allows. Idempotent.
"""
rows = frappe.db.sql(
"select user, data from `__UserSettings` where doctype = 'Sales Order'",
as_dict=True,
)
for r in rows:
try:
data = json.loads(r.data or "{}")
except (ValueError, TypeError):
continue
grid_view = data.get("GridView") or {}
cols = grid_view.get("Sales Order Item")
if not cols:
continue
names = [c.get("fieldname") for c in cols]
if "item_tax_template" in names:
continue
new_col = {"fieldname": "item_tax_template", "columns": 2}
if "custom_tax_articles_display" in names:
cols.insert(names.index("custom_tax_articles_display"), new_col)
else:
cols.append(new_col)
grid_view["Sales Order Item"] = cols
data["GridView"] = grid_view
frappe.db.sql(
"update `__UserSettings` set data = %s where user = %s and doctype = 'Sales Order'",
(json.dumps(data), r.user),
)
frappe.db.commit()
frappe.cache().delete_value("_user_settings")