feat(sales-invoice): multiple Tax Articles per item row + Payment Entry multi-select

Per-row multi-select of Tax Article records on Sales Invoice item rows
(VAT 0% / VAT-exempt, codes 302.x/303.x) via a parent child table linked
by a stable row key; Tax Article multi-select on Payment Entry. Legacy
single tax_article demoted to hidden/read-only.

- custom_fields.py: new fields + section; legacy tax_article -> primary
- custom/sales_invoice_tax_articles.py: validate hook (row keys + display)
- doctype/{sales_invoice,payment_entry}_tax_article: child tables (0%/exempt)
- public/js: Table MultiSelect dialog + vat_calculator SI refresh-chain fix
- patches: backfill row keys + historic single-article values

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-02 12:25:24 +00:00
parent 4c1f582db9
commit ea8818d07f
15 changed files with 535 additions and 52 deletions

View File

@ -0,0 +1,61 @@
# 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) mirror first article -> single field + refresh the 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")

View File

@ -416,6 +416,14 @@ def create_custom_fields():
fieldtype='Small Text', fieldtype='Small Text',
insert_after='payment_type', insert_after='payment_type',
), ),
dict(
fieldname='tax_article',
label='Tax Article',
fieldtype='Table MultiSelect',
options='Payment Entry Tax Article',
insert_after='purpose',
description='Yalnız 0% və ƏDV-dən azad olunan əməliyyatlar üzrə maddələr',
),
], ],
"Purchase Order": [ "Purchase Order": [
dict( dict(
@ -497,7 +505,7 @@ def create_custom_fields():
label='E-Taxes Purchase Act', label='E-Taxes Purchase Act',
fieldtype='Section Break', fieldtype='Section Break',
insert_after='amended_from', insert_after='amended_from',
collapsible=1 collapsible=0
), ),
dict( dict(
fieldname='act_kind', fieldname='act_kind',
@ -723,6 +731,21 @@ def create_custom_fields():
insert_after='act_type', insert_after='act_type',
depends_on='eval:doc.purchase_type && doc.act_type == "Import"' depends_on='eval:doc.purchase_type && doc.act_type == "Import"'
), ),
# --- TEST: backing store for per-item multi tax-article ---
dict(
fieldname='custom_item_tax_articles_section',
label='Item Tax Articles',
fieldtype='Section Break',
insert_after='items',
collapsible=1
),
dict(
fieldname='custom_item_tax_articles',
label='Item Tax Articles',
fieldtype='Table',
options='Sales Invoice Tax Article',
insert_after='custom_item_tax_articles_section'
),
# New comments section after time_sheet_list # New comments section after time_sheet_list
dict( dict(
fieldname='comments_section', fieldname='comments_section',
@ -880,13 +903,35 @@ def create_custom_fields():
), ),
dict( dict(
fieldname='tax_article', fieldname='tax_article',
label='Tax Article', label='Tax Article (primary)',
fieldtype='Link', fieldtype='Link',
options='Tax Article', options='Tax Article',
insert_after='vat_free_amount', insert_after='vat_free_amount',
in_list_view=0,
hidden=1,
read_only=1,
no_copy=1,
description='Auto-synced to the first selected article. Replaced by the multi-select "Tax Articles (multi)" cell.'
),
# --- multi tax-article per item row (click the cell to edit) ---
dict(
fieldname='custom_si_row_key',
label='Row Key',
fieldtype='Data',
insert_after='tax_article',
read_only=1,
hidden=1,
no_copy=1
),
dict(
fieldname='custom_tax_articles_display',
label='Tax Articles',
fieldtype='Data',
insert_after='custom_si_row_key',
read_only=1,
in_list_view=1, in_list_view=1,
columns=2, columns=2,
description='Tax Article field for VAT purposes' description='Click to select tax articles'
) )
], ],
"Purchase Invoice Item": [ "Purchase Invoice Item": [

View File

@ -12,6 +12,7 @@ app_include_js = [
"/assets/jey_erp/js/vat_calculator.js", "/assets/jey_erp/js/vat_calculator.js",
"/assets/jey_erp/js/sales_invoice.js", "/assets/jey_erp/js/sales_invoice.js",
"/assets/jey_erp/js/sales_invoice_vat.js", "/assets/jey_erp/js/sales_invoice_vat.js",
"/assets/jey_erp/js/sales_invoice_tax_articles.js",
"/assets/jey_erp/js/sales_order_vat.js", "/assets/jey_erp/js/sales_order_vat.js",
"/assets/jey_erp/js/asset.js", "/assets/jey_erp/js/asset.js",
"/assets/jey_erp/js/link_field_fix.js", "/assets/jey_erp/js/link_field_fix.js",
@ -56,6 +57,7 @@ doc_events = {
"Sales Invoice": { "Sales Invoice": {
"validate": [ "validate": [
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts", "jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
"jey_erp.custom.sales_invoice_tax_articles.sync_item_tax_articles",
], ],
"on_update": [ "on_update": [
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts", "jey_erp.custom.sales_invoice.calculate_tax_free_amounts",

View File

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2026-06-01 00:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"tax_article"
],
"fields": [
{
"fieldname": "tax_article",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax Article",
"link_filters": "[[\"Tax Article\",\"parent_tax_article\",\"in\",[\"ƏDV-yə 0% dərəcə ilə tutulan əməliyyatlar\",\"ƏDV-dən azad olunan əməliyyatlar\"]]]",
"options": "Tax Article",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-06-01 00:00:00.000000",
"modified_by": "Administrator",
"module": "Jey Erp",
"name": "Payment Entry Tax Article",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2026, Ali and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class PaymentEntryTaxArticle(Document):
pass

View File

@ -0,0 +1,41 @@
{
"actions": [],
"creation": "2026-06-01 00:00:00.000000",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"parent_row",
"tax_article"
],
"fields": [
{
"fieldname": "parent_row",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Row Key",
"read_only": 1
},
{
"fieldname": "tax_article",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax Article",
"link_filters": "[[\"Tax Article\",\"parent_tax_article\",\"in\",[\"ƏDV-yə 0% dərəcə ilə tutulan əməliyyatlar\",\"ƏDV-dən azad olunan əməliyyatlar\"]]]",
"options": "Tax Article",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-06-01 00:00:00.000000",
"modified_by": "Administrator",
"module": "Jey Erp",
"name": "Sales Invoice Tax Article",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2026, Ali and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class SalesInvoiceTaxArticle(Document):
pass

View File

@ -12,3 +12,5 @@ jey_erp.patches.remove_old_won_lost_fields
jey_erp.patches.remove_won_lost_breaks jey_erp.patches.remove_won_lost_breaks
jey_erp.patches.remove_won_lost_checkboxes jey_erp.patches.remove_won_lost_checkboxes
jey_erp.patches.drop_bank_integration_account_mapping jey_erp.patches.drop_bank_integration_account_mapping
jey_erp.patches.backfill_si_tax_article_row_keys
jey_erp.patches.backfill_sales_invoice_tax_articles_history

View File

@ -0,0 +1,66 @@
import frappe
def execute():
"""Migrate historic single `tax_article` values into the multi child table.
Phase 2 of the multi tax-article migration. The Sales-Invoice tax_article
consumers (VAT Allocation, tax-authority reports) now JOIN
`Sales Invoice Tax Article` (parent table `custom_item_tax_articles`, linked by
`custom_si_row_key`). Historic Sales Invoice Items still carry their old single
`tax_article`; copy each into a child row so reports over past data keep working.
Idempotent: skips rows already present.
"""
if not frappe.db.has_column("Sales Invoice Item", "custom_si_row_key"):
return
if not frappe.db.exists("DocType", "Sales Invoice Tax Article"):
return
items = frappe.db.sql(
"""
select sii.parent as si, sii.custom_si_row_key as rk, sii.tax_article as ta
from `tabSales Invoice Item` sii
where ifnull(sii.tax_article, '') != ''
and ifnull(sii.custom_si_row_key, '') != ''
""",
as_dict=True,
)
created = 0
for it in items:
already = frappe.db.exists(
"Sales Invoice Tax Article",
{
"parenttype": "Sales Invoice",
"parent": it.si,
"parentfield": "custom_item_tax_articles",
"parent_row": it.rk,
"tax_article": it.ta,
},
)
if already:
continue
max_idx = frappe.db.sql(
"""
select ifnull(max(idx), 0) from `tabSales Invoice Tax Article`
where parent=%s and parenttype='Sales Invoice'
and parentfield='custom_item_tax_articles'
""",
it.si,
)[0][0]
child = frappe.new_doc("Sales Invoice Tax Article")
child.parent = it.si
child.parenttype = "Sales Invoice"
child.parentfield = "custom_item_tax_articles"
child.parent_row = it.rk
child.tax_article = it.ta
child.idx = (max_idx or 0) + 1
child.insert(ignore_permissions=True)
created += 1
frappe.db.commit()
frappe.logger().info(
f"backfill_sales_invoice_tax_articles_history: created {created} child rows"
)

View File

@ -0,0 +1,30 @@
import frappe
def execute():
"""Backfill a stable custom_si_row_key for existing Sales Invoice Item rows.
Phase 1 of the multi tax-article migration. Only fills missing row keys so
that future joins from `Sales Invoice Item` to `Sales Invoice Tax Article`
are reliable. Does NOT touch tax_article values (history keeps its single
value, which the taxes_az consumers still read during the transition).
"""
if not frappe.db.has_column("Sales Invoice Item", "custom_si_row_key"):
return
rows = frappe.db.get_all(
"Sales Invoice Item",
filters={"custom_si_row_key": ["in", ["", None]]},
pluck="name",
)
for name in rows:
frappe.db.set_value(
"Sales Invoice Item",
name,
"custom_si_row_key",
"r" + frappe.generate_hash(length=10),
update_modified=False,
)
frappe.db.commit()
frappe.logger().info(f"backfill_si_tax_article_row_keys: filled {len(rows)} row keys")

View File

@ -0,0 +1,210 @@
// Multiple Tax Articles per Sales Invoice Item row.
// Click the read-only "Tax Articles" cell -> a dialog opens with a Table MultiSelect
// field (a Link control under the hood => native search + native Advanced Search)
// pre-filled with the row's articles as pills. Source of truth = parent table
// `custom_item_tax_articles`, linked to each item row by `custom_si_row_key`.
//
// The article list is filtered to the parent group matching the line's tax template
// (ƏDV 0% -> 0% group, ƏDV-dən azadolma -> exempt group); the dialog only opens for
// those templates.
const DIALOG_CLASS = "tax-articles-dialog";
const CHILD_DOCTYPE = "Sales Invoice Tax Article";
const DISPLAY_FIELD = "custom_tax_articles_display";
const GROUP_ZERO = "ƏDV-yə 0% dərəcə ilə tutulan əməliyyatlar";
const GROUP_EXEMPT = "ƏDV-dən azad olunan əməliyyatlar";
function tax_article_group_for(template) {
const t = (template || "").trim();
if (t.indexOf("ƏDV 0%") === 0) return GROUP_ZERO;
if (t.indexOf("ƏDV-dən azadolma") === 0) return GROUP_EXEMPT;
return null;
}
if (!document.getElementById("taxmulti-style")) {
const style = document.createElement("style");
style.id = "taxmulti-style";
const F = DISPLAY_FIELD;
const D = "." + DIALOG_CLASS;
style.textContent = [
// grid cell: clickable, click passes through the read-only input
'.grid-row [data-fieldname="' + F + '"]{cursor:pointer;}',
'.grid-row [data-fieldname="' + F + '"] input,',
'.grid-row [data-fieldname="' + F + '"] textarea,',
'.grid-row [data-fieldname="' + F + '"] .like-disabled-input{pointer-events:none;}',
// keep the dropdown (incl. native "Advanced Search") reachable, not clipped
D + " .modal-body," + D + " .frappe-control," + D + " .form-group," +
D + " .control-input-wrapper{overflow:visible!important;}",
D + " .awesomplete > ul{max-height:260px;overflow-y:auto;z-index:1080;}",
// ONE seamless surface: keep only the single .modal-content gradient
D + " .modal-header," + D + " .modal-body," + D + " .modal-footer," +
D + " .form-layout," + D + " .form-page," + D + " .form-section," +
D + " .section-body," + D + " .form-column," + D + " .table-multiselect{" +
"background:transparent!important;background-image:none!important;" +
"box-shadow:none!important;border:none!important;}",
// thin dividers under the title and above the footer
D + " .modal-header{border-bottom:1px solid rgba(0,0,0,.15)!important;}",
D + " .modal-footer{border-top:1px solid rgba(0,0,0,.12)!important;}",
// trim the empty space above the field
D + " .modal-body{padding-top:14px!important;}",
// the search input as its own bordered field, just below the pills
D + " .table-multiselect .awesomplete{display:block;width:100%;margin-top:8px;}",
D + " .table-multiselect .awesomplete input{display:block;width:100%;" +
"border:1px solid var(--border-color,#999)!important;" +
"border-radius:var(--border-radius,6px)!important;padding:4px 8px!important;min-height:28px;}",
].join("");
document.head.appendChild(style);
}
frappe.ui.form.on("Sales Invoice", {
onload(frm) {
ensure_setup(frm);
},
refresh(frm) {
ensure_setup(frm);
},
});
frappe.ui.form.on("Sales Invoice Item", {
// When the tax template changes, the row's already-selected articles belong to the
// previous template's group and are no longer valid -> clear them and warn.
item_tax_template(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (!row || !row.custom_si_row_key) return;
const key = row.custom_si_row_key;
const had = (frm.doc.custom_item_tax_articles || []).some((r) => r.parent_row === key);
if (!had) return;
frm.doc.custom_item_tax_articles = (frm.doc.custom_item_tax_articles || []).filter(
(r) => r.parent_row !== key
);
frm.refresh_field("custom_item_tax_articles");
const allowed = !!tax_article_group_for(row.item_tax_template);
frappe.model.set_value(cdt, cdn, DISPLAY_FIELD, allowed ? __("Click to select") : "");
frappe.show_alert(
{
message: __("Tax articles for {0} were cleared because the tax template changed.", [
row.item_code || "#" + row.idx,
]),
indicator: "orange",
},
7
);
},
});
function ensure_setup(frm) {
if (frm.__tax_articles_bound) return;
frm.__tax_articles_bound = true;
$(document)
.off("mousedown.taxmulti")
.on("mousedown.taxmulti", '[data-fieldname="' + DISPLAY_FIELD + '"]', function (ev) {
const grid_row = $(this).closest(".grid-row").data("grid_row");
const cdn = grid_row && grid_row.doc && grid_row.doc.name;
if (cdn && frm.fields_dict.items && frm.fields_dict.items.grid) {
ev.stopPropagation();
open_tax_article_dialog(frm, cdn);
}
});
}
function open_tax_article_dialog(frm, cdn) {
const row = locals["Sales Invoice Item"][cdn];
if (!row) return;
const group = tax_article_group_for(row.item_tax_template);
if (!group) {
frappe.show_alert({
message: __(
"Tax articles apply only to ƏDV 0% / ƏDV-dən azadolma lines. Set the item's Tax Template first."
),
indicator: "orange",
});
return;
}
if (!row.custom_si_row_key) {
row.custom_si_row_key = "r" + Math.random().toString(36).slice(2, 12);
}
const key = row.custom_si_row_key;
// Table MultiSelect value = array of child-row objects keyed by the link field.
const current = (frm.doc.custom_item_tax_articles || [])
.filter((r) => r.parent_row === key && r.tax_article)
.map((r) => ({ tax_article: r.tax_article }));
const d = new frappe.ui.Dialog({
title: __("Tax Articles") + " — " + (row.item_code || "#" + row.idx),
fields: [
{
fieldname: "articles",
fieldtype: "Table MultiSelect",
label: __("Select tax articles"),
options: CHILD_DOCTYPE,
get_query() {
return { filters: { parent_tax_article: group } };
},
},
],
primary_action_label: __("Save"),
primary_action(values) {
const selected = Array.from(
new Set((values.articles || []).map((r) => r.tax_article).filter(Boolean))
);
frm.doc.custom_item_tax_articles = (frm.doc.custom_item_tax_articles || []).filter(
(r) => r.parent_row !== key
);
selected.forEach((a) => {
const c = frm.add_child("custom_item_tax_articles");
c.parent_row = key;
c.tax_article = a;
});
const display = selected.length
? __("{0} selected", [selected.length])
: __("Click to select");
frappe.model.set_value(row.doctype, row.name, DISPLAY_FIELD, display);
frm.refresh_field("custom_item_tax_articles");
frm.refresh_field("items");
d.hide();
},
});
d.$wrapper.addClass(DIALOG_CLASS);
d.add_custom_action(__("Clear all"), () => d.set_value("articles", []));
// Robust pill removal inside a Dialog. Table MultiSelect's own `.btn-remove`
// handler does not update the field value in a dialog (it relies on a form's child
// docs), which causes the "needs a second click / removes the wrong pills"
// behaviour. We intercept the click in the CAPTURE phase, stop the control's broken
// handler, and do the removal ourselves: value = current minus the clicked article.
const fld = d.fields_dict.articles;
fld.$wrapper[0].addEventListener(
"click",
function (e) {
const btn = e.target.closest(".btn-remove");
if (!btn) return;
e.stopPropagation();
const pill = btn.closest(".tb-selected-value");
const removed = pill ? decodeURIComponent(pill.getAttribute("data-value") || "") : "";
const remaining = (fld.get_value() || [])
.map((r) => (typeof r === "string" ? r : r && r.tax_article))
.filter((n) => n && n !== removed)
.map((n) => ({ tax_article: n }));
fld.set_value(remaining);
},
true
);
d.set_value("articles", current);
d.show();
}

View File

@ -13,7 +13,12 @@ jey_erp.vat_calculator = {
// 1. Стандартные триггеры Frappe // 1. Стандартные триггеры Frappe
frappe.ui.form.on(doctype, { frappe.ui.form.on(doctype, {
refresh: (frm) => { refresh: (frm) => {
if (frm.doc.docstatus === 0) { // Do NOT recompute on every open: clear_all_vat_fields() does
// set_value(...,0) which marks the form dirty on load. VAT fields are
// recomputed by the qty/rate/amount/item_tax_template triggers when the
// user edits, and authoritatively on the server at save. Skipping the
// refresh recompute for Sales Invoice keeps it clean ("only when needed").
if (frm.doc.docstatus === 0 && doctype !== 'Sales Invoice') {
this.setup_vat_system(frm, child_doctype); this.setup_vat_system(frm, child_doctype);
} }
} }
@ -114,6 +119,11 @@ jey_erp.vat_calculator = {
// 3. ОБНОВЛЕННАЯ функция с поддержкой индивидуальных фильтров для tax_article // 3. ОБНОВЛЕННАЯ функция с поддержкой индивидуальных фильтров для tax_article
update_tax_article_readonly: function(frm, cdt, cdn) { update_tax_article_readonly: function(frm, cdt, cdn) {
// On Sales Invoice the single `tax_article` field is vestigial (hidden;
// replaced by the multi-select child table). Touching/clearing it here
// marks the form dirty on every refresh — skip it entirely for Sales Invoice.
if (frm && frm.doctype === 'Sales Invoice') return;
const item = locals[cdt][cdn]; const item = locals[cdt][cdn];
if (!item?.item_code) return; if (!item?.item_code) return;
@ -237,57 +247,17 @@ jey_erp.vat_calculator = {
}); });
}, },
// Альтернативный метод получения актуальных значений из DOM
get_current_values: function(frm, cdt, cdn) {
const item = locals[cdt][cdn];
const row_index = frm.doc.items.findIndex(i => i.name === cdn);
if (row_index === -1) {
return {
qty: flt(item.qty || 0),
rate: flt(item.rate || 0),
amount: flt(item.qty || 0) * flt(item.rate || 0)
};
}
// Пытаемся получить значения из DOM полей
const grid_row = frm.fields_dict.items.grid?.grid_rows?.[row_index];
if (grid_row) {
const qty_field = grid_row.get_field('qty');
const rate_field = grid_row.get_field('rate');
const qty = qty_field ? flt(qty_field.get_value()) : flt(item.qty || 0);
const rate = rate_field ? flt(rate_field.get_value()) : flt(item.rate || 0);
return {
qty: qty,
rate: rate,
amount: qty * rate
};
}
// Fallback к обычному способу
return {
qty: flt(item.qty || 0),
rate: flt(item.rate || 0),
amount: flt(item.qty || 0) * flt(item.rate || 0)
};
},
calculate_vat_fields: function(cdt, cdn, frm = null) { calculate_vat_fields: function(cdt, cdn, frm = null) {
const item = locals[cdt][cdn]; const item = locals[cdt][cdn];
// Получаем актуальные значения // amount считаем из значений МОДЕЛИ (locals), а не из DOM.
let amount; // Прежний вариант читал qty/rate через grid_row.get_field(...), который
if (frm) { // бросает "fieldname qty not found", когда колонка не отрисована / строка
const values = this.get_current_values(frm, cdt, cdn); // свёрнута — это исключение рвало всю последовательную refresh-цепочку
amount = values.amount; // Sales Invoice (frappe.run_serially) и отключало остальные refresh-хендлеры.
} else { // К моменту триггеров qty/rate/amount значения в locals уже актуальны.
// Fallback: вычисляем из значений в locals const amount = flt(item.qty || 0) * flt(item.rate || 0);
const qty = flt(item.qty || 0);
const rate = flt(item.rate || 0);
amount = qty * rate;
}
// Очищаем все поля НДС перед расчетом // Очищаем все поля НДС перед расчетом
this.clear_all_vat_fields(cdt, cdn); this.clear_all_vat_fields(cdt, cdn);

View File

@ -30,6 +30,7 @@ _lt("Area")
_lt("Asset Type") _lt("Asset Type")
_lt("Ata adı") _lt("Ata adı")
_lt("Auto-calculated as 5% of amount when Tax Type is Taxable") _lt("Auto-calculated as 5% of amount when Tax Type is Taxable")
_lt("Auto-synced to the first selected article. Replaced by the multi-select \"Tax Articles (multi)\" cell.")
_lt("Bank Accounts") _lt("Bank Accounts")
_lt("Bank Code") _lt("Bank Code")
_lt("Bank Integration") _lt("Bank Integration")
@ -41,6 +42,7 @@ _lt("Cash Registers")
_lt("Chief Executive Officer") _lt("Chief Executive Officer")
_lt("Citizenship Country") _lt("Citizenship Country")
_lt("City (for print formats)") _lt("City (for print formats)")
_lt("Click to select tax articles")
_lt("Code of the Cadastral Valuation District") _lt("Code of the Cadastral Valuation District")
_lt("Code of the Territorial Unit") _lt("Code of the Territorial Unit")
_lt("Comment") _lt("Comment")
@ -92,6 +94,7 @@ _lt("Is Sub Account")
_lt("Is Taxpayer in Cancellation Process") _lt("Is Taxpayer in Cancellation Process")
_lt("Is taxes document") _lt("Is taxes document")
_lt("Item") _lt("Item")
_lt("Item Tax Articles")
_lt("Job Applicant") _lt("Job Applicant")
_lt("Kassa metodu — gəlir və xərclər yalnız ödəniş zamanı tanınır. ƏDV öhdəliyi yalnız ödəniş alındıqda yaranır.\nHesablama metodu — gəlir və xərclər faktura zamanı tanınır.") _lt("Kassa metodu — gəlir və xərclər yalnız ödəniş zamanı tanınır. ƏDV öhdəliyi yalnız ödəniş alındıqda yaranır.\nHesablama metodu — gəlir və xərclər faktura zamanı tanınır.")
_lt("Land") _lt("Land")
@ -141,6 +144,7 @@ _lt("Quality Groups")
_lt("Reason") _lt("Reason")
_lt("Registration Information") _lt("Registration Information")
_lt("Residence Permit FIN") _lt("Residence Permit FIN")
_lt("Row Key")
_lt("SSN") _lt("SSN")
_lt("Selected customer object from E-Taxes (auto-filled)") _lt("Selected customer object from E-Taxes (auto-filled)")
_lt("Seller") _lt("Seller")
@ -153,7 +157,9 @@ _lt("Suspension End Date")
_lt("Suspension Start Date") _lt("Suspension Start Date")
_lt("TIN Type") _lt("TIN Type")
_lt("Tax Article") _lt("Tax Article")
_lt("Tax Article (primary)")
_lt("Tax Article field for VAT purposes") _lt("Tax Article field for VAT purposes")
_lt("Tax Articles")
_lt("Tax Authority") _lt("Tax Authority")
_lt("Tax Closing Wizards") _lt("Tax Closing Wizards")
_lt("Tax Exempt Assets Information") _lt("Tax Exempt Assets Information")
@ -184,6 +190,7 @@ _lt("VAT free amount")
_lt("VAT registration date") _lt("VAT registration date")
_lt("Vergi") _lt("Vergi")
_lt("Wizards") _lt("Wizards")
_lt("Yalnız 0% və ƏDV-dən azad olunan əməliyyatlar üzrə maddələr")
_lt("taxes_doc") _lt("taxes_doc")
_lt("İcbari Tibbi Sığorta") _lt("İcbari Tibbi Sığorta")
_lt("İcbari tibbi sığorta üzrə") _lt("İcbari tibbi sığorta üzrə")