From a9d48e5fbf72e951c2f7430fc64f3e2a5c98f5c9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 13 Mar 2021 12:48:14 +0530 Subject: [PATCH 01/44] fix: revert stock balance value calculation --- erpnext/stock/report/stock_balance/stock_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 145562cdc1b..8ea98cba6f6 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -196,7 +196,7 @@ def get_item_warehouse_map(filters, sle): else: qty_diff = flt(d.actual_qty) - value_diff = flt(d.stock_value) - flt(qty_dict.bal_val) + value_diff = flt(d.stock_value_difference) if d.posting_date < from_date: qty_dict.opening_qty += qty_diff From 4db8d1390049c1456a016735ca8561198b6be703 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 22 Feb 2021 21:27:14 +0530 Subject: [PATCH 02/44] fix: item attributes not editable until refresh --- erpnext/stock/doctype/item/item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 34476273c28..b41aea8b880 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -85,7 +85,7 @@ frappe.ui.form.on("Item", { } if (frm.doc.variant_of) { frm.set_intro(__('This Item is a Variant of {0} (Template).', - [`${frm.doc.variant_of}`]), true); + [`${frm.doc.variant_of}`]), true); } if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) { From ef09599a9fef9ea1c0c9a6dd1a4e6c9319e3c734 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 17 Mar 2021 15:47:03 +0530 Subject: [PATCH 03/44] fix(India): Incorrect Nil Exempt and Non GST amount in GSTR3B report --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 9e841483554..d3b03b9faf5 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -354,7 +354,7 @@ class GSTR3BReport(Document): inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i where p.docstatus = 1 and p.name = i.parent - and i.is_nil_exempt = 1 or i.is_non_gst = 1 and + and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) From 4c6d068a739f45e1bdb55bd9940b5edf058c7032 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 17 Mar 2021 18:00:08 +0530 Subject: [PATCH 04/44] fix: Group nil exempted and non gst items separately --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index d3b03b9faf5..526f6b42286 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -350,14 +350,15 @@ class GSTR3BReport(Document): return inter_state_supply_details def get_inward_nil_exempt(self, state): - + print("@@@@@@@@@@@@") inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i where p.docstatus = 1 and p.name = i.parent and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s - group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + print(inward_nil_exempt, "$#$#$#$") inward_nil_exempt_details = { "gst": { "intra": 0.0, From 6da7f658482e77a2f98d0e26cb8db7d2eb335da5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 17 Mar 2021 18:03:17 +0530 Subject: [PATCH 05/44] fix: Remove print statement --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 526f6b42286..f1c5210b237 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -350,7 +350,6 @@ class GSTR3BReport(Document): return inter_state_supply_details def get_inward_nil_exempt(self, state): - print("@@@@@@@@@@@@") inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i where p.docstatus = 1 and p.name = i.parent @@ -358,7 +357,6 @@ class GSTR3BReport(Document): month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) - print(inward_nil_exempt, "$#$#$#$") inward_nil_exempt_details = { "gst": { "intra": 0.0, From b2ae422ed7029f310b54bc7575c398535621fd88 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 7 Aug 2020 14:34:28 +0530 Subject: [PATCH 06/44] feat: add image alt field in items --- erpnext/stock/doctype/item/item.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 11be6588957..d6635201707 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -122,6 +122,7 @@ "weightage", "slideshow", "website_image", + "website_image_alt", "thumbnail", "cb72", "website_warehouse", @@ -1053,14 +1054,21 @@ "fieldtype": "Data", "label": "Default Manufacturer Part No", "read_only": 1 + }, + { + "fieldname": "website_image_alt", + "fieldtype": "Data", + "label": "Image Alt Text" } ], "has_web_view": 1, "icon": "fa fa-tag", "idx": 2, "image_field": "image", + "index_web_pages_for_search": 1, + "links": [], "max_attachments": 1, - "modified": "2020-08-06 17:03:26.594319", + "modified": "2021-03-18 11:24:58.384992", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1122,4 +1130,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} From 063ab520d3b19fba98d55fdff55ca2acdb5382d5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 7 Aug 2020 14:34:54 +0530 Subject: [PATCH 07/44] feat: add image alt argument for product_image macros --- erpnext/templates/includes/macros.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 3c82e90cc0d..ea6b00fc58a 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -7,9 +7,9 @@ {% endmacro %} -{% macro product_image(website_image, css_class="") %} +{% macro product_image(website_image, css_class="", alt="") %}
- + {{ alt }}
{% endmacro %} From f29cdb39a12ad67cb0c8fa03b4ac7ddb7ea9c643 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 7 Aug 2020 14:38:40 +0530 Subject: [PATCH 08/44] feat: add alt value in product page --- erpnext/stock/doctype/item/item.py | 2 +- erpnext/templates/generators/item/item_image.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 6f454d5cb8b..f231aac086b 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -344,7 +344,7 @@ class Item(WebsiteGenerator): if variant: context.variant = frappe.get_doc("Item", variant) - for fieldname in ("website_image", "web_long_description", "description", + for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", "website_specifications"): if context.variant.get(fieldname): value = context.variant.get(fieldname) diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html index 0dd4c3505e1..5d46a45053d 100644 --- a/erpnext/templates/generators/item/item_image.html +++ b/erpnext/templates/generators/item/item_image.html @@ -23,7 +23,7 @@ }) {% else %} -{{ product_image(website_image or image or 'no-image.jpg') }} +{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }} {% endif %} From 437c771701133dacd5b55c440ce8c66fce2680f3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 10 Aug 2020 12:45:07 +0530 Subject: [PATCH 09/44] refactor: update confusing label --- erpnext/stock/doctype/item/item.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index d6635201707..2c9d895a3b0 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1058,7 +1058,7 @@ { "fieldname": "website_image_alt", "fieldtype": "Data", - "label": "Image Alt Text" + "label": "Image Description" } ], "has_web_view": 1, From bb3c44dbb3596d86720430ac777500bdd0d31fb9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 22 Mar 2021 19:19:55 +0530 Subject: [PATCH 10/44] fix: TDS check getting checked after reload --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3cf4d5994a5..8c0d4efb954 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -509,7 +509,7 @@ frappe.ui.form.on("Purchase Invoice", { }, onload: function(frm) { - if(frm.doc.__onload) { + if(frm.doc.__onload && frm.is_new()) { if(frm.doc.supplier) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } From bf91ee9b99de9226df1c2515c74cf9c7ae6f19b5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 15 Mar 2021 19:10:54 +0530 Subject: [PATCH 11/44] fix: Allow zero valuation in stock reconciliation Stock reconciliation can not be done for customer provided item as they have zero valuation. This change adds a checkbox in item table to allow such items. Related issue: ISS-20-21-10248 --- .../stock_reconciliation/stock_reconciliation.py | 5 +++-- .../stock_reconciliation_item.json | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 1b81b97392c..09b5fa68e8a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -31,6 +31,7 @@ class StockReconciliation(StockController): self.validate_data() self.validate_expense_account() self.set_total_qty_and_amount() + self.validate_customer_provided_item() if self._action=="submit": self.make_batches('warehouse') @@ -213,7 +214,7 @@ class StockReconciliation(StockController): if row.valuation_rate in ("", None): row.valuation_rate = previous_sle.get("valuation_rate", 0) - if row.qty and not row.valuation_rate: + if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate: frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) if (not item.has_batch_no and (previous_sle and row.qty == previous_sle.get("qty_after_transaction") @@ -548,4 +549,4 @@ def get_difference_account(purpose, company): account = frappe.db.get_value('Account', {'is_group': 0, 'company': company, 'account_type': 'Temporary'}, 'name') - return account \ No newline at end of file + return account diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index e53db0772b4..85c7ebe2634 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -13,6 +13,7 @@ "qty", "valuation_rate", "amount", + "allow_zero_valuation_rate", "serial_no_and_batch_section", "serial_no", "column_break_11", @@ -166,10 +167,19 @@ "fieldtype": "Link", "label": "Batch No", "options": "Batch" + }, + { + "default": "0", + "fieldname": "allow_zero_valuation_rate", + "fieldtype": "Check", + "label": "Allow Zero Valuation Rate", + "print_hide": 1, + "read_only": 1 } ], "istable": 1, - "modified": "2019-06-14 17:10:53.188305", + "links": [], + "modified": "2021-03-23 11:09:44.407157", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -179,4 +189,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From ebd230e7485976553fb98ea446bd8256c8665efe Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 23 Mar 2021 12:41:19 +0530 Subject: [PATCH 12/44] test: customer item in stock reconciliation --- .../stock_reconciliation/test_stock_reconciliation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 7c55fd6da1c..1e6ae1e5d4d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -420,6 +420,16 @@ class TestStockReconciliation(unittest.TestCase): for doc in [sr, ste2, ste1]: doc.cancel() + def test_customer_provided_items(self): + item_code = 'Stock-Reco-customer-Item-100' + create_item(item_code, is_customer_provided_item = 1, + customer = '_Test Customer', is_purchase_item = 0) + + sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420) + + self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1) + self.assertEqual(sr.get("items")[0].valuation_rate, 0) + self.assertEqual(sr.get("items")[0].amount, 0) def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 19193e8f1c42443ca79b5f1d8e399ea3da4acf44 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 23 Mar 2021 12:16:58 +0530 Subject: [PATCH 13/44] fix: set valuation rate for customer items to zero - In stock reconciliation always set valuation rate of customer provided items to zero during validation. - Let user know the valuation has been changed. --- .../stock_reconciliation.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 09b5fa68e8a..2792c4bfd46 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -30,8 +30,9 @@ class StockReconciliation(StockController): self.remove_items_with_no_change() self.validate_data() self.validate_expense_account() - self.set_total_qty_and_amount() self.validate_customer_provided_item() + self.set_zero_value_for_customer_provided_items() + self.set_total_qty_and_amount() if self._action=="submit": self.make_batches('warehouse') @@ -448,6 +449,20 @@ class StockReconciliation(StockController): if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError) + def set_zero_value_for_customer_provided_items(self): + changed_any_values = False + + for d in self.get('items'): + is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item') + if is_customer_item and d.valuation_rate: + d.valuation_rate = 0.0 + changed_any_values = True + + if changed_any_values: + msgprint(_("Valuation rate for customer provided items has been set to zero."), + title=_("Note"), indicator="blue") + + def set_total_qty_and_amount(self): for d in self.get("items"): d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate")) From 5bd58ef85cd545501845cf43b5b8e4d4131ef8da Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Thu, 25 Mar 2021 11:46:20 +0530 Subject: [PATCH 14/44] fix: opportunity-quotation mapping order status (#25002) --- erpnext/crm/doctype/opportunity/opportunity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 8302978e1c1..6ed3a239e98 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -244,7 +244,6 @@ def make_quotation(source_name, target_doc=None): "doctype": "Quotation", "field_map": { "opportunity_from": "quotation_to", - "opportunity_type": "order_type", "name": "enq_no", } }, From a52ea48a21c1026257dfd022ed46c8a4d937f1af Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 25 Mar 2021 15:23:19 +0530 Subject: [PATCH 15/44] fix: serial no trim issue (#24950) * fix: serial no trim issue * fix: valid serial nos --- erpnext/public/js/controllers/transaction.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 665517fd4c7..ea16501b72f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -635,21 +635,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } else { var valid_serial_nos = []; - + var serialnos = []; // Replacing all occurences of comma with carriage return - var serial_nos = item.serial_no.trim().replace(/,/g, '\n'); - - serial_nos = serial_nos.trim().split('\n'); - - // Trim each string and push unique string to new list - for (var x=0; x<=serial_nos.length - 1; x++) { - if (serial_nos[x].trim() != "" && valid_serial_nos.indexOf(serial_nos[x].trim()) == -1) { - valid_serial_nos.push(serial_nos[x].trim()); + item.serial_no = item.serial_no.replace(/,/g, '\n'); + serialnos = item.serial_no.split("\n"); + for (var i = 0; i < serialnos.length; i++) { + if (serialnos[i] != "") { + valid_serial_nos.push(serialnos[i]); } } - - // Add the new list to the serial no. field in grid with each in new line - item.serial_no = valid_serial_nos.join('\n'); item.conversion_factor = item.conversion_factor || 1; refresh_field("serial_no", item.name, item.parentfield); From bd742d7ad9a710ca3676fd8891c80fe632723d0c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 29 Mar 2021 21:21:19 +0530 Subject: [PATCH 16/44] fix(Italy): setup, validations, optimisations --- erpnext/regional/italy/sales_invoice.js | 13 +--- erpnext/regional/italy/setup.py | 7 +- erpnext/regional/italy/utils.py | 98 ++++++++++++++----------- 3 files changed, 62 insertions(+), 56 deletions(-) diff --git a/erpnext/regional/italy/sales_invoice.js b/erpnext/regional/italy/sales_invoice.js index 586a52937b5..b54ac538126 100644 --- a/erpnext/regional/italy/sales_invoice.js +++ b/erpnext/regional/italy/sales_invoice.js @@ -11,15 +11,10 @@ erpnext.setup_e_invoice_button = (doctype) => { callback: function(r) { frm.reload_doc(); if(r.message) { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.italy.utils.download_e_invoice_file?" - + "file_name=" + r.message - ) - ) - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + open_url_post(frappe.request.url, { + cmd: 'frappe.core.doctype.file.file.download_file', + file_url: r.message + }); } } }); diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 6ab73413df2..309aedfb0d6 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -128,11 +128,8 @@ def make_custom_fields(update=True): fetch_from="company.vat_collectability"), dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing', fieldtype='Section Break', insert_after='pos_total_qty', print_hide=1), - dict(fieldname='company_tax_id', label='Company Tax ID', - fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, - fetch_from="company.tax_id"), dict(fieldname='company_fiscal_code', label='Company Fiscal Code', - fieldtype='Data', insert_after='company_tax_id', print_hide=1, read_only=1, + fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, fetch_from="company.fiscal_code"), dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime', fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1, @@ -219,4 +216,4 @@ def add_permissions(): update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1) add_permission(doctype, 'Accounts Manager', 1) update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) \ No newline at end of file + update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index 2af72f8e276..9d10fe974d3 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals -import frappe, json, os +import io +import json +import frappe from frappe.utils import flt, cstr from erpnext.controllers.taxes_and_totals import get_itemised_tax from frappe import _ @@ -28,20 +30,22 @@ def update_itemised_tax_data(doc): @frappe.whitelist() def export_invoices(filters=None): - saved_xmls = [] + frappe.has_permission('Sales Invoice', throw=True) - invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"]) + invoices = frappe.get_all( + "Sales Invoice", + filters=get_conditions(filters), + fields=["name", "company_tax_id"] + ) - for invoice in invoices: - attachments = get_e_invoice_attachments(invoice) - saved_xmls += [attachment.file_name for attachment in attachments] + attachments = get_e_invoice_attachments(invoices) - zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) + zip_filename = "{0}-einvoices.zip".format( + frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) - download_zip(saved_xmls, zip_filename) + download_zip(attachments, zip_filename) -@frappe.whitelist() def prepare_invoice(invoice, progressive_number): #set company information company = frappe.get_doc("Company", invoice.company) @@ -98,7 +102,7 @@ def prepare_invoice(invoice, progressive_number): def get_conditions(filters): filters = json.loads(filters) - conditions = {"docstatus": 1} + conditions = {"docstatus": 1, "company_tax_id": ("!=", "")} if filters.get("company"): conditions["company"] = filters["company"] if filters.get("customer"): conditions["customer"] = filters["customer"] @@ -111,23 +115,22 @@ def get_conditions(filters): return conditions -#TODO: Use function from frappe once PR #6853 is merged. + def download_zip(files, output_filename): - from zipfile import ZipFile + import zipfile - input_files = [frappe.get_site_path('private', 'files', filename) for filename in files] - output_path = frappe.get_site_path('private', 'files', output_filename) + zip_stream = io.BytesIO() + with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for file in files: + file_path = frappe.utils.get_files_path( + file.file_name, is_private=file.is_private) - with ZipFile(output_path, 'w') as output_zip: - for input_file in input_files: - output_zip.write(input_file, arcname=os.path.basename(input_file)) - - with open(output_path, 'rb') as fileobj: - filedata = fileobj.read() + zip_file.write(file_path, arcname=file.file_name) frappe.local.response.filename = output_filename - frappe.local.response.filecontent = filedata + frappe.local.response.filecontent = zip_stream.getvalue() frappe.local.response.type = "download" + zip_stream.close() def get_invoice_summary(items, taxes): summary_data = frappe._dict() @@ -307,23 +310,12 @@ def prepare_and_attach_invoice(doc, replace=False): @frappe.whitelist() def generate_single_invoice(docname): doc = frappe.get_doc("Sales Invoice", docname) - + frappe.has_permission("Sales Invoice", doc=doc, throw=True) e_invoice = prepare_and_attach_invoice(doc, True) + return e_invoice.file_url - return e_invoice.file_name - -@frappe.whitelist() -def download_e_invoice_file(file_name): - content = None - with open(frappe.get_site_path('private', 'files', file_name), "r") as f: - content = f.read() - - frappe.local.response.filename = file_name - frappe.local.response.filecontent = content - frappe.local.response.type = "download" - -#Delete e-invoice attachment on cancel. +# Delete e-invoice attachment on cancel. def sales_invoice_on_cancel(doc, method): if get_company_country(doc.company) not in ['Italy', 'Italia', 'Italian Republic', 'Repubblica Italiana']: @@ -335,16 +327,38 @@ def sales_invoice_on_cancel(doc, method): def get_company_country(company): return frappe.get_cached_value('Company', company, 'country') -def get_e_invoice_attachments(invoice): - if not invoice.company_tax_id: - return [] +def get_e_invoice_attachments(invoices): + if not isinstance(invoices, list): + if not invoices.company_tax_id: + return + + invoices = [invoices] + + tax_id_map = { + invoice.name: ( + invoice.company_tax_id + if invoice.company_tax_id.startswith("IT") + else "IT" + invoice.company_tax_id + ) for invoice in invoices + } + + attachments = frappe.get_all( + "File", + fields=("name", "file_name", "attached_to_name", "is_private"), + filters= { + "attached_to_name": ('in', tax_id_map), + "attached_to_doctype": 'Sales Invoice' + } + ) out = [] - attachments = get_attachments(invoice.doctype, invoice.name) - company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id - for attachment in attachments: - if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"): + if ( + attachment.file_name + and attachment.file_name.endswith(".xml") + and attachment.file_name.startswith( + tax_id_map.get(attachment.attached_to_name)) + ): out.append(attachment) return out From b904304268663706a36fc7c38b19b66acc86d054 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 29 Mar 2021 21:26:53 +0530 Subject: [PATCH 17/44] fix: revert removed field; not standard in v12 --- erpnext/regional/italy/setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 309aedfb0d6..6bcde1c6864 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -128,8 +128,11 @@ def make_custom_fields(update=True): fetch_from="company.vat_collectability"), dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing', fieldtype='Section Break', insert_after='pos_total_qty', print_hide=1), - dict(fieldname='company_fiscal_code', label='Company Fiscal Code', + dict(fieldname='company_tax_id', label='Company Tax ID', fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, + fetch_from="company.tax_id"), + dict(fieldname='company_fiscal_code', label='Company Fiscal Code', + fieldtype='Data', insert_after='company_tax_id', print_hide=1, read_only=1, fetch_from="company.fiscal_code"), dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime', fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1, From 53a32fecdbe9af8697b621b2b83684e6858127ab Mon Sep 17 00:00:00 2001 From: Rohan Date: Wed, 31 Mar 2021 15:23:04 +0530 Subject: [PATCH 18/44] fix: commit individual SLE rename for large datasets (#25085) --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 4702b8a434f..b4599ba0f46 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -294,4 +294,8 @@ def rename_temporarily_named_docs(doctype): oldname = doc.name set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc) newname = doc.name - frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname)) + frappe.db.sql( + "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype), + (newname, oldname), + auto_commit=True + ) From 8d31d21b88ae08d8207588f4e04ab815ca971589 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 30 Mar 2021 13:06:17 +0530 Subject: [PATCH 19/44] fix: do not fetch stopped MR in production plan - Ignore stopped MR while using fetch button - Add filter on field inside child table. Related issue: ISS-20-21-10757 --- .../doctype/production_plan/production_plan.js | 10 ++++++++++ .../doctype/production_plan/production_plan.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index cb8d3a02068..be4babe417a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -24,6 +24,16 @@ frappe.ui.form.on('Production Plan', { } }); + frm.set_query('material_request', 'material_requests', function() { + return { + filters: { + material_request_type: "Manufacture", + docstatus: 1, + status: ["!=", "Stopped"], + } + }; + }); + frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) { return { query: "erpnext.controllers.queries.item_query", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 5a193d7e4bf..1b2cd542a4f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -68,7 +68,7 @@ class ProductionPlan(Document): from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr_item.parent = mr.name and mr.material_request_type = "Manufacture" - and mr.docstatus = 1 and mr.company = %(company)s + and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code and bom.is_active = 1)) From 2479e070c45b6f036fba91d91fb39e47a4248a06 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 1 Apr 2021 19:27:41 +0530 Subject: [PATCH 20/44] fix: validation msg for TransDocNo e-invoicing (#25120) --- erpnext/regional/india/e_invoice/einv_validation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json index 86290cfe524..f4a3542a60e 100644 --- a/erpnext/regional/india/e_invoice/einv_validation.json +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -919,7 +919,8 @@ "minLength": 1, "maxLength": 15, "pattern": "^([0-9A-Z/-]){1,15}$", - "description": "Tranport Document Number" + "description": "Tranport Document Number", + "validationMsg": "Transport Receipt No is invalid" }, "TransDocDt": { "type": "string", From 883598890d6bb7d2a639e698fcf3ee306fcf992d Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 2 Apr 2021 12:16:09 +0530 Subject: [PATCH 21/44] fix: place of supply of e-invoicing (#25149) --- erpnext/regional/india/e_invoice/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 305f411cae1..7de84f7729a 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -328,8 +328,11 @@ def make_einvoice(invoice): buyer_details = get_overseas_address_details(invoice.customer_address) else: buyer_details = get_party_details(invoice.customer_address) - place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin - place_of_supply = place_of_supply[:2] + place_of_supply = get_place_of_supply(invoice, invoice.doctype) + if place_of_supply: + place_of_supply = place_of_supply.split('-')[0] + else: + place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) From 15af5904826021229c4d688030fedf17a8afa689 Mon Sep 17 00:00:00 2001 From: bhavesh95863 <34086262+bhavesh95863@users.noreply.github.com> Date: Fri, 2 Apr 2021 15:05:07 +0530 Subject: [PATCH 22/44] fix(regional): remove shipping address GSTIN validation for e-invoice (#25133) Co-authored-by: Saqib --- erpnext/regional/india/e_invoice/utils.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 7de84f7729a..e6c63d68255 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -86,10 +86,10 @@ def get_doc_details(invoice): invoice_date=invoice_date )) -def get_party_details(address_name): +def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None): d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - if (not d.gstin + if ((not d.gstin and not shipping_address) or not d.city or not d.pincode or not d.address_title @@ -107,13 +107,16 @@ def get_party_details(address_name): # according to einvoice standard pincode = 999999 - return frappe._dict(dict( - gstin=d.gstin, legal_name=d.address_title, + party_address_details = frappe._dict(dict( + legal_name=d.address_title, location=d.city, pincode=d.pincode, state_code=d.gst_state_number, address_line1=d.address_line1, address_line2=d.address_line2 )) + if d.gstin: + party_address_details.gstin = d.gstin + return party_address_details def get_gstin_details(gstin): if not hasattr(frappe.local, 'gstin_cache'): @@ -322,12 +325,12 @@ def make_einvoice(invoice): item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) invoice_value_details = get_invoice_value_details(invoice) - seller_details = get_party_details(invoice.company_address) + seller_details = get_party_details(invoice.company_address, company_address=1) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) else: - buyer_details = get_party_details(invoice.customer_address) + buyer_details = get_party_details(invoice.customer_address, billing_address=1) place_of_supply = get_place_of_supply(invoice, invoice.doctype) if place_of_supply: place_of_supply = place_of_supply.split('-')[0] @@ -340,7 +343,7 @@ def make_einvoice(invoice): if invoice.gst_category == 'Overseas': shipping_details = get_overseas_address_details(invoice.shipping_address_name) else: - shipping_details = get_party_details(invoice.shipping_address_name) + shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) @@ -833,4 +836,4 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): gsp_connector = GSPConnector(doctype, docname) - gsp_connector.cancel_eway_bill(eway_bill, reason, remark) \ No newline at end of file + gsp_connector.cancel_eway_bill(eway_bill, reason, remark) From f8dcd06dad42e43b8ac0ba7d4b690e1f593feed1 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 2 Apr 2021 16:47:50 +0530 Subject: [PATCH 23/44] fix: santize_for_json is not defined (#25157) --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e6c63d68255..61b755660cf 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -335,7 +335,7 @@ def make_einvoice(invoice): if place_of_supply: place_of_supply = place_of_supply.split('-')[0] else: - place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] + place_of_supply = invoice.billing_address_gstin[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) From 3707b004c41eda1fb5b6fb7e8f988606595d5c24 Mon Sep 17 00:00:00 2001 From: Kaviya Periyasamy Date: Sat, 3 Apr 2021 12:18:19 +0530 Subject: [PATCH 24/44] fix: object referencing same memory address issue --- erpnext/regional/india/e_invoice/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 61b755660cf..dd130b69fff 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -386,7 +386,9 @@ def throw_error_list(errors, title): else: frappe.throw(errors[0], title=title) -def validate_einvoice(validations, einvoice, errors=[]): +def validate_einvoice(validations, einvoice, errors=None): + if errors is None: + errors = [] for fieldname, field_validation in validations.items(): value = einvoice.get(fieldname, None) if not value or value == "None": From 2e04026fd6415ee6fba32aaf9993a5b0b42a0cf0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 3 Apr 2021 14:20:30 +0530 Subject: [PATCH 25/44] fix: incorrect batch picked in subcontracted purchase receipt --- erpnext/controllers/buying_controller.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 59c371200c8..ac50fe9f12e 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -6,6 +6,7 @@ import frappe from frappe import _, msgprint from frappe.utils import flt,cint, cstr, getdate from six import iteritems +from collections import OrderedDict from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate @@ -325,10 +326,12 @@ class BuyingController(StockController): batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) + for batch_data in batches_qty: qty = batch_data['qty'] raw_material.batch_no = batch_data['batch'] - self.append_raw_material_to_be_backflushed(item, raw_material, qty) + if qty > 0: + self.append_raw_material_to_be_backflushed(item, raw_material, qty) else: self.append_raw_material_to_be_backflushed(item, raw_material, qty) @@ -1009,7 +1012,7 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): for batch_data in transferred_batches: key = ((batch_data.item_code, fg_item) if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) - transferred_batch_qty_map.setdefault(key, {}) + transferred_batch_qty_map.setdefault(key, OrderedDict()) transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty return transferred_batch_qty_map @@ -1062,8 +1065,14 @@ def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty if available_qty >= required_qty: available_batches.append({'batch': batch, 'qty': required_qty}) break - else: + elif available_qty != 0: available_batches.append({'batch': batch, 'qty': available_qty}) required_qty -= available_qty + for row in available_batches: + if backflushed_batches.get(row.get('batch'), 0) > 0: + backflushed_batches[row.get('batch')] += row.get('qty') + else: + backflushed_batches[row.get('batch')] = row.get('qty') + return available_batches From 6678450ba00e53f4a8a0816a3c0a900aba549563 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 1 Apr 2021 16:52:28 +0530 Subject: [PATCH 26/44] fix(India): create property setters for shorter naming series (#25134) --- .../doctype/purchase_invoice/test_records.json | 4 ++-- .../doctype/sales_invoice/test_records.json | 8 ++++---- .../doctype/sales_invoice/test_sales_invoice.py | 1 + erpnext/regional/india/setup.py | 13 ++++++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json index 171927c1822..2a8c97b0ecf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_records.json +++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json @@ -43,7 +43,7 @@ } ], "grand_total": 0, - "naming_series": "_T-BILL", + "naming_series": "T-PINV-", "taxes": [ { "account_head": "_Test Account Shipping Charges - _TC", @@ -168,7 +168,7 @@ } ], "grand_total": 0, - "naming_series": "_T-Purchase Invoice-", + "naming_series": "T-PINV-", "taxes": [ { "account_head": "_Test Account Shipping Charges - _TC", diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index 11ebe6a573a..5fad6e263e3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_records.json +++ b/erpnext/accounts/doctype/sales_invoice/test_records.json @@ -30,7 +30,7 @@ "base_grand_total": 561.8, "grand_total": 561.8, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "base_net_total": 500.0, "taxes": [ { @@ -103,7 +103,7 @@ "base_grand_total": 630.0, "grand_total": 630.0, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "base_net_total": 500.0, "taxes": [ { @@ -174,7 +174,7 @@ ], "grand_total": 0, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "taxes": [ { "account_head": "_Test Account Shipping Charges - _TC", @@ -300,7 +300,7 @@ ], "grand_total": 0, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "taxes": [ { "account_head": "_Test Account Excise Duty - _TC", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 4db739e09df..d3e85ead446 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2063,6 +2063,7 @@ def create_sales_invoice(**args): si.return_against = args.return_against si.currency=args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 + si.naming_series = args.naming_series or "T-SINV-" si.append("items", { "item_code": args.item or args.item_code or "_Test Item", diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 4954b3438dc..62c15dcd715 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.permissions import add_permission, update_permission_property from erpnext.regional.india import states from erpnext.accounts.utils import get_fiscal_year, FiscalYearError @@ -19,6 +20,7 @@ def setup(company=None, patch=True): # TODO: for all countries def setup_company_independent_fixtures(): make_custom_fields() + make_property_setters() add_permissions() add_custom_roles_for_reports() frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) @@ -98,6 +100,11 @@ def add_print_formats(): frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) +def make_property_setters(): + # GST rules do not allow for an invoice no. bigger than 16 characters + make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') + make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') + def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', @@ -397,9 +404,9 @@ def make_custom_fields(update=True): si_einvoice_fields = [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), - + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, @@ -616,7 +623,7 @@ def set_tax_withholding_category(company): fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] if not fy_exist: doc.append("rates", d.get('rates')[0]) - + doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True doc.save() From 34b6f9389c006c9da24b1833148959216831aad7 Mon Sep 17 00:00:00 2001 From: Abdullah Abouzekry <1107178+abdouz@users.noreply.github.com> Date: Tue, 6 Apr 2021 06:44:51 +0200 Subject: [PATCH 27/44] fix: sales order not saving due type mismatch in promo scheme (#24748) (#25056) Co-authored-by: Ankush Menat --- erpnext/accounts/doctype/pricing_rule/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index fb2f9ff8991..c9a820e2e27 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -467,7 +467,7 @@ def apply_pricing_rule_on_transaction(doc): if not d.get(pr_field): continue - if d.validate_applied_rule and doc.get(field) < d.get(pr_field): + if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field): frappe.msgprint(_("User has not applied rule on the invoice {0}") .format(doc.name)) else: From ac58e05bbb51e5bdd1963728a03996bb6a5a8363 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 6 Apr 2021 11:40:21 +0530 Subject: [PATCH 28/44] fix: error message compensatory leave request --- .../compensatory_leave_request.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index 7a9727f18c4..aa5a67f40c7 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_days, getdate, cint +from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.model.document import Document from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ get_holidays_for_employee, create_additional_leave_ledger_entry @@ -40,7 +40,12 @@ class CompensatoryLeaveRequest(Document): def validate_holidays(self): holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date) if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: - frappe.throw(_("Compensatory leave request days not in valid holidays")) + if date_diff(self.work_end_date, self.work_from_date): + msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) + else: + msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date))) + + frappe.throw(msg) def on_submit(self): company = frappe.db.get_value("Employee", self.employee, "company") @@ -63,7 +68,7 @@ class CompensatoryLeaveRequest(Document): leave_allocation = self.create_leave_allocation(leave_period, date_difference) self.leave_allocation=leave_allocation.name else: - frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) + frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date))) def on_cancel(self): if self.leave_allocation: From 2541ab178a607e9be4a81774b96f1774fb884748 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Apr 2021 23:40:46 +0530 Subject: [PATCH 29/44] fix: RCM tax calculation --- erpnext/regional/india/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index a808048d17d..0f70f4abf9e 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -817,7 +817,11 @@ def get_gst_tax_amount(doc): continue if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: - base_gst_tax += tax.base_tax_amount_after_discount_amount - gst_tax += tax.tax_amount_after_discount_amount + if tax.add_deduct_tax == "Add": + base_gst_tax += tax.base_tax_amount_after_discount_amount + gst_tax += tax.tax_amount_after_discount_amount + else: + base_gst_tax -= tax.base_tax_amount_after_discount_amount + gst_tax -= tax.tax_amount_after_discount_amount return gst_tax, base_gst_tax From 83df26197b62ddc0aa716dddd6ffda1fd1c71729 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 11 Apr 2021 18:45:22 +0530 Subject: [PATCH 30/44] fix: Rounding adjustment for RCM --- erpnext/regional/india/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0f70f4abf9e..5d5c2e9610c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -748,16 +748,18 @@ def update_grand_total_for_rcm(doc, method): update_totals(gst_tax, base_gst_tax, doc) def update_totals(gst_tax, base_gst_tax, doc): - doc.base_grand_total -= base_gst_tax doc.grand_total -= gst_tax + doc.base_grand_total = (doc.grand_total * doc.conversion_rate) if doc.meta.get_field("rounded_total"): if not doc.is_rounded_total_disabled(): doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, doc.currency, doc.precision("rounded_total")) + doc.base_rounded_total += doc.rounded_total * doc.conversion_rate doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment")) + doc.base_rounding_adjustment = doc.rounding_adjustment * doc.conversion_rate calculate_outstanding_amount(doc) From 878b10eff54cb9ce97db4a6117cb2e9fdc8bbc90 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Mon, 12 Apr 2021 10:37:12 +0530 Subject: [PATCH 31/44] fix: don't delete mode of payment account details while deleting company transactions (#25218) --- erpnext/setup/doctype/company/delete_company_transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index d6155de80a9..9caff2b1633 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -27,7 +27,7 @@ def delete_company_transactions(company_name): if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", - "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", + "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Mode of Payment Account", "Item Default", "Customer", "Supplier", "GST Account"): delete_for_doctype(doctype, company_name) From a59523970babc470e3d25e858048317f39ef54e7 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 17 Mar 2021 20:06:35 +0530 Subject: [PATCH 32/44] chore: add sider config (#24892) Co-authored-by: Sagar Vora --- .flake8 | 32 ++++++++++++++++++++++++++++++++ sider.yml | 3 +++ 2 files changed, 35 insertions(+) create mode 100644 .flake8 create mode 100644 sider.yml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000000..399b176e1d0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,32 @@ +[flake8] +ignore = + E121, + E126, + E127, + E128, + E203, + E225, + E226, + E231, + E241, + E251, + E261, + E265, + E302, + E303, + E305, + E402, + E501, + E741, + W291, + W292, + W293, + W391, + W503, + W504, + F403, + B007, + B950, + W191, + +max-line-length = 200 \ No newline at end of file diff --git a/sider.yml b/sider.yml new file mode 100644 index 00000000000..2ca6e8deb15 --- /dev/null +++ b/sider.yml @@ -0,0 +1,3 @@ +linter: + flake8: + config: .flake8 \ No newline at end of file From d5d7ef7349e5be01ee22adfa5fbf4814ace4fd28 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Apr 2021 18:36:53 +0530 Subject: [PATCH 33/44] fix: Zero amount completed delivery notes being shown in Sales Invocie get items --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index afc63fc8f51..b664df82470 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -320,7 +320,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, and status not in ("Stopped", "Closed") %(fcond)s and ( (`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100) - or `tabDelivery Note`.grand_total = 0 + or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100) or ( `tabDelivery Note`.is_return = 1 and return_against in (select name from `tabDelivery Note` where per_billed < 100) From 2c63f833b69f593483bad3d8684774801c32fffd Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 13 Apr 2021 18:51:05 +0530 Subject: [PATCH 34/44] fix: serial no refresh issue (#25130) Co-authored-by: Nabin Hait --- erpnext/public/js/controllers/transaction.js | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ea16501b72f..77550d7b365 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -634,28 +634,34 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.frm.trigger("item_code", cdt, cdn); } else { - var valid_serial_nos = []; - var serialnos = []; // Replacing all occurences of comma with carriage return item.serial_no = item.serial_no.replace(/,/g, '\n'); - serialnos = item.serial_no.split("\n"); - for (var i = 0; i < serialnos.length; i++) { - if (serialnos[i] != "") { - valid_serial_nos.push(serialnos[i]); - } - } item.conversion_factor = item.conversion_factor || 1; - refresh_field("serial_no", item.name, item.parentfield); - if(!doc.is_return && cint(user_defaults.set_qty_in_transactions_based_on_serial_no_input)) { - frappe.model.set_value(item.doctype, item.name, - "qty", valid_serial_nos.length / item.conversion_factor); - frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); + if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) { + setTimeout(() => { + me.update_qty(cdt, cdn); + }, 10000); } } } }, + update_qty: function(cdt, cdn) { + var valid_serial_nos = []; + var serialnos = []; + var item = frappe.get_doc(cdt, cdn); + serialnos = item.serial_no.split("\n"); + for (var i = 0; i < serialnos.length; i++) { + if (serialnos[i] != "") { + valid_serial_nos.push(serialnos[i]); + } + } + frappe.model.set_value(item.doctype, item.name, + "qty", valid_serial_nos.length / item.conversion_factor); + frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); + }, + validate: function() { this.calculate_taxes_and_totals(false); }, From 583a8bf580b79e0e5db3853897861beca13f7fca Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 13 Apr 2021 18:51:46 +0530 Subject: [PATCH 35/44] fix: Added parent task expected end date validation (#24889) --- erpnext/projects/doctype/task/task.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 0d403b193f3..6238306ae25 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -30,6 +30,7 @@ class Task(NestedSet): def validate(self): self.validate_dates() + self.validate_parent_expected_end_date() self.validate_parent_project_dates() self.validate_progress() self.validate_status() @@ -44,6 +45,12 @@ class Task(NestedSet): frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ frappe.bold("Actual End Date"))) + def validate_parent_expected_end_date(self): + if self.parent_task: + parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date") + if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date): + frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date))) + def validate_parent_project_dates(self): if not self.project or frappe.flags.in_test: return From dd78ffb1764c677357d52fa90a4a6ac9deb67fa6 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 13 Apr 2021 18:52:37 +0530 Subject: [PATCH 36/44] fix: Assignment Rule Unassign Condition doesn't work (#24890) --- erpnext/selling/doctype/customer/customer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index a4a63d2aef2..a119aef3f98 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -114,7 +114,9 @@ class Customer(TransactionBase): '''If Customer created from Lead, update lead status to "Converted" update Customer link in Quotation, Opportunity''' if self.lead_name: - frappe.db.set_value('Lead', self.lead_name, 'status', 'Converted', update_modified=False) + lead = frappe.get_doc('Lead', self.lead_name) + lead.status = 'Converted' + lead.save() def create_lead_address_contact(self): if self.lead_name: From 16c3a76c49c55e096a9c319c65fd852174ad5719 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 13 Apr 2021 18:54:42 +0530 Subject: [PATCH 37/44] fix: Applly single transaction threshold on net_total instead of supplier credit amount (#25208) --- .../tax_withholding_category/tax_withholding_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index f6a7218d601..05ecbc9677b 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -163,7 +163,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date) supplier_credit_amount -= debit_note_amount - if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold) + if ((tax_details.get('threshold', 0) and net_total >= tax_details.threshold) or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, From e6eab3b5f8b28d99fe59e819a6c9de754a20f2e8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 13 Apr 2021 18:57:03 +0530 Subject: [PATCH 38/44] fix: incorrect status creating PR from PO after creating PI (#25203) * test: add tests for PO->PI, PO->PR workflow Add failing test cases for PR status and PR percent billed. * fix: update PR status in database * fix: do not copy percent billed from PO to PR * fix: patch for purchase receipt status * fix: remove logging in patch for v12 compatibility --- .../doctype/purchase_order/purchase_order.py | 1 - erpnext/patches.txt | 1 + .../patches/v12_0/purchase_receipt_status.py | 24 +++++++++ .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 50 +++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v12_0/purchase_receipt_status.py diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c7efb8a1a17..a2290170bd8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -366,7 +366,6 @@ def make_purchase_receipt(source_name, target_doc=None): "Purchase Order": { "doctype": "Purchase Receipt", "field_map": { - "per_billed": "per_billed", "supplier_warehouse":"supplier_warehouse" }, "validation": { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b40549c5ba3..6b6e2ecdf54 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -681,3 +681,4 @@ erpnext.patches.v12_0.update_payment_entry_status erpnext.patches.v12_0.add_transporter_address_field #2020-10-27 erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v12_0.add_state_code_for_ladakh +erpnext.patches.v12_0.purchase_receipt_status diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py new file mode 100644 index 00000000000..62d2a726bb0 --- /dev/null +++ b/erpnext/patches/v12_0/purchase_receipt_status.py @@ -0,0 +1,24 @@ +""" This patch fixes old purchase receipts (PR) where even after submitting + the PR, the `status` remains "Draft". `per_billed` field was copied over from previous + doc (PO), hence it is recalculated for setting new correct status of PR. +""" + +import frappe + +def execute(): + affected_purchase_receipts = frappe.db.sql( + """select name from `tabPurchase Receipt` + where status = 'Draft' and per_billed = 100 and docstatus = 1""" + ) + + if not affected_purchase_receipts: + return + + + for pr in affected_purchase_receipts: + pr_name = pr[0] + + pr_doc = frappe.get_doc("Purchase Receipt", pr_name) + + pr_doc.update_billing_status(update_modified=False) + pr_doc.set_status(update=True, update_modified=False) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 85b6d578bbd..538d0d61911 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -155,7 +155,7 @@ class PurchaseReceipt(BuyingController): if flt(self.per_billed) < 100: self.update_billing_status() else: - self.status = "Completed" + self.db_set("status", "Completed") # Updating stock ledger should always be called after updating prevdoc status, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 90bd4f5afd8..0d701650b0f 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -676,6 +676,56 @@ class TestPurchaseReceipt(unittest.TestCase): update_backflush_based_on("BOM") + def test_po_to_pi_and_po_to_pr_worflow_full(self): + """Test following behaviour: + - Create PO + - Create PI from PO and submit + - Create PR from PO and submit + """ + from erpnext.buying.doctype.purchase_order import test_purchase_order + from erpnext.buying.doctype.purchase_order import purchase_order + + po = test_purchase_order.create_purchase_order() + + pi = purchase_order.make_purchase_invoice(po.name) + pi.submit() + + pr = purchase_order.make_purchase_receipt(po.name) + pr.submit() + + pr.load_from_db() + + self.assertEqual(pr.status, "Completed") + self.assertEqual(pr.per_billed, 100) + + def test_po_to_pi_and_po_to_pr_worflow_partial(self): + """Test following behaviour: + - Create PO + - Create partial PI from PO and submit + - Create PR from PO and submit + """ + from erpnext.buying.doctype.purchase_order import test_purchase_order + from erpnext.buying.doctype.purchase_order import purchase_order + + po = test_purchase_order.create_purchase_order() + + pi = purchase_order.make_purchase_invoice(po.name) + pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. + pi.submit() + + pr = purchase_order.make_purchase_receipt(po.name) + pr.save() + # per_billed is only updated after submission. + self.assertEqual(flt(pr.per_billed), 0) + + pr.submit() + + pi.load_from_db() + pr.load_from_db() + + self.assertEqual(pr.status, "To Bill") + self.assertAlmostEqual(pr.per_billed, 50.0, places=2) + def get_gl_entries(voucher_type, voucher_no): return frappe.db.sql("""select account, debit, credit, cost_center from `tabGL Entry` where voucher_type=%s and voucher_no=%s From 061c82a5cfa439dad62221be8429e95663bddb8b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 13 Apr 2021 18:58:19 +0530 Subject: [PATCH 39/44] fix: patch fixes for property setter (#25313) partial backport of #25220 --- erpnext/regional/india/setup.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 62c15dcd715..d920a73ff4e 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -12,15 +12,15 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): - setup_company_independent_fixtures() + setup_company_independent_fixtures(patch=patch) if not patch: update_address_template() make_fixtures(company) # TODO: for all countries -def setup_company_independent_fixtures(): +def setup_company_independent_fixtures(patch=False): make_custom_fields() - make_property_setters() + make_property_setters(patch=patch) add_permissions() add_custom_roles_for_reports() frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) @@ -100,10 +100,11 @@ def add_print_formats(): frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) -def make_property_setters(): +def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters - make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') + if not patch: + make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') + make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', From d2d025b586b07f2b481714b386bcda73e1f2928b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 13 Apr 2021 18:58:42 +0530 Subject: [PATCH 40/44] fix: round total quantity in job card (#25246) Co-authored-by: Marica --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 7b924f7025a..dfb4a03c3f4 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -39,6 +39,8 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty + self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) + def get_overlap_for(self, args): existing = frappe.db.sql("""select jc.name as name from `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and From 73a7bb88fbbcddc69b813e495f448c5a09f27d3c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 13 Apr 2021 19:00:38 +0530 Subject: [PATCH 41/44] fix: remove gst name validation for purch Invoice (#25236) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/hooks.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2d995ed0e67..c1947d91c7c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -246,7 +246,7 @@ doc_events = { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_trash": "erpnext.regional.check_deletion_permission", - "validate": "erpnext.regional.india.utils.set_transporter_address" + "validate": ["erpnext.regional.india.utils.set_transporter_address", "erpnext.regional.india.utils.validate_document_name"] }, "Purchase Invoice": { "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" @@ -261,9 +261,6 @@ doc_events = { ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, - ('Sales Invoice', 'Purchase Invoice'): { - 'validate': ['erpnext.regional.india.utils.validate_document_name'] - }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", From e787d05f662531153f846b7f1c62a60131179b9c Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 15 Apr 2021 18:57:08 +0530 Subject: [PATCH 42/44] fix(e-invoicing): validations & tax calculation fixes (#25315) * fix: GST on freight charge in e-invoicing * fix: cannot fetch e invoice settings * fix: address validations & cancel eway bill dialog * fix: except einvoice loading error seperately * fix: sider * fix: import format_date * fix: imports * fix: test * fix: test * fix: test * fix: validate total condition * feat: add company link to e-invoice settings * fix: remove extra condition * fix: test Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../sales_invoice/test_sales_invoice.py | 15 +- .../test_tax_withholding_category.py | 41 -- .../shopify_settings/test_shopify_settings.py | 3 +- erpnext/hooks.py | 2 +- .../compensatory_leave_request.py | 2 +- erpnext/patches.txt | 2 + .../add_company_link_to_einvoice_settings.py | 16 + .../v12_0/create_taxable_value_field.py | 18 + .../e_invoice_user/e_invoice_user.json | 11 +- erpnext/regional/india/e_invoice/einvoice.js | 55 +-- erpnext/regional/india/e_invoice/utils.py | 382 +++++++++++------- erpnext/regional/india/setup.py | 5 +- erpnext/regional/india/utils.py | 47 ++- 13 files changed, 374 insertions(+), 225 deletions(-) create mode 100644 erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py create mode 100644 erpnext/patches/v12_0/create_taxable_value_field.py diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d3e85ead446..a5efe546dda 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1860,7 +1860,17 @@ class TestSalesInvoice(unittest.TestCase): def test_einvoice_submission_without_irn(self): # init - frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1) + einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings.enable = 1 + einvoice_settings.applicable_from = nowdate() + einvoice_settings.append('credentials', { + 'company': '_Test Company', + 'gstin': '27AAECE4835E1ZR', + 'username': 'test', + 'password': 'test' + }) + einvoice_settings.save() + country = frappe.flags.country frappe.flags.country = 'India' @@ -1871,7 +1881,8 @@ class TestSalesInvoice(unittest.TestCase): si.submit() # reset - frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0) + einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings.enable = 0 frappe.flags.country = country def test_einvoice_json(self): diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index a0b0cbb9956..a0b02bccf10 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -83,47 +83,6 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() - def test_single_threshold_tds_with_previous_vouchers(self): - invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS") - pi = create_purchase_invoice(supplier="Test TDS Supplier2") - pi.submit() - invoices.append(pi) - - pi = create_purchase_invoice(supplier="Test TDS Supplier2") - pi.submit() - invoices.append(pi) - - self.assertEqual(pi.taxes_and_charges_deducted, 2000) - self.assertEqual(pi.grand_total, 8000) - - # delete invoices to avoid clashing - for d in invoices: - d.cancel() - - def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self): - invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS") - pi = create_purchase_invoice(supplier="Test TDS Supplier2") - pi.submit() - invoices.append(pi) - - # TDS not applied - pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True) - pi.submit() - invoices.append(pi) - - pi = create_purchase_invoice(supplier="Test TDS Supplier2") - pi.submit() - invoices.append(pi) - - self.assertEqual(pi.taxes_and_charges_deducted, 2000) - self.assertEqual(pi.grand_total, 8000) - - # delete invoices to avoid clashing - for d in invoices: - d.cancel() - def create_purchase_invoice(**args): # return sales invoice doc object item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 64ef3dc0859..58e64e83bf8 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -17,8 +17,7 @@ class ShopifySettings(unittest.TestCase): frappe.set_user("Administrator") # use the fixture data - import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"), - ignore_links=True, overwrite=True) + import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) frappe.reload_doctype("Customer") frappe.reload_doctype("Sales Order") diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c1947d91c7c..56bffdd5637 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -246,7 +246,7 @@ doc_events = { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_trash": "erpnext.regional.check_deletion_permission", - "validate": ["erpnext.regional.india.utils.set_transporter_address", "erpnext.regional.india.utils.validate_document_name"] + "validate": ["erpnext.regional.india.utils.set_transporter_address", "erpnext.regional.india.utils.update_taxable_values", "erpnext.regional.india.utils.validate_document_name"] }, "Purchase Invoice": { "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index aa5a67f40c7..f11f08462ad 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_days, getdate, cint, format_date +from frappe.utils import date_diff, add_days, getdate, cint, formatdate as format_date from frappe.model.document import Document from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ get_holidays_for_employee, create_additional_leave_ledger_entry diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6b6e2ecdf54..28349f93fff 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -681,4 +681,6 @@ erpnext.patches.v12_0.update_payment_entry_status erpnext.patches.v12_0.add_transporter_address_field #2020-10-27 erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v12_0.add_state_code_for_ladakh +erpnext.patches.v12_0.create_taxable_value_field erpnext.patches.v12_0.purchase_receipt_status +erpnext.patches.v12_0.add_company_link_to_einvoice_settings diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py new file mode 100644 index 00000000000..b6bd5fa311c --- /dev/null +++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company or not frappe.db.count('E Invoice User'): + return + + frappe.reload_doc("regional", "doctype", "e_invoice_user") + for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): + company_name = frappe.db.sql(""" + select dl.link_name from `tabAddress` a, `tabDynamic Link` dl + where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' + """, (creds.get('gstin'))) + if company_name and len(company_name) > 0: + frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) \ No newline at end of file diff --git a/erpnext/patches/v12_0/create_taxable_value_field.py b/erpnext/patches/v12_0/create_taxable_value_field.py new file mode 100644 index 00000000000..a0c9fcf4cbe --- /dev/null +++ b/erpnext/patches/v12_0/create_taxable_value_field.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = { + 'Sales Invoice Item': [ + dict(fieldname='taxable_value', label='Taxable Value', + fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", + print_hide=1) + ] + } + + create_custom_fields(custom_fields, update=True) \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json index dd9d99773a3..a65b1ca7ca8 100644 --- a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json @@ -5,6 +5,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "company", "gstin", "username", "password" @@ -30,12 +31,20 @@ "in_list_view": 1, "label": "Password", "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-22 15:10:53.466205", + "modified": "2021-03-22 12:16:56.365616", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice User", diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 9fa1334840d..cda732b0fb7 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -1,12 +1,13 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.ui.form.on(doctype, { - refresh(frm) { - const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); - const supply_type = frm.doc.gst_category; - const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); - const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; + async refresh(frm) { + const res = await frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', + args: { doc: frm.doc } + }); + const invoice_eligible = res.message; - if (!einvoicing_enabled || !valid_supply_type || company_transaction) return; + if (!invoice_eligible) return; const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; @@ -113,45 +114,25 @@ erpnext.setup_einvoice_actions = (doctype) => { } if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { - const fields = [ - { - "label": "Reason", - "fieldname": "reason", - "fieldtype": "Select", - "reqd": 1, - "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", - "fieldname": "remark", - "fieldtype": "Data", - "reqd": 1 - } - ]; const action = () => { - const d = new frappe.ui.Dialog({ - title: __('Cancel E-Way Bill'), - fields: fields, + let message = __('Cancellation of e-way bill is currently not supported. '); + message += '

'; + message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); + + frappe.msgprint({ + title: __('Update E-Way Bill Cancelled Status?'), + message: message, + indicator: 'orange', primary_action: function() { - const data = d.get_values(); frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { - doctype, - docname: name, - eway_bill: ewaybill, - reason: data.reason.split('-')[0], - remark: data.remark - }, + args: { doctype, docname: name }, freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() + callback: () => frm.reload_doc() }); }, - primary_action_label: __('Submit') + primary_action_label: __('Yes') }); - d.show(); }; add_custom_button(__("Cancel E-Way Bill"), action); } diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index dd130b69fff..ed7e28a1b0a 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import os import re +import six import jwt import sys import json @@ -16,16 +17,38 @@ from frappe import _, bold from pyqrcode import create as qrcreate from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form +from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, getdate, get_link_to_form -def validate_einvoice_fields(doc): - einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) - invalid_doctype = doc.doctype != 'Sales Invoice' +@frappe.whitelist() +def validate_eligibility(doc): + if isinstance(doc, six.string_types): + doc = json.loads(doc) + + invalid_doctype = doc.get('doctype') != 'Sales Invoice' + if invalid_doctype: + return False + + einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable')) + if not einvoicing_enabled: + return False + + if getdate(doc.get('posting_date')) < getdate('2021-04-01'): + return False + + invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') no_taxes_applied = not doc.get('taxes') - if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied: + if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied: + return False + + return True + +def validate_einvoice_fields(doc): + invoice_eligible = validate_eligibility(doc) + + if not invoice_eligible: return if doc.docstatus == 0 and doc._action == 'save': @@ -86,36 +109,37 @@ def get_doc_details(invoice): invoice_date=invoice_date )) -def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None): - d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - - if ((not d.gstin and not shipping_address) - or not d.city - or not d.pincode - or not d.address_title - or not d.address_line1 - or not d.gst_state_number): +def validate_address_fields(address, is_shipping_address): + if ((not address.gstin and not is_shipping_address) + or not address.city + or not address.pincode + or not address.address_title + or not address.address_line1 + or not address.gst_state_number): frappe.throw( - msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format( - get_link_to_form('Address', address_name) - ), + msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name), title=_('Missing Address Fields') ) - if d.gst_state_number == 97: +def get_party_details(address_name, is_shipping_address=False): + addr = frappe.get_doc('Address', address_name) + + validate_address_fields(addr, is_shipping_address) + + if addr.gst_state_number == 97: # according to einvoice standard - pincode = 999999 + addr.pincode = 999999 party_address_details = frappe._dict(dict( - legal_name=d.address_title, - location=d.city, pincode=d.pincode, - state_code=d.gst_state_number, - address_line1=d.address_line1, - address_line2=d.address_line2 + legal_name=sanitize_for_json(addr.address_title), + location=sanitize_for_json(addr.city), + pincode=addr.pincode, gstin=addr.gstin, + state_code=addr.gst_state_number, + address_line1=sanitize_for_json(addr.address_line1), + address_line2=sanitize_for_json(addr.address_line2) )) - if d.gstin: - party_address_details.gstin = d.gstin + return party_address_details def get_gstin_details(gstin): @@ -166,10 +190,15 @@ def get_item_list(invoice): item.description = json.dumps(d.item_name)[1:-1] item.qty = abs(item.qty) - item.discount_amount = 0 - item.unit_rate = abs(item.base_net_amount / item.qty) - item.gross_amount = abs(item.base_net_amount) - item.taxable_value = abs(item.base_net_amount) + + if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: + item.discount_amount = abs(item.base_amount - item.base_net_amount) + else: + item.discount_amount = 0 + + item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) + item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.taxable_value = abs(item.taxable_value) item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -202,11 +231,11 @@ def update_item_taxes(invoice, item): is_applicable = t.tax_amount and t.account_head in gst_accounts_list if is_applicable: # this contains item wise tax rate & tax amount (incl. discount) - item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name) item_tax_rate = item_tax_detail[0] # item tax amount excluding discount amount - item_tax_amount = (item_tax_rate / 100) * item.base_net_amount + item_tax_amount = (item_tax_rate / 100) * item.taxable_value if t.account_head in gst_accounts.cess_account: item_tax_amount_after_discount = item_tax_detail[1] @@ -227,10 +256,14 @@ def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - invoice_value_details.base_total = abs(invoice.base_total) - invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount) + # Discount already applied on net total which means on items + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.invoice_discount_amt = 0 + elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount: + invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) else: - invoice_value_details.base_total = abs(invoice.base_net_total) + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) # since tax already considers discount amount invoice_value_details.invoice_discount_amt = 0 @@ -251,7 +284,11 @@ def update_invoice_taxes(invoice, invoice_value_details): invoice_value_details.total_igst_amt = 0 invoice_value_details.total_cess_amt = 0 invoice_value_details.total_other_charges = 0 + considered_rows = [] + for t in invoice.taxes: + tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \ + else t.base_tax_amount_after_discount_amount if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: # using after discount amt since item also uses after discount amt for cess calc @@ -259,12 +296,26 @@ def update_invoice_taxes(invoice, invoice_value_details): for tax_type in ['igst', 'cgst', 'sgst']: if t.account_head in gst_accounts['{}_account'.format(tax_type)]: - invoice_value_details['total_{}_amt'.format(tax_type)] += abs(t.base_tax_amount_after_discount_amount) + + invoice_value_details['total_{}_amt'.format(tax_type)] += abs(tax_amount) + update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) - + invoice_value_details.total_other_charges += abs(tax_amount) + return invoice_value_details +def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows): + prev_row_id = cint(tax_row.row_id) - 1 + if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: + if tax_row.charge_type == 'On Previous Row Amount': + amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount + invoice_value_details.total_other_charges -= abs(amount) + considered_rows.append(prev_row_id) + if tax_row.charge_type == 'On Previous Row Total': + amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total + invoice_value_details.total_other_charges -= abs(amount) + considered_rows.append(prev_row_id) + def get_payment_details(invoice): payee_name = invoice.company mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) @@ -277,6 +328,10 @@ def get_payment_details(invoice): )) def get_return_doc_reference(invoice): + if not invoice.return_against: + frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') + .format(frappe.bold('Return Against')), title=_('Missing Field')) + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') @@ -284,7 +339,11 @@ def get_return_doc_reference(invoice): def get_eway_bill_details(invoice): if invoice.is_return: - frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) + frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), + title=_('Invalid Fields')) + + if not invoice.distance: + frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field')) mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } @@ -302,9 +361,15 @@ def get_eway_bill_details(invoice): def validate_mandatory_fields(invoice): if not invoice.company_address: - frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields')) + frappe.throw( + _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'), + title=_('Missing Fields') + ) if not invoice.customer_address: - frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields')) + frappe.throw( + _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'), + title=_('Missing Fields') + ) if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): frappe.throw( _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), @@ -316,6 +381,39 @@ def validate_mandatory_fields(invoice): title=_('Missing Fields') ) +def validate_totals(einvoice): + item_list = einvoice['ItemList'] + value_details = einvoice['ValDtls'] + + total_item_ass_value = 0 + total_item_cgst_value = 0 + total_item_sgst_value = 0 + total_item_igst_value = 0 + total_item_value = 0 + for item in item_list: + total_item_ass_value += flt(item['AssAmt']) + total_item_cgst_value += flt(item['CgstAmt']) + total_item_sgst_value += flt(item['SgstAmt']) + total_item_igst_value += flt(item['IgstAmt']) + total_item_value += flt(item['TotItemVal']) + + if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1: + frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx)) + + if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: + frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) + + if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1: + frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) + + calculated_invoice_value = \ + flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ + + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ + + flt(value_details['OthChrg']) - flt(value_details['Discount']) + + if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: + frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) + def make_einvoice(invoice): validate_mandatory_fields(invoice) @@ -325,35 +423,38 @@ def make_einvoice(invoice): item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) invoice_value_details = get_invoice_value_details(invoice) - seller_details = get_party_details(invoice.company_address, company_address=1) + seller_details = get_party_details(invoice.company_address) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) else: - buyer_details = get_party_details(invoice.customer_address, billing_address=1) + buyer_details = get_party_details(invoice.customer_address) place_of_supply = get_place_of_supply(invoice, invoice.doctype) if place_of_supply: place_of_supply = place_of_supply.split('-')[0] else: place_of_supply = invoice.billing_address_gstin[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) + + seller_details.update(dict(legal_name=invoice.company)) + buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer)) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: if invoice.gst_category == 'Overseas': shipping_details = get_overseas_address_details(invoice.shipping_address_name) else: - shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1) + shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=True) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) - - if invoice.transporter: + + if invoice.transporter and flt(invoice.distance) and not invoice.is_return: eway_bill_details = get_eway_bill_details(invoice) - + # not yet implemented dispatch_details = period_details = export_details = frappe._dict({}) @@ -364,80 +465,86 @@ def make_einvoice(invoice): period_details=period_details, prev_doc_details=prev_doc_details, export_details=export_details, eway_bill_details=eway_bill_details ) - einvoice = json.loads(einvoice) - validations = json.loads(read_json('einv_validation')) - errors = validate_einvoice(validations, einvoice) - if errors: - message = "\n".join([ - "E Invoice: ", json.dumps(einvoice, indent=4), - "-" * 50, - "Errors: ", json.dumps(errors, indent=4) - ]) - frappe.log_error(title="E Invoice Validation Failed", message=message) - throw_error_list(errors, _('E Invoice Validation Failed')) + try: + einvoice = safe_json_load(einvoice) + einvoice = santize_einvoice_fields(einvoice) + except Exception: + show_link_to_error_log(invoice, einvoice) + + validate_totals(einvoice) return einvoice -def throw_error_list(errors, title): - if len(errors) > 1: - li = ['
  • '+ d +'
  • ' for d in errors] - frappe.throw("
      {}
    ".format(''.join(li)), title=title) - else: - frappe.throw(errors[0], title=title) +def show_link_to_error_log(invoice, einvoice): + err_log = log_error(einvoice) + link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log') + frappe.throw( + _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format( + invoice.name, link_to_error_log), + title=_('E Invoice Creation Failed') + ) -def validate_einvoice(validations, einvoice, errors=None): - if errors is None: - errors = [] - for fieldname, field_validation in validations.items(): - value = einvoice.get(fieldname, None) - if not value or value == "None": - # remove keys with empty values - einvoice.pop(fieldname, None) - continue +def log_error(data=None): + if isinstance(data, six.string_types): + data = json.loads(data) - value_type = field_validation.get("type").lower() - if value_type in ['object', 'array']: - child_validations = field_validation.get('properties') + seperator = "--" * 50 + err_tb = traceback.format_exc() + err_msg = str(sys.exc_info()[1]) + data = json.dumps(data, indent=4) - if isinstance(value, list): - for d in value: - validate_einvoice(child_validations, d, errors) - if not d: - # remove empty dicts - einvoice.pop(fieldname, None) + message = "\n".join([ + "Error", err_msg, seperator, + "Data:", data, seperator, + "Exception:", err_tb + ]) + frappe.log_error(title=_('E Invoice Request Failed'), message=message) + +def santize_einvoice_fields(einvoice): + int_fields = ["Pin","Distance","CrDay"] + float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",] + copy = einvoice.copy() + for key, value in copy.items(): + if isinstance(value, list): + for idx, d in enumerate(value): + santized_dict = santize_einvoice_fields(d) + if santized_dict: + einvoice[key][idx] = santized_dict + else: + einvoice[key].pop(idx) + + if not einvoice[key]: + einvoice.pop(key, None) + + elif isinstance(value, dict): + santized_dict = santize_einvoice_fields(value) + if santized_dict: + einvoice[key] = santized_dict else: - validate_einvoice(child_validations, value, errors) - if not value: - # remove empty dicts - einvoice.pop(fieldname, None) - continue + einvoice.pop(key, None) - # convert to int or str - if value_type == 'string': - einvoice[fieldname] = str(value) - elif value_type == 'number': - is_integer = '.' not in str(field_validation.get('maximum')) - precision = 3 if '.999' in str(field_validation.get('maximum')) else 2 - einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value) - value = einvoice[fieldname] + elif not value or value == "None": + einvoice.pop(key, None) - max_length = field_validation.get('maxLength') - minimum = flt(field_validation.get('minimum')) - maximum = flt(field_validation.get('maximum')) - pattern_str = field_validation.get('pattern') - pattern = re.compile(pattern_str or '') + elif key in float_fields: + einvoice[key] = flt(value, 2) - label = field_validation.get('description') or fieldname + elif key in int_fields: + einvoice[key] = cint(value) - if value_type == 'string' and len(value) > max_length: - errors.append(_('{} should not exceed {} characters').format(label, max_length)) - if value_type == 'number' and (value > maximum or value < minimum): - errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) - if pattern_str and not pattern.match(value): - errors.append(field_validation.get('validationMsg')) - - return errors + return einvoice + +def safe_json_load(json_string): + JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError + try: + return json.loads(json_string) + except JSONDecodeError as e: + # print a snippet of 40 characters around the location where error occured + pos = e.pos + start, end = max(0, pos-20), min(len(json_string)-1, pos+20) + snippet = json_string[start:end] + frappe.throw(_("Error in input data. Please check for any special characters near following input:
    {}").format(snippet)) class RequestFailed(Exception): pass @@ -463,13 +570,17 @@ class GSPConnector(): def get_credentials(self): if self.invoice: gstin = self.get_seller_gstin() - credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) + credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin] + if credentials_for_gstin: + self.credentials = credentials_for_gstin[0] + else: + frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings')) else: credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None return credentials def get_seller_gstin(self): - gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin') if not gstin: frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) return gstin @@ -517,7 +628,7 @@ class GSPConnector(): self.e_invoice_settings.reload() except Exception: - self.log_error(res) + log_error(res) self.raise_error(True) def get_headers(self): @@ -539,14 +650,14 @@ class GSPConnector(): if res.get('success'): return res.get('result') else: - self.log_error(res) + log_error(res) raise RequestFailed except RequestFailed: self.raise_error() except Exception: - self.log_error() + log_error() self.raise_error(True) @staticmethod @@ -592,7 +703,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error(data) + log_error(data) self.raise_error(True) def get_irn_details(self, irn): @@ -611,7 +722,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error() + log_error() self.raise_error(True) def cancel_irn(self, irn, reason, remark): @@ -624,7 +735,7 @@ class GSPConnector(): try: res = self.make_request('post', self.cancel_irn_url, headers, data) - if res.get('success'): + if res.get('success') or '9999' in res.get('message'): self.invoice.irn_cancelled = 1 self.invoice.flags.updater_reference = { 'doctype': self.invoice.doctype, @@ -641,7 +752,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error(data) + log_error(data) self.raise_error(True) def generate_eway_bill(self, **kwargs): @@ -682,7 +793,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error(data) + log_error(data) self.raise_error(True) def cancel_eway_bill(self, eway_bill, reason, remark): @@ -715,7 +826,7 @@ class GSPConnector(): self.raise_error(errors=errors) except Exception: - self.log_error(data) + log_error(data) self.raise_error(True) def sanitize_error_message(self, message): @@ -740,22 +851,6 @@ class GSPConnector(): errors[idx] = errors[idx][:-6] return errors - - def log_error(self, data={}): - if not isinstance(data, dict): - data = json.loads(data) - - seperator = "--" * 50 - err_tb = traceback.format_exc() - err_msg = str(sys.exc_info()[1]) - data = json.dumps(data, indent=4) - - message = "\n".join([ - "Error", err_msg, seperator, - "Data:", data, seperator, - "Exception:", err_tb - ]) - frappe.log_error(title=_('E Invoice Request Failed'), message=message) def raise_error(self, raise_exception=False, errors=[]): title = _('E Invoice Request Failed') @@ -776,6 +871,8 @@ class GSPConnector(): self.invoice.irn = res.get('Irn') self.invoice.ewaybill = res.get('EwbNo') + self.invoice.ack_no = res.get('AckNo') + self.invoice.ack_date = res.get('AckDt') self.invoice.signed_einvoice = dec_signed_invoice self.invoice.signed_qr_code = res.get('SignedQRCode') @@ -815,6 +912,11 @@ class GSPConnector(): self.invoice.flags.ignore_validate = True self.invoice.save() +def sanitize_for_json(string): + """Escape JSON specific characters from a string.""" + # json.dumps adds double-quotes to the string. Indexing to remove them. + return json.dumps(string)[1:-1] + @frappe.whitelist() def get_einvoice(doctype, docname): invoice = frappe.get_doc(doctype, docname) @@ -837,5 +939,9 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): - gsp_connector = GSPConnector(doctype, docname) - gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + # TODO: uncomment when eway_bill api from Adequare is enabled + # gsp_connector = GSPConnector(doctype, docname) + # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + + # update cancelled status only, to be able to cancel irn next + frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index d920a73ff4e..d417360d938 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -116,6 +116,9 @@ def make_custom_fields(update=True): is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', print_hide=1) + taxable_value = dict(fieldname='taxable_value', label='Taxable Value', + fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", + print_hide=1) purchase_invoice_gst_category = [ dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', @@ -451,7 +454,7 @@ def make_custom_fields(update=True): 'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], + 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], 'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 5d5c2e9610c..f973dc657fe 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe, re, json from frappe import _ import erpnext -from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate +from frappe.utils import cstr, flt, cint, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate from erpnext.regional.india import states, state_numbers from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount from erpnext.controllers.accounts_controller import get_taxes_and_charges @@ -827,3 +827,48 @@ def get_gst_tax_amount(doc): gst_tax -= tax.tax_amount_after_discount_amount return gst_tax, base_gst_tax + +def update_taxable_values(doc, method): + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'India': + return + + gst_accounts = get_gst_accounts(doc.company) + + # Only considering sgst account to avoid inflating taxable value + gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \ + + gst_accounts.get('igst_account', []) + + additional_taxes = 0 + total_charges = 0 + item_count = 0 + considered_rows = [] + + for tax in doc.get('taxes'): + prev_row_id = cint(tax.row_id) - 1 + if tax.account_head in gst_account_list and prev_row_id not in considered_rows: + if tax.charge_type == 'On Previous Row Amount': + additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount + considered_rows.append(prev_row_id) + if tax.charge_type == 'On Previous Row Total': + additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total + considered_rows.append(prev_row_id) + + for item in doc.get('items'): + if doc.apply_discount_on == 'Grand Total' and doc.discount_amount: + proportionate_value = item.base_amount if doc.base_total else item.qty + total_value = doc.base_total if doc.base_total else doc.total_qty + else: + proportionate_value = item.base_net_amount if doc.base_net_total else item.qty + total_value = doc.base_net_total if doc.base_net_total else doc.total_qty + + applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), + item.precision('taxable_value'))) + item.taxable_value = applicable_charges + proportionate_value + total_charges += applicable_charges + item_count += 1 + + if total_charges != additional_taxes: + diff = additional_taxes - total_charges + doc.get('items')[item_count - 1].taxable_value += diff From 4481d4e965144647ae703596312f3d00e552931e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Apr 2021 12:48:27 +0530 Subject: [PATCH 43/44] chore: added change log --- erpnext/change_log/v12/v12_20_0.md | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 erpnext/change_log/v12/v12_20_0.md diff --git a/erpnext/change_log/v12/v12_20_0.md b/erpnext/change_log/v12/v12_20_0.md new file mode 100644 index 00000000000..b7c16c236ca --- /dev/null +++ b/erpnext/change_log/v12/v12_20_0.md @@ -0,0 +1,34 @@ +## Version 12.20.0 Release Notes + + +### Fixes & Enhancements + +- E-invoicing - Validations & tax calculation fixes ([#25315](https://github.com/frappe/erpnext/pull/25315)) +- Allow zero valuation in stock reconciliation ([#24986](https://github.com/frappe/erpnext/pull/24986)) +- Remove shipping address GSTIN validation for e-invoice ([#25133](https://github.com/frappe/erpnext/pull/25133)) +- Allow Item image alt ([#24935](https://github.com/frappe/erpnext/pull/24935)) +- Added parent task expected end date validation ([#24889](https://github.com/frappe/erpnext/pull/24889)) +- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24919](https://github.com/frappe/erpnext/pull/24919)) +- Item attributes not editable until refresh ([#24887](https://github.com/frappe/erpnext/pull/24887)) +- Create property setters for shorter naming series ([#25134](https://github.com/frappe/erpnext/pull/25134)) +- Serial no refresh issue ([#25130](https://github.com/frappe/erpnext/pull/25130)) +- Sales Order not saving due type mismatch in promo scheme (#24748) ([#25056](https://github.com/frappe/erpnext/pull/25056)) +- TDS check getting checked after reload ([#24974](https://github.com/frappe/erpnext/pull/24974)) +- Round total quantity in job card ([#25246](https://github.com/frappe/erpnext/pull/25246)) +- santize_for_json is not defined ([#25157](https://github.com/frappe/erpnext/pull/25157)) +- Apply single transaction threshold on net_total instead of supplier credit amount ([#25208](https://github.com/frappe/erpnext/pull/25208)) +- Zero amount completed delivery notes being shown in Sales Invoice get items ([#25318](https://github.com/frappe/erpnext/pull/25318)) +- Error message compensatory leave request ([#25216](https://github.com/frappe/erpnext/pull/25216)) +- RCM tax calculation ([#25226](https://github.com/frappe/erpnext/pull/25226)) +- Don't delete mode of payment account details while deleting comp… ([#25218](https://github.com/frappe/erpnext/pull/25218)) +- Opportunity-quotation mapping order status ([#25002](https://github.com/frappe/erpnext/pull/25002)) +- Object referencing same memory address issue ([#25165](https://github.com/frappe/erpnext/pull/25165)) +- Validation msg for TransDocNo e-invoicing ([#25120](https://github.com/frappe/erpnext/pull/25120)) +- Do not fetch stopped MR in production plan ([#25110](https://github.com/frappe/erpnext/pull/25110)) +- Incorrect status creating PR from PO after creating PI ([#25203](https://github.com/frappe/erpnext/pull/25203)) +- Remove gst name validation for purchase Invoice ([#25236](https://github.com/frappe/erpnext/pull/25236)) +- Assignment Rule Unassign Condition doesn't work ([#24890](https://github.com/frappe/erpnext/pull/24890)) +- Place of supply for e-invoicing ([#25149](https://github.com/frappe/erpnext/pull/25149)) +- Serial no trim issue ([#24950](https://github.com/frappe/erpnext/pull/24950)) +- Commit individual SLE rename for large datasets (v12) ([#25085](https://github.com/frappe/erpnext/pull/25085)) +- Incorrect batch picked in subcontracted purchase receipt ([#25169](https://github.com/frappe/erpnext/pull/25169)) \ No newline at end of file From 08399f90159d08f6fbe60ad7b076332c4b150743 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Apr 2021 13:11:07 +0550 Subject: [PATCH 44/44] bumped to version 12.20.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a76090d609d..566dd158a67 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '12.19.0' +__version__ = '12.20.0' def get_default_company(user=None): '''Get default company for user'''