From 68db53ad3497813b14121f7b7c45457883708142 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 25 Jul 2025 16:33:19 +0400 Subject: [PATCH] added tax free field to sales invoice tax table --- jey_erp/custom/sales_invoice.py | 96 ++++++++++++++ jey_erp/custom_fields.py | 15 ++- jey_erp/hooks.py | 10 +- jey_erp/public/js/sales_invoice.js | 194 +++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 jey_erp/custom/sales_invoice.py create mode 100644 jey_erp/public/js/sales_invoice.js diff --git a/jey_erp/custom/sales_invoice.py b/jey_erp/custom/sales_invoice.py new file mode 100644 index 0000000..089e2dc --- /dev/null +++ b/jey_erp/custom/sales_invoice.py @@ -0,0 +1,96 @@ +import frappe +import json +from frappe.utils import flt + + +def calculate_tax_free_amounts(doc, method=None): + """ + Calculate tax free amounts for Sales Invoice + Called after calculate_taxes_and_totals + """ + if doc.doctype != "Sales Invoice": + return + + # Clear existing tax_free_amount values + for tax in doc.get("taxes", []): + if hasattr(tax, 'tax_free_amount'): + tax.tax_free_amount = 0.0 + + # Calculate tax free amounts + for item in doc.get("items", []): + if not item.get("item_code"): + continue + + # Try to get tax template from item row first + item_tax_template = item.get("item_tax_template") + item_tax_rate = item.get("item_tax_rate") + + # If no tax info in item row, get from Item master + if not item_tax_template and not item_tax_rate: + try: + item_doc = frappe.get_doc("Item", item.item_code) + item_tax_template = item_doc.get("item_tax_template") + except Exception: + continue + + # Process item_tax_rate (JSON format) + if item_tax_rate: + try: + if isinstance(item_tax_rate, str): + tax_rates = json.loads(item_tax_rate) + else: + tax_rates = item_tax_rate + + for account_head, rate in tax_rates.items(): + if flt(rate) == 0.0: + # Find corresponding tax in sales invoice + for doc_tax in doc.get("taxes", []): + if doc_tax.account_head == account_head: + if hasattr(doc_tax, 'tax_free_amount'): + doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount) + break + except Exception: + pass + + # Process item_tax_template + elif item_tax_template: + try: + tax_template = frappe.get_doc("Item Tax Template", item_tax_template) + + for template_tax in tax_template.get("taxes", []): + if flt(template_tax.tax_rate) == 0.0: + # Find corresponding tax in sales invoice + for doc_tax in doc.get("taxes", []): + if doc_tax.account_head == template_tax.tax_type: + if hasattr(doc_tax, 'tax_free_amount'): + doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount) + break + except Exception: + pass + + +@frappe.whitelist() +def recalculate_tax_free_amounts(doc): + """ + API method to recalculate tax free amounts + Called from client side when items change + """ + if isinstance(doc, str): + doc = frappe.parse_json(doc) + + # Convert to document object if it's a dict + if isinstance(doc, dict): + sales_invoice = frappe.get_doc(doc) + else: + sales_invoice = doc + + # Calculate tax free amounts + calculate_tax_free_amounts(sales_invoice) + + # Return updated taxes + taxes_data = [] + for tax in sales_invoice.get("taxes", []): + tax_dict = tax.as_dict() if hasattr(tax, 'as_dict') else dict(tax) + taxes_data.append(tax_dict) + + return taxes_data \ No newline at end of file diff --git a/jey_erp/custom_fields.py b/jey_erp/custom_fields.py index 717c88b..7e1331b 100644 --- a/jey_erp/custom_fields.py +++ b/jey_erp/custom_fields.py @@ -91,6 +91,16 @@ def create_custom_fields(): insert_after='comments_column_break' ) ], + "Sales Taxes and Charges": [ + dict( + fieldname='tax_free_amount', + label='Tax Free Amount', + fieldtype='Currency', + insert_after='tax_amount', + in_list_view=1, + columns=2 + ) + ], "Item": [ # Product Group Code field in Details tab dict( @@ -633,5 +643,8 @@ def create_custom_fields(): print(f"Created field {field['fieldname']} in {doctype}") except Exception as e: print(f"Failed to create field {field['fieldname']} in {doctype}: {e}") - + + # Register the calculation hook for existing Sales Invoices + print("Setting up Tax Free Amount calculation hooks...") + create_custom_fields() \ No newline at end of file diff --git a/jey_erp/hooks.py b/jey_erp/hooks.py index 950be43..355d677 100644 --- a/jey_erp/hooks.py +++ b/jey_erp/hooks.py @@ -9,12 +9,14 @@ app_license = "unlicense" boot_session = "jey_erp.extend_party_types.add_company_to_party_account_types" app_include_js = [ - "/assets/jey_erp/js/account_tree_custom.js" + "/assets/jey_erp/js/account_tree_custom.js", + "/assets/jey_erp/js/sales_invoice.js" ] doctype_js = { "Payment Entry": "/public/js/payment_entry_mod.js", - "Account": "/public/js/account_tree_custom.js" + "Account": "/public/js/account_tree_custom.js", + "Sales Invoice": "public/js/sales_invoice.js" } override_doctype_class = { @@ -24,6 +26,10 @@ override_doctype_class = { doc_events = { "Employee Onboarding": { "on_update": "jey_erp.custom_employee_onboarding.on_update" + }, + "Sales Invoice": { + "validate": "jey_erp.jey_erp.custom.sales_invoice.calculate_tax_free_amounts", + "on_update": "jey_erp.jey_erp.custom.sales_invoice.calculate_tax_free_amounts", } } diff --git a/jey_erp/public/js/sales_invoice.js b/jey_erp/public/js/sales_invoice.js new file mode 100644 index 0000000..1d1c0db --- /dev/null +++ b/jey_erp/public/js/sales_invoice.js @@ -0,0 +1,194 @@ +// Sales Invoice customizations for Tax Free Amount calculation + +frappe.ui.form.on('Sales Invoice', { + onload: function(frm) { + // Add custom calculate_tax_free_amounts method to frm + frm.calculate_tax_free_amounts = function() { + calculate_tax_free_amounts(frm); + }; + }, + + refresh: function(frm) { + // Calculate tax free amounts on refresh + if (frm.doc.docstatus === 0) { + frm.calculate_tax_free_amounts(); + } + } +}); + +frappe.ui.form.on('Sales Invoice Item', { + item_code: function(frm, cdt, cdn) { + // Try immediate calculation, then with delay as fallback + setTimeout(function() { + frm.calculate_tax_free_amounts(); + }, 100); + + setTimeout(function() { + frm.calculate_tax_free_amounts(); + }, 1000); + }, + + item_tax_template: function(frm, cdt, cdn) { + frm.calculate_tax_free_amounts(); + }, + + item_tax_rate: function(frm, cdt, cdn) { + frm.calculate_tax_free_amounts(); + }, + + qty: function(frm, cdt, cdn) { + setTimeout(function() { + frm.calculate_tax_free_amounts(); + }, 100); + }, + + rate: function(frm, cdt, cdn) { + setTimeout(function() { + frm.calculate_tax_free_amounts(); + }, 100); + }, + + net_amount: function(frm, cdt, cdn) { + frm.calculate_tax_free_amounts(); + }, + + items_remove: function(frm, cdt, cdn) { + frm.calculate_tax_free_amounts(); + } +}); + +function calculate_tax_free_amounts(frm) { + if (frm.doc.docstatus !== 0) { + return; + } + + // Clear existing tax_free_amount values + $.each(frm.doc.taxes || [], function(i, tax) { + tax.tax_free_amount = 0.0; + }); + + // Direct calculation without waiting + calculate_tax_free_amounts_direct(frm); +} + +function calculate_tax_free_amounts_direct(frm) { + // Calculate tax free amounts for each item + $.each(frm.doc.items || [], function(i, item) { + if (!item.item_code) { + return; + } + + var tax_template = item.item_tax_template; + var tax_rate = item.item_tax_rate; + + if (!tax_template && !tax_rate) { + return; + } + + // Process item_tax_rate (JSON format) - this is the most common case + if (tax_rate) { + try { + var tax_rates = typeof tax_rate === 'string' ? JSON.parse(tax_rate) : tax_rate; + + // Check each tax account in the item_tax_rate + $.each(tax_rates, function(account_head, rate) { + if (flt(rate) === 0.0) { + // Find corresponding tax in sales invoice + $.each(frm.doc.taxes || [], function(k, doc_tax) { + if (doc_tax.account_head === account_head) { + // Add item amount to tax_free_amount + doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount); + return false; // break + } + }); + } + }); + + } catch (e) { + console.error("Error parsing item_tax_rate:", e); + } + } + + // Process item_tax_template if no item_tax_rate + else if (tax_template) { + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Item Tax Template', + name: tax_template + }, + async: false, + callback: function(r) { + if (r.message) { + var tax_template_doc = r.message; + + // Check each tax in template + $.each(tax_template_doc.taxes || [], function(j, template_tax) { + if (flt(template_tax.tax_rate) === 0.0) { + // Find corresponding tax in sales invoice + $.each(frm.doc.taxes || [], function(k, doc_tax) { + if (doc_tax.account_head === template_tax.tax_type) { + // Add item amount to tax_free_amount + doc_tax.tax_free_amount = flt(doc_tax.tax_free_amount) + flt(item.net_amount); + return false; // break + } + }); + } + }); + } + } + }); + } + }); + + // Refresh taxes grid + frm.refresh_field('taxes'); +} + +// Override methods in the taxes calculation chain for immediate updates +let original_calculate_taxes_and_totals = erpnext.taxes_and_totals.prototype.calculate_taxes_and_totals; +let original_calculate_taxes = erpnext.taxes_and_totals.prototype.calculate_taxes; + +erpnext.taxes_and_totals.prototype.calculate_taxes_and_totals = function(update_paid_amount) { + // Call original method + let result = original_calculate_taxes_and_totals.call(this, update_paid_amount); + + // Calculate tax free amounts immediately after tax calculation + if (this.frm.doc.doctype === 'Sales Invoice') { + calculate_tax_free_amounts(this.frm); + } + + return result; +}; + +// Also override calculate_taxes to catch earlier in the process +erpnext.taxes_and_totals.prototype.calculate_taxes = function() { + // Call original method + let result = original_calculate_taxes.call(this); + + // Calculate tax free amounts immediately after taxes calculation + if (this.frm.doc.doctype === 'Sales Invoice') { + calculate_tax_free_amounts(this.frm); + } + + return result; +}; + +// Override the item details loading to trigger calculation +if (erpnext.selling && erpnext.selling.SellingController) { + let original_item_code = erpnext.selling.SellingController.prototype.item_code; + + erpnext.selling.SellingController.prototype.item_code = function(doc, cdt, cdn) { + // Call original method + let result = original_item_code.call(this, doc, cdt, cdn); + + // Calculate tax free amounts after item details are loaded + if (this.frm.doc.doctype === 'Sales Invoice') { + setTimeout(() => { + calculate_tax_free_amounts(this.frm); + }, 200); + } + + return result; + }; +} \ No newline at end of file