feat(sales-invoice): multi presented-certificate per item row
Add a "Sertifikatın seriyə / nömrəsi" multi-select cell to Sales Invoice Item, mirroring the existing tax-article cell: - new child DocType "Sales Invoice Certificate" (parent_row + certificate -> E-Taxes Presented Certificate), parent table custom_item_certificates, per-row display field custom_certificates_display; reuses the existing custom_si_row_key to link rows to certificates. - sales_invoice_certificates.js: clicking an empty cell first asks "0% dərəcəsi ilə ƏDV-yə cəlb olunma haqqında sertifikatlar var?" (yes -> open the Table MultiSelect picker, no -> open nothing); rows that already have certificates open the picker directly. - server hook sync_item_certificates: stable row keys, drop orphans, refresh the display label (not bound to the tax template). - patch add_certificates_to_si_grid_views: inject the new column into saved per-user grid layouts. - lock the tax-article cell (read-only) for any line that has a certificate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4926d5c637
commit
575e0829b3
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Sales Invoice multi presented-certificate — server-side foundation.
|
||||||
|
#
|
||||||
|
# Source of truth for the (possibly multiple) presented certificates of an invoice
|
||||||
|
# line is the parent table `custom_item_certificates` (child DocType "Sales Invoice
|
||||||
|
# Certificate"), whose rows are linked to an item row by `custom_si_row_key` — the
|
||||||
|
# SAME stable row key used by the tax-article multi-select.
|
||||||
|
#
|
||||||
|
# This validate hook:
|
||||||
|
# 1. guarantees every item row has a stable `custom_si_row_key`,
|
||||||
|
# 2. drops ONLY orphaned certificate rows (whose parent_row matches no item row),
|
||||||
|
# 3. refreshes the `custom_certificates_display` text (the grid cell summary).
|
||||||
|
#
|
||||||
|
# Certificates are NOT bound to the line's tax template, so — unlike tax articles —
|
||||||
|
# the selection is never cleared on template change.
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def sync_item_certificates(doc, method=None):
|
||||||
|
if doc.doctype != "Sales Invoice":
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1) ensure each item row has a unique, stable row key (links it to its certs)
|
||||||
|
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 certificate row that points at a real item row; drop only orphans.
|
||||||
|
item_keys = {item.custom_si_row_key for item in doc.get("items", [])}
|
||||||
|
rows = [
|
||||||
|
r
|
||||||
|
for r in (doc.get("custom_item_certificates") or [])
|
||||||
|
if r.get("parent_row") in item_keys and r.get("certificate")
|
||||||
|
]
|
||||||
|
doc.custom_item_certificates = rows
|
||||||
|
|
||||||
|
# 3) refresh the per-row "Sertifikatın seriyə / nömrəsi" display label
|
||||||
|
by_row = {}
|
||||||
|
for r in rows:
|
||||||
|
by_row.setdefault(r.parent_row, []).append(r.certificate)
|
||||||
|
|
||||||
|
for item in doc.get("items", []):
|
||||||
|
certs = by_row.get(item.custom_si_row_key, [])
|
||||||
|
if hasattr(item, "custom_certificates_display"):
|
||||||
|
item.custom_certificates_display = _display_label(len(certs))
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
@ -748,6 +748,23 @@ def create_custom_fields():
|
||||||
options='Sales Invoice Tax Article',
|
options='Sales Invoice Tax Article',
|
||||||
insert_after='custom_item_tax_articles_section'
|
insert_after='custom_item_tax_articles_section'
|
||||||
),
|
),
|
||||||
|
# Multiple presented certificates per item row, stored on the parent and
|
||||||
|
# linked to each item row by custom_si_row_key (same row-key as the tax
|
||||||
|
# articles above; Frappe has no nested child tables).
|
||||||
|
dict(
|
||||||
|
fieldname='custom_item_certificates_section',
|
||||||
|
label='Item Certificates',
|
||||||
|
fieldtype='Section Break',
|
||||||
|
insert_after='custom_item_tax_articles',
|
||||||
|
collapsible=1
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
fieldname='custom_item_certificates',
|
||||||
|
label='Item Certificates',
|
||||||
|
fieldtype='Table',
|
||||||
|
options='Sales Invoice Certificate',
|
||||||
|
insert_after='custom_item_certificates_section'
|
||||||
|
),
|
||||||
# New comments section after time_sheet_list
|
# New comments section after time_sheet_list
|
||||||
dict(
|
dict(
|
||||||
fieldname='comments_section',
|
fieldname='comments_section',
|
||||||
|
|
@ -935,6 +952,18 @@ def create_custom_fields():
|
||||||
hidden=0,
|
hidden=0,
|
||||||
columns=2,
|
columns=2,
|
||||||
description='Click to select tax articles'
|
description='Click to select tax articles'
|
||||||
|
),
|
||||||
|
# --- multi presented-certificate per item row (click the cell to edit) ---
|
||||||
|
dict(
|
||||||
|
fieldname='custom_certificates_display',
|
||||||
|
label='Sertifikatın seriyə / nömrəsi',
|
||||||
|
fieldtype='Data',
|
||||||
|
insert_after='custom_tax_articles_display',
|
||||||
|
read_only=1,
|
||||||
|
in_list_view=1,
|
||||||
|
hidden=0,
|
||||||
|
columns=2,
|
||||||
|
description='Click to select certificates'
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
"Purchase Invoice Item": [
|
"Purchase Invoice Item": [
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ app_include_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_invoice_tax_articles.js",
|
||||||
|
"/assets/jey_erp/js/sales_invoice_certificates.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",
|
||||||
|
|
@ -59,6 +60,7 @@ doc_events = {
|
||||||
"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",
|
"jey_erp.custom.sales_invoice_tax_articles.sync_item_tax_articles",
|
||||||
|
"jey_erp.custom.sales_invoice_certificates.sync_item_certificates",
|
||||||
],
|
],
|
||||||
"on_update": [
|
"on_update": [
|
||||||
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
"jey_erp.custom.sales_invoice.calculate_tax_free_amounts",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2026-06-16 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"parent_row",
|
||||||
|
"certificate"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "parent_row",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Row Key",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certificate",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Certificate",
|
||||||
|
"options": "E-Taxes Presented Certificate",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-06-16 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Jey Erp",
|
||||||
|
"name": "Sales Invoice Certificate",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Copyright (c) 2026, Ali and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class SalesInvoiceCertificate(Document):
|
||||||
|
pass
|
||||||
|
|
@ -15,4 +15,5 @@ jey_erp.patches.remove_won_lost_checkboxes
|
||||||
jey_erp.patches.drop_orphan_bank_doctypes
|
jey_erp.patches.drop_orphan_bank_doctypes
|
||||||
jey_erp.patches.backfill_si_tax_article_row_keys
|
jey_erp.patches.backfill_si_tax_article_row_keys
|
||||||
jey_erp.patches.backfill_sales_invoice_tax_articles_history
|
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
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""Inject the new "Sertifikatın seriyə / nömrəsi" column into saved Sales Invoice
|
||||||
|
Item grid layouts.
|
||||||
|
|
||||||
|
Per-user grid column layouts live in `__UserSettings` (GridView). A layout saved
|
||||||
|
before this feature does not list `custom_certificates_display`. Frappe renders the
|
||||||
|
saved list verbatim and ignores the new in_list_view field, so the certificate cell
|
||||||
|
never appears for those users until they add it manually via the grid gear.
|
||||||
|
|
||||||
|
Add `custom_certificates_display` to every saved layout that doesn't already have it,
|
||||||
|
positioned right after the tax-articles cell. Purely additive and idempotent.
|
||||||
|
"""
|
||||||
|
rows = frappe.db.sql(
|
||||||
|
"select user, data from `__UserSettings` where doctype = 'Sales Invoice'",
|
||||||
|
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 Invoice Item")
|
||||||
|
if not cols:
|
||||||
|
continue
|
||||||
|
|
||||||
|
names = [c.get("fieldname") for c in cols]
|
||||||
|
if "custom_certificates_display" in names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_col = {"fieldname": "custom_certificates_display", "columns": 2}
|
||||||
|
if "custom_tax_articles_display" in names:
|
||||||
|
cols.insert(names.index("custom_tax_articles_display") + 1, new_col)
|
||||||
|
elif "item_tax_template" in names:
|
||||||
|
cols.insert(names.index("item_tax_template") + 1, new_col)
|
||||||
|
else:
|
||||||
|
cols.append(new_col)
|
||||||
|
|
||||||
|
grid_view["Sales Invoice Item"] = cols
|
||||||
|
data["GridView"] = grid_view
|
||||||
|
frappe.db.sql(
|
||||||
|
"update `__UserSettings` set data = %s where user = %s and doctype = 'Sales Invoice'",
|
||||||
|
(json.dumps(data), r.user),
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
|
frappe.cache().delete_value("_user_settings")
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
// Multiple presented certificates per Sales Invoice Item row.
|
||||||
|
// Click the read-only "Sertifikatın seriyə / nömrəsi" cell -> on a row that has no
|
||||||
|
// certificate yet, a yes/no question is asked first ("0% dərəcəsi ilə ƏDV-yə cəlb
|
||||||
|
// olunma haqqında sertifikatlar var?"). Answering "Hə" opens a dialog with a Table
|
||||||
|
// MultiSelect field (a Link control under the hood => native search + Advanced
|
||||||
|
// Search) pre-filled with the row's certificates as pills; answering "Yox" closes
|
||||||
|
// it without opening anything. Once the row already has certificates, the cell opens
|
||||||
|
// the picker directly.
|
||||||
|
//
|
||||||
|
// Source of truth = parent table `custom_item_certificates`, linked to each item row
|
||||||
|
// by `custom_si_row_key` — the SAME row key the tax-article multi-select uses.
|
||||||
|
|
||||||
|
const CERT_DIALOG_CLASS = "certificates-dialog";
|
||||||
|
const CERT_CHILD_DOCTYPE = "Sales Invoice Certificate";
|
||||||
|
const CERT_DISPLAY_FIELD = "custom_certificates_display";
|
||||||
|
const CERT_CONFIRM_MSG = "0% dərəcəsi ilə ƏDV-yə cəlb olunma haqqında sertifikatlar var?";
|
||||||
|
|
||||||
|
if (!document.getElementById("certmulti-style")) {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = "certmulti-style";
|
||||||
|
const F = CERT_DISPLAY_FIELD;
|
||||||
|
const D = "." + CERT_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_cert_setup(frm);
|
||||||
|
},
|
||||||
|
refresh(frm) {
|
||||||
|
ensure_cert_setup(frm);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function ensure_cert_setup(frm) {
|
||||||
|
if (frm.__certificates_bound) return;
|
||||||
|
frm.__certificates_bound = true;
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.off("mousedown.certmulti")
|
||||||
|
.on("mousedown.certmulti", '[data-fieldname="' + CERT_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();
|
||||||
|
handle_certificate_cell_click(frm, cdn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_certificate_cell_click(frm, cdn) {
|
||||||
|
const row = locals["Sales Invoice Item"][cdn];
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
// Already has certificates on this row -> open the picker straight away.
|
||||||
|
const key = row.custom_si_row_key;
|
||||||
|
const has_certs =
|
||||||
|
key &&
|
||||||
|
(frm.doc.custom_item_certificates || []).some(
|
||||||
|
(r) => r.parent_row === key && r.certificate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (has_certs) {
|
||||||
|
open_certificate_dialog(frm, cdn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First time on this row -> ask whether such certificates exist at all.
|
||||||
|
frappe.confirm(
|
||||||
|
__(CERT_CONFIRM_MSG),
|
||||||
|
() => open_certificate_dialog(frm, cdn), // "Hə" / Yes
|
||||||
|
() => {} // "Yox" / No -> open nothing
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function open_certificate_dialog(frm, cdn) {
|
||||||
|
const row = locals["Sales Invoice Item"][cdn];
|
||||||
|
if (!row) 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_certificates || [])
|
||||||
|
.filter((r) => r.parent_row === key && r.certificate)
|
||||||
|
.map((r) => ({ certificate: r.certificate }));
|
||||||
|
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __("Sertifikatın seriyə / nömrəsi") + " — " + (row.item_code || "#" + row.idx),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "certificates",
|
||||||
|
fieldtype: "Table MultiSelect",
|
||||||
|
label: __("Select certificates"),
|
||||||
|
options: CERT_CHILD_DOCTYPE,
|
||||||
|
get_query() {
|
||||||
|
return frm.doc.company ? { filters: { company: frm.doc.company } } : {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action_label: __("Save"),
|
||||||
|
primary_action(values) {
|
||||||
|
const selected = Array.from(
|
||||||
|
new Set((values.certificates || []).map((r) => r.certificate).filter(Boolean))
|
||||||
|
);
|
||||||
|
frm.doc.custom_item_certificates = (frm.doc.custom_item_certificates || []).filter(
|
||||||
|
(r) => r.parent_row !== key
|
||||||
|
);
|
||||||
|
selected.forEach((c) => {
|
||||||
|
const child = frm.add_child("custom_item_certificates");
|
||||||
|
child.parent_row = key;
|
||||||
|
child.certificate = c;
|
||||||
|
});
|
||||||
|
const display = selected.length
|
||||||
|
? __("{0} selected", [selected.length])
|
||||||
|
: __("Click to select");
|
||||||
|
frappe.model.set_value(row.doctype, row.name, CERT_DISPLAY_FIELD, display);
|
||||||
|
frm.refresh_field("custom_item_certificates");
|
||||||
|
frm.refresh_field("items");
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
d.$wrapper.addClass(CERT_DIALOG_CLASS);
|
||||||
|
d.add_custom_action(__("Clear all"), () => d.set_value("certificates", []));
|
||||||
|
|
||||||
|
// Robust pill removal inside a Dialog (Table MultiSelect's own .btn-remove handler
|
||||||
|
// does not update the field value in a dialog). Intercept in the CAPTURE phase and
|
||||||
|
// rebuild the value as: current minus the clicked certificate.
|
||||||
|
const fld = d.fields_dict.certificates;
|
||||||
|
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.certificate))
|
||||||
|
.filter((n) => n && n !== removed)
|
||||||
|
.map((n) => ({ certificate: n }));
|
||||||
|
fld.set_value(remaining);
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
d.set_value("certificates", current);
|
||||||
|
d.show();
|
||||||
|
}
|
||||||
|
|
@ -127,6 +127,24 @@ function open_tax_article_dialog(frm, cdn) {
|
||||||
const row = locals["Sales Invoice Item"][cdn];
|
const row = locals["Sales Invoice Item"][cdn];
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
|
|
||||||
|
// Tax articles are locked (read-only) for a line that already has a presented
|
||||||
|
// certificate — the certificate determines the 0% treatment for that line.
|
||||||
|
const cert_key = row.custom_si_row_key;
|
||||||
|
const has_cert =
|
||||||
|
cert_key &&
|
||||||
|
(frm.doc.custom_item_certificates || []).some(
|
||||||
|
(r) => r.parent_row === cert_key && r.certificate
|
||||||
|
);
|
||||||
|
if (has_cert) {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __(
|
||||||
|
"Tax articles are read-only while a certificate is selected for this line. Remove the certificate to edit them."
|
||||||
|
),
|
||||||
|
indicator: "orange",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const group = tax_article_group_for(row.item_tax_template);
|
const group = tax_article_group_for(row.item_tax_template);
|
||||||
if (!group) {
|
if (!group) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,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 certificates")
|
||||||
_lt("Click to select tax articles")
|
_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")
|
||||||
|
|
@ -96,6 +97,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 Certificates")
|
||||||
_lt("Item Tax Articles")
|
_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.")
|
||||||
|
|
@ -150,6 +152,7 @@ _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")
|
||||||
|
_lt("Sertifikatın seriyə / nömrəsi")
|
||||||
_lt("Settlements")
|
_lt("Settlements")
|
||||||
_lt("Special Tax Regime")
|
_lt("Special Tax Regime")
|
||||||
_lt("Sport Betting Operator")
|
_lt("Sport Betting Operator")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue