From 9578ead7f9d6ac25094108e341a9f7b9c09561d0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 28 Mar 2020 18:44:53 +0530 Subject: [PATCH 001/178] fix: no server side validations for accounts in asset category --- .../doctype/asset_category/asset_category.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index fc08841be99..855c7094115 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -11,12 +11,29 @@ from frappe.model.document import Document class AssetCategory(Document): def validate(self): self.validate_finance_books() + self.validate_accounts() def validate_finance_books(self): for d in self.finance_books: for field in ("Total Number of Depreciations", "Frequency of Depreciation"): if cint(d.get(frappe.scrub(field)))<1: frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) + + def validate_accounts(self): + account_type_map = { + 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, + 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, + 'depreciation_expense_account': { 'account_type': 'Expense' }, + 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } + } + for d in self.accounts: + for account in account_type_map.keys(): + if d.get(account): + account_type = frappe.db.get_value('Account', d.get(account), 'account_type') + if account_type != account_type_map[account]['account_type']: + frappe.throw(_("Row {}: {} should be a {} account".format(d.idx, frappe.bold(frappe.unscrub(account)), + frappe.bold(account_type_map[account]['account_type']))), title=_("Invalid Account")) + @frappe.whitelist() def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None): From 086e5c4dac235ef99775488fbd0d497ff23b54f2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 28 Mar 2020 18:44:53 +0530 Subject: [PATCH 002/178] fix: no server side validations for accounts in asset category --- erpnext/assets/doctype/asset_category/asset_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 855c7094115..e9ef0c7d9ee 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -31,7 +31,7 @@ class AssetCategory(Document): if d.get(account): account_type = frappe.db.get_value('Account', d.get(account), 'account_type') if account_type != account_type_map[account]['account_type']: - frappe.throw(_("Row {}: {} should be a {} account".format(d.idx, frappe.bold(frappe.unscrub(account)), + frappe.throw(_("Row {}: Account Type of {} should be {} account".format(d.idx, frappe.bold(frappe.unscrub(account)), frappe.bold(account_type_map[account]['account_type']))), title=_("Invalid Account")) From 04201028d15063901faf952c8b03dc125006efc7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Apr 2020 22:17:41 +0530 Subject: [PATCH 003/178] fix: tests --- erpnext/accounts/doctype/account/test_account.py | 5 +++-- .../doctype/asset_category/asset_category.py | 16 ++++++++++------ .../purchase_receipt/test_purchase_receipt.py | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index dc23b2b2d05..9894b9309aa 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -69,6 +69,7 @@ class TestAccount(unittest.TestCase): acc.account_name = "Accumulated Depreciation" acc.parent_account = "Fixed Assets - _TC" acc.company = "_Test Company" + acc.account_type = "Accumulated Depreciation" acc.insert() doc = frappe.get_doc("Account", "Securities and Deposits - _TC") @@ -149,8 +150,8 @@ def _make_test_records(verbose): # fixed asset depreciation ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], - ["_Test Accumulated Depreciations", "Current Assets", 0, None, None], - ["_Test Depreciations", "Expenses", 0, None, None], + ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], + ["_Test Depreciations", "Expenses", 0, "Expense", None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], # Receivable / Payable Account diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index e9ef0c7d9ee..85e5d98d163 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -27,12 +27,16 @@ class AssetCategory(Document): 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } } for d in self.accounts: - for account in account_type_map.keys(): - if d.get(account): - account_type = frappe.db.get_value('Account', d.get(account), 'account_type') - if account_type != account_type_map[account]['account_type']: - frappe.throw(_("Row {}: Account Type of {} should be {} account".format(d.idx, frappe.bold(frappe.unscrub(account)), - frappe.bold(account_type_map[account]['account_type']))), title=_("Invalid Account")) + for fieldname in account_type_map.keys(): + if d.get(fieldname): + selected_account = d.get(fieldname) + selected_account_type = frappe.db.get_value('Account', selected_account, 'account_type') + expected_account_type = account_type_map[fieldname]['account_type'] + + if selected_account_type != expected_account_type: + frappe.throw(_("Row #{}: Account Type of {} should be {}. Please modify the account type or select a different account." + .format(d.idx, frappe.bold(selected_account), frappe.bold(expected_account_type))), + title=_("Invalid Account")) @frappe.whitelist() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a51b25bf36d..09271cebfa8 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -355,8 +355,8 @@ class TestPurchaseReceipt(unittest.TestCase): 'accounts': [{ 'company_name': '_Test Company', 'fixed_asset_account': '_Test Fixed Asset - _TC', - 'accumulated_depreciation_account': 'Depreciation - _TC', - 'depreciation_expense_account': 'Depreciation - _TC' + 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC', + 'depreciation_expense_account': '_Test Depreciation - _TC' }] }).insert() From de9c73c5cd89b8e071388a3b299aa81787472d4e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Apr 2020 22:17:41 +0530 Subject: [PATCH 004/178] fix: tests --- .../assets/doctype/asset_category/asset_category.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 85e5d98d163..770b1ee9a41 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -23,19 +23,20 @@ class AssetCategory(Document): account_type_map = { 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, - 'depreciation_expense_account': { 'account_type': 'Expense' }, + 'depreciation_expense_account': { 'root_type': 'Expense' }, 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } } for d in self.accounts: for fieldname in account_type_map.keys(): if d.get(fieldname): selected_account = d.get(fieldname) - selected_account_type = frappe.db.get_value('Account', selected_account, 'account_type') - expected_account_type = account_type_map[fieldname]['account_type'] + key_to_match = account_type_map[fieldname].keys()[0] + selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) + expected_key_type = account_type_map[fieldname][key_to_match] - if selected_account_type != expected_account_type: - frappe.throw(_("Row #{}: Account Type of {} should be {}. Please modify the account type or select a different account." - .format(d.idx, frappe.bold(selected_account), frappe.bold(expected_account_type))), + if selected_key_type != expected_key_type: + frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account." + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type))), title=_("Invalid Account")) From 849ec84852dfb3437d65b579c8175c14dce89af4 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Apr 2020 21:00:57 +0530 Subject: [PATCH 005/178] feat: Provision to set Default Item Manufacturer - Is Default checkbox added in Item Manufacturer - Default Item Manufacturer and Part No fields added to Item Master - Manufacturer Part No field editable in all child tables with validation - Manufacturer and Part No auto fetched via get_item_details in child table --- .../purchase_invoice_item.json | 5 +-- .../purchase_order_item.json | 5 +-- .../supplier_quotation_item.json | 7 ++-- erpnext/public/js/controllers/buying.js | 26 ++++++++++++- erpnext/stock/doctype/item/item.json | 16 +++++++- .../item_manufacturer/item_manufacturer.json | 14 ++++++- .../item_manufacturer/item_manufacturer.py | 38 +++++++++++++++++++ .../material_request_item.json | 5 +-- .../purchase_receipt_item.json | 5 +-- erpnext/stock/get_item_details.py | 3 ++ 10 files changed, 105 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 296c7049ce4..a8dfab6a54b 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -753,8 +753,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "depends_on": "is_fixed_asset", @@ -770,7 +769,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-01 14:20:17.297284", + "modified": "2020-04-07 18:34:35.104178", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c409c1f46e0..5a964485a5d 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -698,8 +698,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "default": "0", @@ -712,7 +711,7 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-07 17:19:12.090355", + "modified": "2020-04-07 18:35:17.558928", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 7d7d6f4d3db..b50e834ec73 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -522,8 +523,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "fieldname": "column_break_15", @@ -532,7 +532,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-06-02 05:32:46.019237", + "links": [], + "modified": "2020-04-07 18:35:51.175947", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 27a9de95e0e..afbdbc661d3 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -379,7 +379,31 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ } }); } - } + }, + + manufacturer_part_no: function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + + if (row.manufacturer_part_no) { + frappe.model.get_value('Item Manufacturer', + { + 'item_code': row.item_code, + 'manufacturer': row.manufacturer, + 'manufacturer_part_no': row.manufacturer_part_no + }, + 'name', + function(data) { + if (!data) { + let msg = { + message: __("Manufacturer Part Number {0} is invalid", [row.manufacturer_part_no]), + title: __("Invalid Part Number") + } + frappe.throw(msg); + } + }); + + } + } }); cur_frm.add_fetch('project', 'cost_center', 'cost_center'); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index aa6b2fedd7c..7d2e3112fb3 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -114,6 +114,8 @@ "is_sub_contracted_item", "column_break_74", "customer_code", + "default_item_manufacturer", + "default_manufacturer_part_no", "website_section", "show_in_website", "show_variant_in_website", @@ -1038,6 +1040,18 @@ "fieldname": "auto_create_assets", "fieldtype": "Check", "label": "Auto Create Assets on Purchase" + }, + { + "fieldname": "default_item_manufacturer", + "fieldtype": "Data", + "label": "Default Item Manufacturer", + "read_only": 1 + }, + { + "fieldname": "default_manufacturer_part_no", + "fieldtype": "Data", + "label": "Default Manufacturer Part No", + "read_only": 1 } ], "has_web_view": 1, @@ -1046,7 +1060,7 @@ "image_field": "image", "links": [], "max_attachments": 1, - "modified": "2020-03-24 16:14:36.950677", + "modified": "2020-04-07 15:56:06.195722", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.json b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.json index 956c92e6738..0cef6eafaef 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.json +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2019-06-02 04:41:37.332911", "doctype": "DocType", @@ -10,7 +11,8 @@ "manufacturer_part_no", "column_break_3", "item_name", - "description" + "description", + "is_default" ], "fields": [ { @@ -52,9 +54,17 @@ "fieldtype": "Small Text", "label": "Description", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Default" } ], - "modified": "2019-06-06 19:07:31.175919", + "links": [], + "modified": "2020-04-07 20:25:55.507905", "modified_by": "Administrator", "module": "Stock", "name": "Item Manufacturer", diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py index 67eab82d970..c27d1be7892 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py @@ -11,6 +11,10 @@ from frappe.model.document import Document class ItemManufacturer(Document): def validate(self): self.validate_duplicate_entry() + self.manage_default_item_manufacturer() + + def on_trash(self): + self.manage_default_item_manufacturer(delete=True) def validate_duplicate_entry(self): if self.is_new(): @@ -24,6 +28,40 @@ class ItemManufacturer(Document): frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}") .format(self.item_code, self.manufacturer)) + def manage_default_item_manufacturer(self, delete=False): + from frappe.model.utils import set_default + + item = frappe.get_doc("Item", self.item_code) + default_manufacturer = item.default_item_manufacturer + default_part_no = item.default_manufacturer_part_no + + if not self.is_default: + # if unchecked and default in Item master, clear it. + if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no: + frappe.db.set_value("Item", item.name, + { + "default_item_manufacturer": None, + "default_manufacturer_part_no": None + }) + + elif self.is_default: + set_default(self, "item_code") + manufacturer, manufacturer_part_no = default_manufacturer, default_part_no + + if delete: + manufacturer, manufacturer_part_no = None, None + + elif (default_manufacturer != self.manufacturer) or \ + (default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no): + manufacturer = self.manufacturer + manufacturer_part_no = self.manufacturer_part_no + + frappe.db.set_value("Item", item.name, + { + "default_item_manufacturer": manufacturer, + "default_manufacturer_part_no": manufacturer_part_no + }) + @frappe.whitelist() def get_item_manufacturer_part_no(item_code, manufacturer): return frappe.db.get_value("Item Manufacturer", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 9d1dafb136b..30206b62d0c 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -404,14 +404,13 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-02-25 03:09:10.698967", + "modified": "2020-04-07 18:37:54.495112", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index d5cf4cadb14..e1dc95be481 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -799,8 +799,7 @@ { "fieldname": "manufacturer_part_no", "fieldtype": "Data", - "label": "Manufacturer Part Number", - "read_only": 1 + "label": "Manufacturer Part Number" }, { "depends_on": "is_fixed_asset", @@ -823,7 +822,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-05 14:19:48.799370", + "modified": "2020-04-07 18:38:21.141558", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 54dc685e4ef..51f27fd3e78 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -339,6 +339,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): else: out["manufacturer_part_no"] = None out["manufacturer"] = None + else: + out["manufacturer"], out["manufacturer_part_no"] = frappe.get_value("Item", item.name, + ["default_item_manufacturer", "default_manufacturer_part_no"] ) child_doctype = args.doctype + ' Item' meta = frappe.get_meta(child_doctype) From 39b8e150bf78c961503b19fe07d7610086a090ce Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 9 Apr 2020 12:41:24 +0530 Subject: [PATCH 006/178] fix: travis --- erpnext/accounts/doctype/account/test_account.py | 2 +- erpnext/assets/doctype/asset_category/asset_category.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 9894b9309aa..89bb0184af8 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -151,7 +151,7 @@ def _make_test_records(verbose): # fixed asset depreciation ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], - ["_Test Depreciations", "Expenses", 0, "Expense", None], + ["_Test Depreciations", "Expenses", 0, None, None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], # Receivable / Payable Account diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 770b1ee9a41..d4ea2504944 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -30,7 +30,7 @@ class AssetCategory(Document): for fieldname in account_type_map.keys(): if d.get(fieldname): selected_account = d.get(fieldname) - key_to_match = account_type_map[fieldname].keys()[0] + key_to_match = account_type_map.get(fieldname).keys()[0] # acount_type or root_type selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) expected_key_type = account_type_map[fieldname][key_to_match] From ae8a0940f526bc06ade2858a5abc6c2915820689 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 11 Apr 2020 18:10:59 +0530 Subject: [PATCH 007/178] fix: cannot iterate over dict_keys --- erpnext/assets/doctype/asset_category/asset_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index d4ea2504944..2178b7d50f3 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -30,7 +30,7 @@ class AssetCategory(Document): for fieldname in account_type_map.keys(): if d.get(fieldname): selected_account = d.get(fieldname) - key_to_match = account_type_map.get(fieldname).keys()[0] # acount_type or root_type + key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) expected_key_type = account_type_map[fieldname][key_to_match] From 25c26b5904d54cf37b0bc0f2913732da42b0b449 Mon Sep 17 00:00:00 2001 From: vishdha Date: Mon, 13 Apr 2020 08:22:34 +0530 Subject: [PATCH 008/178] fix: Chart of account importer UX improved --- erpnext/accounts/doctype/account/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 0a72d4fa4e6..bd013dc60e3 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -89,7 +89,7 @@ class Account(NestedSet): throw(_("Root cannot be edited."), RootNotEditable) if not self.parent_account and not self.is_group: - frappe.throw(_("Root Account must be a group")) + frappe.throw(_("Root Account must be a group for {0}".format(self.name))) def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies From af33f715626020ae7fa327ccaeda02b9425c8925 Mon Sep 17 00:00:00 2001 From: vishdha Date: Mon, 13 Apr 2020 18:43:14 +0530 Subject: [PATCH 009/178] fix: message bold --- erpnext/accounts/doctype/account/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index bd013dc60e3..9e29ce6748f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -89,7 +89,7 @@ class Account(NestedSet): throw(_("Root cannot be edited."), RootNotEditable) if not self.parent_account and not self.is_group: - frappe.throw(_("Root Account must be a group for {0}".format(self.name))) + frappe.throw(_("Root Account must be a group for {0}".format(frappe.bold(self.name)))) def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies From 8e1201d31e42dad7c973c636c1bc43efc4cde1cd Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 15 Apr 2020 21:19:04 +0530 Subject: [PATCH 010/178] fix: Fetch Material Requests with type Issue as well in Stock Entry via Get Items from --- erpnext/stock/doctype/stock_entry/stock_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index b54233f4585..c54e3cdfb87 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -220,8 +220,8 @@ frappe.ui.form.on('Stock Entry', { }, get_query_filters: { docstatus: 1, - material_request_type: "Material Transfer", - status: ['!=', 'Transferred'] + material_request_type: ["in", ["Material Transfer", "Material Issue"]], + status: ["not in", ["Transferred", "Issued"]] } }) }, __("Get items from")); From 773eb6ce68efb5f1f7d3e66bb7e0e35958eee94b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 15 Apr 2020 22:08:04 +0530 Subject: [PATCH 011/178] fix: pos not accessible without default customer --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b8c45aba1f5..5218b740edd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -432,11 +432,12 @@ class SalesInvoice(SellingController): if pos.get("company_address"): self.company_address = pos.get("company_address") - customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) - - customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') - - selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + if self.customer: + customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) + customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') + selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + else: + selling_price_list = pos.get('selling_price_list') if selling_price_list: self.set('selling_price_list', selling_price_list) From 4879858657c8429ec6e66076039ad352ad7fddea Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 15 Apr 2020 22:08:12 +0530 Subject: [PATCH 012/178] fix: warehouse unset when cannot find item warehouse * cannot set delivery date when all items gets deleted and new are added --- erpnext/controllers/accounts_controller.py | 24 ++++---- erpnext/public/js/utils.js | 69 +++++++++++++--------- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c17927a35c0..95fe6a2f3fa 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1123,36 +1123,39 @@ def get_supplier_block_status(party_name): } return info -def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): +def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values """ p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname) - item = frappe.get_doc("Item", item_code) + item = frappe.get_doc("Item", trans_item.get('item_code')) child_item.item_code = item.item_code child_item.item_name = item.item_name child_item.description = item.description - child_item.reqd_by_date = p_doc.delivery_date + child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 + child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) + if not child_item.warehouse: + frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") + .format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) return child_item -def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): +def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Purchase Order Item child item containing the default values """ p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname) - item = frappe.get_doc("Item", item_code) + item = frappe.get_doc("Item", trans_item.get('item_code')) child_item.item_code = item.item_code child_item.item_name = item.item_name child_item.description = item.description - child_item.schedule_date = p_doc.schedule_date + child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 + child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item @@ -1196,9 +1199,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if not d.get("docname"): new_child_flag = True if parent_doctype == "Sales Order": - child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) + child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) if parent_doctype == "Purchase Order": - child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) + child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) else: child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")): @@ -1243,6 +1246,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil child_item.flags.ignore_validate_update_after_submit = True if new_child_flag: + parent.load_from_db() child_item.idx = len(parent.items) + 1 child_item.insert() else: diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 0a352648304..224f4f5525d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -436,6 +436,44 @@ erpnext.utils.update_child_items = function(opts) { const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; this.data = []; + const fields = [{ + fieldtype:'Data', + fieldname:"docname", + read_only: 1, + hidden: 1, + }, { + fieldtype:'Link', + fieldname:"item_code", + options: 'Item', + in_list_view: 1, + read_only: 0, + disabled: 0, + label: __('Item Code') + }, { + fieldtype:'Float', + fieldname:"qty", + default: 0, + read_only: 0, + in_list_view: 1, + label: __('Qty') + }, { + fieldtype:'Currency', + fieldname:"rate", + default: 0, + read_only: 0, + in_list_view: 1, + label: __('Rate') + }]; + + if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { + fields.splice(2, 0, { + fieldtype: 'Date', + fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date", + in_list_view: 1, + label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date") + }) + } + const dialog = new frappe.ui.Dialog({ title: __("Update Items"), fields: [ @@ -450,34 +488,7 @@ erpnext.utils.update_child_items = function(opts) { get_data: () => { return this.data; }, - fields: [{ - fieldtype:'Data', - fieldname:"docname", - read_only: 1, - hidden: 1, - }, { - fieldtype:'Link', - fieldname:"item_code", - options: 'Item', - in_list_view: 1, - read_only: 0, - disabled: 0, - label: __('Item Code') - }, { - fieldtype:'Float', - fieldname:"qty", - default: 0, - read_only: 0, - in_list_view: 1, - label: __('Qty') - }, { - fieldtype:'Currency', - fieldname:"rate", - default: 0, - read_only: 0, - in_list_view: 1, - label: __('Rate') - }] + fields: fields }, ], primary_action: function() { @@ -506,6 +517,8 @@ erpnext.utils.update_child_items = function(opts) { "docname": d.name, "name": d.name, "item_code": d.item_code, + "delivery_date": d.delivery_date, + "schedule_date": d.schedule_date, "qty": d.qty, "rate": d.rate, }); From c43e759b87f5a7891df902c5131837a3c6151a88 Mon Sep 17 00:00:00 2001 From: vishdha Date: Thu, 16 Apr 2020 11:09:54 +0530 Subject: [PATCH 013/178] fix: fix Transalation --- erpnext/accounts/doctype/account/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 9e29ce6748f..c6de6410ebc 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -89,7 +89,7 @@ class Account(NestedSet): throw(_("Root cannot be edited."), RootNotEditable) if not self.parent_account and not self.is_group: - frappe.throw(_("Root Account must be a group for {0}".format(frappe.bold(self.name)))) + frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) def validate_root_company_and_sync_account_to_children(self): # ignore validation while creating new compnay or while syncing to child companies From 4dca8067375084f86450ebdb73d7428ece77cc7b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 16 Apr 2020 14:27:25 +0530 Subject: [PATCH 014/178] fix: requested qty calculation fix for UOM --- .../material_request/material_request.py | 5 ++- .../material_request/test_material_request.py | 32 +++++++++++++++++ erpnext/stock/stock_balance.py | 36 +++++++++++-------- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index c11f59fd81e..dcc5c5bac58 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -174,12 +174,11 @@ class MaterialRequest(BuyingController): frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) - target_ref_field = 'qty' if self.material_request_type == "Manufacture" else 'stock_qty' self._update_percent_field({ "target_dt": "Material Request Item", "target_parent_dt": self.doctype, "target_parent_field": "per_ordered", - "target_ref_field": target_ref_field, + "target_ref_field": "stock_qty", "target_field": "ordered_qty", "name": self.name, }, update_modified) @@ -482,7 +481,7 @@ def raise_work_orders(material_request): default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") for d in mr.items: - if (d.qty - d.ordered_qty) >0: + if (d.stock_qty - d.ordered_qty) > 0: if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}): wo_order = frappe.new_doc("Work Order") wo_order.update({ diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index b925aedd1ac..30c47c3671c 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -563,6 +563,36 @@ class TestMaterialRequest(unittest.TestCase): item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] self.assertEqual(requested_qty, new_requested_qty) + def test_requested_qty_multi_uom(self): + existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + + mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', + uom="_Test UOM 1", conversion_factor=12) + + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + + self.assertEqual(requested_qty, existing_requested_qty + 120) + + work_order = raise_work_orders(mr.name) + wo = frappe.get_doc("Work Order", work_order[0]) + wo.qty = 50 + wo.wip_warehouse = "_Test Warehouse 1 - _TC" + wo.submit() + + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + self.assertEqual(requested_qty, existing_requested_qty + 70) + + wo.cancel() + + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + self.assertEqual(requested_qty, existing_requested_qty + 120) + + mr.reload() + mr.cancel() + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + self.assertEqual(requested_qty, existing_requested_qty) + + def test_multi_uom_for_purchase(self): from erpnext.stock.doctype.material_request.material_request import make_purchase_order @@ -633,6 +663,8 @@ def make_material_request(**args): mr.append("items", { "item_code": args.item_code or "_Test Item", "qty": args.qty or 10, + "uom": args.uom or "_Test UOM", + "conversion_factor": args.conversion_factor or 1, "schedule_date": args.schedule_date or today(), "warehouse": args.warehouse or "_Test Warehouse - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC" diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 2bdb04ed2c9..d9434e3fe78 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -113,24 +113,32 @@ def get_reserved_qty(item_code, warehouse): return flt(reserved_qty[0][0]) if reserved_qty else 0 def get_indented_qty(item_code, warehouse): - inward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor) - from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr - where mr_item.item_code=%s and mr_item.warehouse=%s - and mr.material_request_type in ('Purchase', 'Manufacture') - and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name - and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse)) - - outward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor) + # Ordered Qty is maintained in purchase UOM + requested_qty_for_purchase_and_manufacture = frappe.db.sql(""" + select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s - and mr.material_request_type in ('Material Issue', 'Material Transfer') - and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name - and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse)) + and mr.material_request_type in ('Purchase', 'Manufacture') + and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name + and mr.status!='Stopped' and mr.docstatus=1 + """, (item_code, warehouse)) + requested_qty_for_purchase_and_manufacture = flt(requested_qty_for_purchase_and_manufacture[0][0]) \ + if requested_qty_for_purchase_and_manufacture else 0 - inward_qty, outward_qty = flt(inward_qty[0][0]) if inward_qty else 0, flt(outward_qty[0][0]) if outward_qty else 0 - indented_qty = inward_qty - outward_qty + requested_qty_for_issue_and_transfer = frappe.db.sql(""" + select sum(mr_item.stock_qty - mr_item.ordered_qty) + from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr + where mr_item.item_code=%s and mr_item.warehouse=%s + and mr.material_request_type in ('Material Issue', 'Material Transfer') + and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name + and mr.status!='Stopped' and mr.docstatus=1 + """, (item_code, warehouse)) + requested_qty_for_issue_and_transfer = flt(requested_qty_for_issue_and_transfer[0][0]) \ + if requested_qty_for_issue_and_transfer else 0 - return indented_qty + requested_qty = requested_qty_for_purchase_and_manufacture - requested_qty_for_issue_and_transfer + + return requested_qty def get_ordered_qty(item_code, warehouse): ordered_qty = frappe.db.sql(""" From 2b6942d9ce7f4c9afa5ea29a5e9da265bba42efb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 16 Apr 2020 11:24:43 +0530 Subject: [PATCH 015/178] fix: Made release date mandatory --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index c5c54837a7b..9292b633fc3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -174,7 +174,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ read_only: 0, fieldtype:'Date', label: __('Release Date'), - default: me.frm.doc.release_date + default: me.frm.doc.release_date, + reqd: 1 }, { fieldname: 'hold_comment', From 90adf076f3c1a2b8a74d020346204550c26a4c6f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 16 Apr 2020 11:25:15 +0530 Subject: [PATCH 016/178] fix: Made received qty readonly and no-copy --- .../stock/doctype/material_request/material_request.py | 2 +- .../material_request_item/material_request_item.json | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index dcc5c5bac58..5c6718bb4d5 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -513,7 +513,7 @@ def raise_work_orders(material_request): msgprint(_("The following Work Orders were created:") + '\n' + new_line_sep(message)) if errors: - frappe.throw(_("Productions Orders cannot be raised for:") + '\n' + new_line_sep(errors)) + frappe.throw(_("Work Order cannot be created for following reason:") + '\n' + new_line_sep(errors)) return work_orders diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 9d1dafb136b..d4408115bbe 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-02-22 01:28:02", "doctype": "DocType", @@ -374,7 +373,10 @@ { "fieldname": "received_qty", "fieldtype": "Float", - "label": "Received Quantity" + "label": "Received Quantity", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 }, { "collapsible": 1, @@ -410,8 +412,7 @@ ], "idx": 1, "istable": 1, - "links": [], - "modified": "2020-02-25 03:09:10.698967", + "modified": "2020-04-16 09:00:00.992835", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", From f390b8062cbef1ee03ebc7aee1b7b7c533fdf3b8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 16 Apr 2020 16:16:09 +0530 Subject: [PATCH 017/178] fix: incorrect transalation --- erpnext/assets/doctype/asset_category/asset_category.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 2178b7d50f3..9bf4df423c2 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -35,8 +35,8 @@ class AssetCategory(Document): expected_key_type = account_type_map[fieldname][key_to_match] if selected_key_type != expected_key_type: - frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account." - .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type))), + frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), title=_("Invalid Account")) From 45d454b14dde2657048fcc40089e3d31ce2fd4a1 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 16 Apr 2020 16:21:54 +0530 Subject: [PATCH 018/178] fix: serial and batch selection from delivery note bug fix (#21292) --- erpnext/public/js/controllers/transaction.js | 4 ++++ erpnext/selling/sales_common.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5312bc06ecc..a326fcc299a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -538,6 +538,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!d[k]) d[k] = v; }); + if (d.has_batch_no && d.has_serial_no) { + d.batch_no = undefined; + } + erpnext.show_serial_batch_selector(me.frm, d, (item) => { me.frm.script_manager.trigger('qty', item.doctype, item.name); if (!me.frm.doc.set_warehouse) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index af100692c6f..095b7c3dffa 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -228,9 +228,15 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ warehouse: function(doc, cdt, cdn) { var me = this; var item = frappe.get_doc(cdt, cdn); + + if (item.serial_no && item.qty === item.serial_no.split(`\n`).length) { + return; + } + if (item.serial_no && !item.batch_no) { item.serial_no = null; } + var has_batch_no; frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => { has_batch_no = r && r.has_batch_no; From 61fc2d3263ebde3ac64bfb84b4b539472a49d0eb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 16 Apr 2020 23:57:49 +0530 Subject: [PATCH 019/178] fix: job card time issue (#21306) --- erpnext/manufacturing/doctype/job_card/job_card.js | 13 +++++++++++++ erpnext/manufacturing/doctype/job_card/job_card.py | 12 ++++++++---- .../manufacturing/doctype/work_order/work_order.js | 2 ++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 9ee5e80f88d..1b8e4d59d7b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -109,6 +109,14 @@ frappe.ui.form.on('Job Card', { }); }, + validate: function(frm) { + if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { + frm.set_value('started_time' , ''); + frm.set_value('job_started', 0); + frm.set_value('current_time' , 0); + } + }, + make_dashboard: function(frm) { if(frm.doc.__islocal) return; @@ -209,5 +217,10 @@ frappe.ui.form.on('Job Card', { frappe.ui.form.on('Job Card Time Log', { completed_qty: function(frm) { frm.events.set_total_completed_qty(frm); + }, + + to_time: function(frm) { + frm.set_value('job_started', 0); + frm.set_value('started_time', ''); } }) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 7c648c79014..4c1ab7479fb 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff +from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff, get_link_to_form from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document @@ -90,11 +90,15 @@ class JobCard(Document): def validate_job_card(self): if not self.time_logs: - frappe.throw(_("Time logs are required for job card {0}").format(self.name)) + frappe.throw(_("Time logs are required for {0} {1}") + .format(frappe.bold("Job Card"), get_link_to_form("Job Card", self.name))) if self.for_quantity and self.total_completed_qty != self.for_quantity: - frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})" - .format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity)))) + total_completed_qty = frappe.bold(_("Total Completed Qty")) + qty_to_manufacture = frappe.bold(_("Qty to Manufacture")) + + frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})" + .format(total_completed_qty, frappe.bold(self.total_completed_qty), qty_to_manufacture,frappe.bold(self.for_quantity)))) def update_work_order(self): if not self.work_order: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 1989c10c394..2d8d75d1f94 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -232,6 +232,8 @@ frappe.ui.form.on("Work Order", { }); }, __("Job Card"), __("Create")); + dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide(); + var pending_qty = 0; frm.doc.operations.forEach(data => { if(data.completed_qty != frm.doc.qty) { From da83af121370d9f1e1540bcae42e96bcfea9e817 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 17 Apr 2020 00:54:20 +0530 Subject: [PATCH 020/178] fix: job card timer issue --- .../doctype/job_card/job_card.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 1b8e4d59d7b..bbce6f55d81 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -86,7 +86,9 @@ frappe.ui.form.on('Job Card', { frm.set_value('current_time' , 0); } - frm.save(); + frm.save("Save", () => {}, "", () => { + frm.doc.time_logs.pop(-1); + }); }, complete_job: function(frm, completed_time, completed_qty) { @@ -111,12 +113,22 @@ frappe.ui.form.on('Job Card', { validate: function(frm) { if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { - frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); + frm.trigger("reset_timer"); } }, + employee: function(frm) { + if (frm.doc.job_started && !frm.doc.current_time) { + frm.trigger("reset_timer"); + } + }, + + reset_timer: function(frm) { + frm.set_value('started_time' , ''); + frm.set_value('job_started', 0); + frm.set_value('current_time' , 0); + }, + make_dashboard: function(frm) { if(frm.doc.__islocal) return; From 3b2aa5ead3a0a220b72c8f2943d904bfb2df8e5e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Apr 2020 10:52:23 +0530 Subject: [PATCH 021/178] fix: requested qty for customer provided item and rate for sales (#21300) * fix: requested qty for customer provided item and rate for sales * fix: requested qty for material transfer * fix: customer provided item can be sales item * fix: requested qty test cases --- .../sales_invoice/test_sales_invoice.py | 10 -- erpnext/controllers/stock_controller.py | 3 - .../delivery_note/test_delivery_note.py | 9 -- .../material_request/test_material_request.py | 124 +++++++----------- erpnext/stock/stock_balance.py | 18 ++- 5 files changed, 53 insertions(+), 111 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ad0e9572497..ced829ee6ee 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1868,16 +1868,6 @@ class TestSalesInvoice(unittest.TestCase): item.taxes = [] item.save() - def test_customer_provided_parts_si(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - si = create_sales_invoice(item_code='CUST-0987', rate=0) - self.assertEqual(si.get("items")[0].allow_zero_valuation_rate, 1) - self.assertEqual(si.get("items")[0].amount, 0) - - # test if Sales Invoice with rate is allowed - si2 = create_sales_invoice(item_code='CUST-0987', do_not_save=True) - self.assertRaises(frappe.ValidationError, si2.save) - def create_sales_invoice(**args): si = frappe.new_doc("Sales Invoice") args = frappe._dict(args) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 5c09068a035..f5c42b49927 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -374,9 +374,6 @@ class StockController(AccountsController): # Customer Provided parts will have zero valuation rate if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 - if d.parenttype in ["Delivery Note", "Sales Invoice"] and d.rate: - frappe.throw(_("Row #{0}: {1} cannot have {2} as it is a Customer Provided Item") - .format(d.idx, frappe.bold(d.item_code), frappe.bold("Rate"))) def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None, company=None): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 47a72b21a69..d7a93fb6917 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -434,15 +434,6 @@ class TestDeliveryNote(unittest.TestCase): update_delivery_note_status(dn.name, "Closed") self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed") - def test_customer_provided_parts_dn(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - dn = create_delivery_note(item_code='CUST-0987', rate=0) - self.assertEqual(dn.get("items")[0].allow_zero_valuation_rate, 1) - - # test if Delivery Note with rate is allowed against Customer Provided Item - dn2 = create_delivery_note(item_code='CUST-0987', do_not_save=True) - self.assertRaises(frappe.ValidationError, dn2.save) - def test_dn_billing_status_case1(self): # SO -> DN -> SI so = make_sales_order() diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 30c47c3671c..19924b16363 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -7,7 +7,8 @@ from __future__ import unicode_literals import frappe, unittest, erpnext from frappe.utils import flt, today -from erpnext.stock.doctype.material_request.material_request import raise_work_orders +from erpnext.stock.doctype.material_request.material_request \ + import raise_work_orders, make_stock_entry, make_purchase_order, make_supplier_quotation from erpnext.stock.doctype.item.test_item import create_item class TestMaterialRequest(unittest.TestCase): @@ -15,8 +16,6 @@ class TestMaterialRequest(unittest.TestCase): erpnext.set_perpetual_inventory(0) def test_make_purchase_order(self): - from erpnext.stock.doctype.material_request.material_request import make_purchase_order - mr = frappe.copy_doc(test_records[0]).insert() self.assertRaises(frappe.ValidationError, make_purchase_order, @@ -30,8 +29,6 @@ class TestMaterialRequest(unittest.TestCase): self.assertEqual(len(po.get("items")), len(mr.get("items"))) def test_make_supplier_quotation(self): - from erpnext.stock.doctype.material_request.material_request import make_supplier_quotation - mr = frappe.copy_doc(test_records[0]).insert() self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name) @@ -45,12 +42,9 @@ class TestMaterialRequest(unittest.TestCase): def test_make_stock_entry(self): - from erpnext.stock.doctype.material_request.material_request import make_stock_entry - mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_stock_entry, - mr.name) + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.material_request_type = "Material Transfer" @@ -62,40 +56,40 @@ class TestMaterialRequest(unittest.TestCase): def _insert_stock_entry(self, qty1, qty2, warehouse = None ): se = frappe.get_doc({ - "company": "_Test Company", - "doctype": "Stock Entry", - "posting_date": "2013-03-01", - "posting_time": "00:00:00", - "purpose": "Material Receipt", - "items": [ - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "items", - "basic_rate": 100, - "qty": qty1, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty1, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - }, - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 200", - "parentfield": "items", - "basic_rate": 100, - "qty": qty2, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty2, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - } - ] - }) + "company": "_Test Company", + "doctype": "Stock Entry", + "posting_date": "2013-03-01", + "posting_time": "00:00:00", + "purpose": "Material Receipt", + "items": [ + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "items", + "basic_rate": 100, + "qty": qty1, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty1, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC" + }, + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 200", + "parentfield": "items", + "basic_rate": 100, + "qty": qty2, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty2, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC" + } + ] + }) se.set_stock_entry_type() se.insert() @@ -198,14 +192,7 @@ class TestMaterialRequest(unittest.TestCase): mr.insert() mr.submit() - # check if per complete is None - mr.load_from_db() - self.assertEqual(mr.per_ordered, 0) - self.assertEqual(mr.get("items")[0].ordered_qty, 0) - self.assertEqual(mr.get("items")[1].ordered_qty, 0) - # map a purchase order - from erpnext.stock.doctype.material_request.material_request import make_purchase_order po_doc = make_purchase_order(mr.name) po_doc.supplier = "_Test Supplier" po_doc.transaction_date = "2013-07-07" @@ -276,10 +263,8 @@ class TestMaterialRequest(unittest.TestCase): current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") - self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0) - self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0) - - from erpnext.stock.doctype.material_request.material_request import make_stock_entry + self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) + self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) # map a stock entry se_doc = make_stock_entry(mr.name) @@ -331,8 +316,8 @@ class TestMaterialRequest(unittest.TestCase): current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") - self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 27.0) - self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 1.5) + self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) + self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) # check if per complete is as expected for Stock Entry cancelled se.cancel() @@ -344,8 +329,8 @@ class TestMaterialRequest(unittest.TestCase): current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") - self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0) - self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0) + self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) + self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_completed_qty_for_over_transfer(self): existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") @@ -357,14 +342,7 @@ class TestMaterialRequest(unittest.TestCase): mr.insert() mr.submit() - # check if per complete is None - mr.load_from_db() - self.assertEqual(mr.per_ordered, 0) - self.assertEqual(mr.get("items")[0].ordered_qty, 0) - self.assertEqual(mr.get("items")[1].ordered_qty, 0) - # map a stock entry - from erpnext.stock.doctype.material_request.material_request import make_stock_entry se_doc = make_stock_entry(mr.name) se_doc.update({ @@ -425,8 +403,8 @@ class TestMaterialRequest(unittest.TestCase): current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") - self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0) - self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0) + self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) + self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_incorrect_mapping_of_stock_entry(self): # submit material request of type Transfer @@ -435,9 +413,6 @@ class TestMaterialRequest(unittest.TestCase): mr.insert() mr.submit() - # map a stock entry - from erpnext.stock.doctype.material_request.material_request import make_stock_entry - se_doc = make_stock_entry(mr.name) se_doc.update({ "posting_date": "2013-03-01", @@ -468,8 +443,6 @@ class TestMaterialRequest(unittest.TestCase): mr.insert() mr.submit() - # map a stock entry - from erpnext.stock.doctype.material_request.material_request import make_stock_entry se_doc = make_stock_entry(mr.name) self.assertEqual(se_doc.get("items")[0].s_warehouse, "_Test Warehouse - _TC") @@ -483,8 +456,6 @@ class TestMaterialRequest(unittest.TestCase): return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")) def test_make_stock_entry_for_material_issue(self): - from erpnext.stock.doctype.material_request.material_request import make_stock_entry - mr = frappe.copy_doc(test_records[0]).insert() self.assertRaises(frappe.ValidationError, make_stock_entry, @@ -503,8 +474,6 @@ class TestMaterialRequest(unittest.TestCase): return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100", "warehouse": "_Test Warehouse - _TC"}, "indented_qty")) - from erpnext.stock.doctype.material_request.material_request import make_stock_entry - existing_requested_qty = _get_requested_qty() mr = frappe.copy_doc(test_records[0]) @@ -594,8 +563,6 @@ class TestMaterialRequest(unittest.TestCase): def test_multi_uom_for_purchase(self): - from erpnext.stock.doctype.material_request.material_request import make_purchase_order - mr = frappe.copy_doc(test_records[0]) mr.material_request_type = 'Purchase' item = mr.items[0] @@ -637,7 +604,6 @@ class TestMaterialRequest(unittest.TestCase): self.assertEqual(mr.per_ordered, 100) def test_customer_provided_parts_mr(self): - from erpnext.stock.doctype.material_request.material_request import make_stock_entry create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC") diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index d9434e3fe78..56973153609 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -113,30 +113,28 @@ def get_reserved_qty(item_code, warehouse): return flt(reserved_qty[0][0]) if reserved_qty else 0 def get_indented_qty(item_code, warehouse): - # Ordered Qty is maintained in purchase UOM - requested_qty_for_purchase_and_manufacture = frappe.db.sql(""" + # Ordered Qty is always maintained in stock UOM + inward_qty = frappe.db.sql(""" select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s - and mr.material_request_type in ('Purchase', 'Manufacture') + and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer') and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 """, (item_code, warehouse)) - requested_qty_for_purchase_and_manufacture = flt(requested_qty_for_purchase_and_manufacture[0][0]) \ - if requested_qty_for_purchase_and_manufacture else 0 + inward_qty = flt(inward_qty[0][0]) if inward_qty else 0 - requested_qty_for_issue_and_transfer = frappe.db.sql(""" + outward_qty = frappe.db.sql(""" select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s - and mr.material_request_type in ('Material Issue', 'Material Transfer') + and mr.material_request_type = 'Material Issue' and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 """, (item_code, warehouse)) - requested_qty_for_issue_and_transfer = flt(requested_qty_for_issue_and_transfer[0][0]) \ - if requested_qty_for_issue_and_transfer else 0 + outward_qty = flt(outward_qty[0][0]) if outward_qty else 0 - requested_qty = requested_qty_for_purchase_and_manufacture - requested_qty_for_issue_and_transfer + requested_qty = inward_qty - outward_qty return requested_qty From 44f0c077ff5e1a6ec9f52513b6726bf02709bbf6 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Fri, 17 Apr 2020 13:15:15 +0530 Subject: [PATCH 022/178] fix(patch): reload 'Import Supplier Invoice' doc --- erpnext/patches/v12_0/set_permission_einvoicing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/set_permission_einvoicing.py b/erpnext/patches/v12_0/set_permission_einvoicing.py index 1095c8c43e7..e2235105f94 100644 --- a/erpnext/patches/v12_0/set_permission_einvoicing.py +++ b/erpnext/patches/v12_0/set_permission_einvoicing.py @@ -10,6 +10,8 @@ def execute(): make_custom_fields() + frappe.reload_doc("regional", "doctype", "import_supplier_invoice") + add_permission('Import Supplier Invoice', 'Accounts Manager', 0) update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'write', 1) - update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1) \ No newline at end of file + update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1) From b33f82d4e856870e1fd0541245cfd8cb5c6fe309 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 17 Apr 2020 19:52:47 +0530 Subject: [PATCH 023/178] fix: add label to gl entry --- erpnext/accounts/report/general_ledger/general_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 6f74e8734e3..d3f3882299b 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -365,6 +365,7 @@ def get_columns(filters): columns = [ { + "label": _("GL Entry"), "fieldname": "gl_entry", "fieldtype": "Link", "options": "GL Entry", From fcd754822069fe25f5546dbb0ca862b313e5834a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 17 Mar 2020 11:47:11 +0530 Subject: [PATCH 024/178] fix: Pick List Enhancements --- .../v12_0/set_updated_purpose_in_pick_list.py | 10 ++++ .../doctype/sales_order/sales_order.py | 2 +- erpnext/stock/doctype/pick_list/pick_list.js | 49 ++++++++++----- .../stock/doctype/pick_list/pick_list.json | 9 ++- erpnext/stock/doctype/pick_list/pick_list.py | 59 ++++++++++++++----- .../pick_list_item/pick_list_item.json | 15 ++++- 6 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py diff --git a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py new file mode 100644 index 00000000000..42491882c4e --- /dev/null +++ b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py @@ -0,0 +1,10 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery' + WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """) \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ef2d19ac546..05e4aa892b0 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1036,7 +1036,7 @@ def create_pick_list(source_name, target_doc=None): }, }, target_doc) - doc.purpose = 'Delivery against Sales Order' + doc.purpose = 'Delivery' doc.set_item_locations() diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 278971125f0..f4fd0a202fe 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -38,11 +38,11 @@ frappe.ui.form.on('Pick List', { }; }); }, - get_item_locations: (frm) => { - if (!frm.doc.locations || !frm.doc.locations.length) { - frappe.msgprint(__('First add items in the Item Locations table')); + get_item_locations: (frm, save=false) => { + if (!(frm.doc.locations && frm.doc.locations.length)) { + frappe.msgprint(__('Add items in the Item Locations table')); } else { - frm.call('set_item_locations'); + frm.call('set_item_locations', {save: save}); } }, refresh: (frm) => { @@ -52,8 +52,13 @@ frappe.ui.form.on('Pick List', { 'pick_list_name': frm.doc.name, 'purpose': frm.doc.purpose }).then(target_document_exists => { + frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1); + if (target_document_exists) return; - if (frm.doc.purpose === 'Delivery against Sales Order') { + + frm.add_custom_button(__('Update Current Stock'), () => frm.trigger('update_pick_list_stock')); + + if (frm.doc.purpose === 'Delivery') { frm.add_custom_button(__('Delivery Note'), () => frm.trigger('create_delivery_note'), __('Create')); } else { frm.add_custom_button(__('Stock Entry'), () => frm.trigger('create_stock_entry'), __('Create')); @@ -101,22 +106,34 @@ frappe.ui.form.on('Pick List', { frm.trigger('add_get_items_button'); }, create_delivery_note: (frm) => { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note', - frm: frm - }); + if (!(frm.doc.locations && frm.doc.locations.length)) { + frappe.msgprint(__('Add items in the Item Locations table')); + } else { + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note', + frm: frm + }); + } + }, create_stock_entry: (frm) => { - frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', { - 'pick_list': frm.doc, - }).then(stock_entry => { - frappe.model.sync(stock_entry); - frappe.set_route("Form", 'Stock Entry', stock_entry.name); - }); + if (!(frm.doc.locations && frm.doc.locations.length)) { + frappe.msgprint(__('Add items in the Item Locations table')); + } else { + frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', { + 'pick_list': frm.doc, + }).then(stock_entry => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", 'Stock Entry', stock_entry.name); + }); + } + }, + update_pick_list_stock: (frm) => { + frm.events.get_item_locations(frm, true); }, add_get_items_button: (frm) => { let purpose = frm.doc.purpose; - if (purpose != 'Delivery against Sales Order' || frm.doc.docstatus !== 0) return; + if (purpose != 'Delivery' || frm.doc.docstatus !== 0) return; let get_query_filters = { docstatus: 1, per_delivered: ['<', 100], diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 8d5ef3d12ab..c01388dcd21 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2019-07-11 16:03:13.681045", "doctype": "DocType", @@ -44,7 +45,7 @@ "options": "Warehouse" }, { - "depends_on": "eval:doc.purpose==='Delivery against Sales Order'", + "depends_on": "eval:doc.purpose==='Delivery'", "fieldname": "customer", "fieldtype": "Link", "in_list_view": 1, @@ -59,6 +60,7 @@ "options": "Work Order" }, { + "allow_on_submit": 1, "fieldname": "locations", "fieldtype": "Table", "label": "Item Locations", @@ -86,7 +88,7 @@ "fieldname": "purpose", "fieldtype": "Select", "label": "Purpose", - "options": "Material Transfer for Manufacture\nMaterial Transfer\nDelivery against Sales Order" + "options": "Material Transfer for Manufacture\nMaterial Transfer\nDelivery" }, { "depends_on": "eval:['Material Transfer', 'Material Issue'].includes(doc.purpose)", @@ -111,7 +113,8 @@ } ], "is_submittable": 1, - "modified": "2019-08-29 21:10:11.572387", + "links": [], + "modified": "2020-03-17 11:38:41.932875", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 7ed998f979a..59e64f1558e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -29,7 +29,7 @@ class PickList(Document): frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity') .format(frappe.bold(item.item_code), frappe.bold(item.idx))) - def set_item_locations(self): + def set_item_locations(self, save=False): items = self.aggregate_item_qty() self.item_location_map = frappe._dict() @@ -43,7 +43,7 @@ class PickList(Document): item_code = item_doc.item_code self.item_location_map.setdefault(item_code, - get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code))) + get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company)) locations = get_items_with_location_and_quantity(item_doc, self.item_location_map) @@ -59,12 +59,17 @@ class PickList(Document): location.update(row) self.append('locations', location) + if save: + self.save() + def aggregate_item_qty(self): locations = self.get('locations') self.item_count_map = {} # aggregate qty for same item item_map = OrderedDict() for item in locations: + if not item.item_code: + frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) item_code = item.item_code reference = item.sales_order_item or item.material_request_item key = (item_code, item.uom, reference) @@ -130,14 +135,14 @@ def get_items_with_location_and_quantity(item_doc, item_location_map): item_location_map[item_doc.item_code] = available_locations return locations -def get_available_item_locations(item_code, from_warehouses, required_qty): +def get_available_item_locations(item_code, from_warehouses, required_qty, company): locations = [] if frappe.get_cached_value('Item', item_code, 'has_serial_no'): - locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty) + locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company) elif frappe.get_cached_value('Item', item_code, 'has_batch_no'): - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty) + locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) else: - locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty) + locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company) total_qty_available = sum(location.get('qty') for location in locations) @@ -150,9 +155,10 @@ def get_available_item_locations(item_code, from_warehouses, required_qty): return locations -def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty): +def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company): filters = frappe._dict({ 'item_code': item_code, + 'company': company, 'warehouse': ['!=', ''] }) @@ -180,7 +186,7 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses, return locations -def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty): +def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company): warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else '' batch_locations = frappe.db.sql(""" SELECT @@ -192,6 +198,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re WHERE sle.batch_no = batch.name and sle.`item_code`=%(item_code)s + and sle.`company` = %(company)s and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY @@ -202,16 +209,20 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` """.format(warehouse_condition=warehouse_condition), { #nosec 'item_code': item_code, + 'company': company, 'today': today(), 'warehouses': from_warehouses }, as_dict=1) return batch_locations -def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty): +def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): # gets all items available in different warehouses + warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")] + filters = frappe._dict({ 'item_code': item_code, + 'warehouse': ['in', warehouses], 'actual_qty': ['>', 0] }) @@ -230,7 +241,7 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ @frappe.whitelist() def create_delivery_note(source_name, target_doc=None): pick_list = frappe.get_doc('Pick List', source_name) - sales_orders = [d.sales_order for d in pick_list.locations] + sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order] sales_orders = set(sales_orders) delivery_note = None @@ -238,6 +249,10 @@ def create_delivery_note(source_name, target_doc=None): delivery_note = create_delivery_note_from_sales_order(sales_order, delivery_note, skip_item_mapping=True) + # map rows without sales orders as well + if not delivery_note: + delivery_note = frappe.new_doc("Delivery Note") + item_table_mapper = { 'doctype': 'Delivery Note Item', 'field_map': { @@ -248,9 +263,25 @@ def create_delivery_note(source_name, target_doc=None): 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 } + item_table_mapper_without_so = { + 'doctype': 'Delivery Note Item', + 'field_map': { + 'rate': 'rate', + 'name': 'name', + 'parent': '', + } + } + for location in pick_list.locations: - sales_order_item = frappe.get_cached_doc('Sales Order Item', location.sales_order_item) - dn_item = map_child_doc(sales_order_item, delivery_note, item_table_mapper) + if location.sales_order_item: + sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item}) + else: + sales_order_item = None + + source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \ + else [location, item_table_mapper_without_so] + + dn_item = map_child_doc(source_doc, delivery_note, table_mapper) if dn_item: dn_item.warehouse = location.warehouse @@ -258,7 +289,7 @@ def create_delivery_note(source_name, target_doc=None): dn_item.batch_no = location.batch_no dn_item.serial_no = location.serial_no - update_delivery_note_item(sales_order_item, dn_item, delivery_note) + update_delivery_note_item(source_doc, dn_item, delivery_note) set_delivery_note_missing_values(delivery_note) @@ -318,7 +349,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte @frappe.whitelist() def target_document_exists(pick_list_name, purpose): - if purpose == 'Delivery against Sales Order': + if purpose == 'Delivery': return frappe.db.exists('Delivery Note', { 'pick_list': pick_list_name }) diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index c7a35df51f5..71fbf9a866a 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-07-11 16:01:22.832885", "doctype": "DocType", "editable_grid": 1, @@ -8,6 +9,7 @@ "item_name", "column_break_2", "description", + "item_group", "section_break_5", "warehouse", "quantity_section", @@ -120,7 +122,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item", - "options": "Item" + "options": "Item", + "reqd": 1 }, { "fieldname": "quantity_section", @@ -166,10 +169,18 @@ "fieldtype": "Data", "label": "Material Request Item", "read_only": 1 + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Data", + "label": "Item Group", + "read_only": 1 } ], "istable": 1, - "modified": "2019-08-29 21:28:39.539007", + "links": [], + "modified": "2020-03-13 19:08:21.995986", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", From 09f95858c01be354b3a31a3849f7c64c6be896f2 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 18 Mar 2020 10:11:23 +0530 Subject: [PATCH 025/178] fix: Added patch to patches.txt --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7cb92d228d9..a7013ff109f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -656,3 +656,4 @@ erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.recalculate_requested_qty_in_bin erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_correct_status_for_expense_claim +erpnext.patches.v12_0.set_updated_purpose_in_pick_list \ No newline at end of file From b9df3793fbd2c6b6b57c281d8ece20296609d3f1 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 2 Apr 2020 13:51:25 +0530 Subject: [PATCH 026/178] fix: Commonified code and added server side validation --- erpnext/stock/doctype/pick_list/pick_list.js | 36 +++++++++----------- erpnext/stock/doctype/pick_list/pick_list.py | 7 ++++ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index f4fd0a202fe..d46b98b461b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -38,13 +38,17 @@ frappe.ui.form.on('Pick List', { }; }); }, - get_item_locations: (frm, save=false) => { + set_item_locations:(frm, save) => { if (!(frm.doc.locations && frm.doc.locations.length)) { frappe.msgprint(__('Add items in the Item Locations table')); } else { frm.call('set_item_locations', {save: save}); } }, + get_item_locations: (frm) => { + // Button on the form + frm.events.set_item_locations(frm, false); + }, refresh: (frm) => { frm.trigger('add_get_items_button'); if (frm.doc.docstatus === 1) { @@ -106,30 +110,22 @@ frappe.ui.form.on('Pick List', { frm.trigger('add_get_items_button'); }, create_delivery_note: (frm) => { - if (!(frm.doc.locations && frm.doc.locations.length)) { - frappe.msgprint(__('Add items in the Item Locations table')); - } else { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note', - frm: frm - }); - } + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.pick_list.pick_list.create_delivery_note', + frm: frm + }); }, create_stock_entry: (frm) => { - if (!(frm.doc.locations && frm.doc.locations.length)) { - frappe.msgprint(__('Add items in the Item Locations table')); - } else { - frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', { - 'pick_list': frm.doc, - }).then(stock_entry => { - frappe.model.sync(stock_entry); - frappe.set_route("Form", 'Stock Entry', stock_entry.name); - }); - } + frappe.xcall('erpnext.stock.doctype.pick_list.pick_list.create_stock_entry', { + 'pick_list': frm.doc, + }).then(stock_entry => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", 'Stock Entry', stock_entry.name); + }); }, update_pick_list_stock: (frm) => { - frm.events.get_item_locations(frm, true); + frm.events.set_item_locations(frm, true); }, add_get_items_button: (frm) => { let purpose = frm.doc.purpose; diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 59e64f1558e..917524929e1 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -90,6 +90,10 @@ class PickList(Document): return item_map.values() +def validate_item_locations(pick_list): + if not pick_list.locations: + frappe.throw(_("Add items in the Item Locations table")) + def get_items_with_location_and_quantity(item_doc, item_location_map): available_locations = item_location_map.get(item_doc.item_code) locations = [] @@ -241,6 +245,8 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ @frappe.whitelist() def create_delivery_note(source_name, target_doc=None): pick_list = frappe.get_doc('Pick List', source_name) + validate_item_locations(pick_list) + sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order] sales_orders = set(sales_orders) @@ -300,6 +306,7 @@ def create_delivery_note(source_name, target_doc=None): @frappe.whitelist() def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) + validate_item_locations(pick_list) if stock_entry_exists(pick_list.get('name')): return frappe.msgprint(_('Stock Entry has been already created against this Pick List')) From 6b7848232c4a72e7db36bb4216b0192d17dd9cc8 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Apr 2020 14:03:50 +0530 Subject: [PATCH 027/178] fix: Use reload_doc in patch --- erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py index 42491882c4e..63ca540a8e2 100644 --- a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py +++ b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py @@ -6,5 +6,6 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doc("stock", "doctype", "pick_list") frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery' WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """) \ No newline at end of file From 64bb9100153441aa6973daa92ab6d97da748f949 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 18 Apr 2020 19:02:07 +0530 Subject: [PATCH 028/178] fix: account name not showing in the gl print --- erpnext/accounts/report/general_ledger/general_ledger.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 9a2205a5791..378fa3791c1 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -2,7 +2,7 @@

{% if (filters.party_name) { %} {%= filters.party_name %} - {% } else if (filters.party) { %} + {% } else if (filters.party && filters.party.length) { %} {%= filters.party %} {% } else if (filters.account) { %} {%= filters.account %} From 19c841eb640b948ac9d1ea851634a8d1f5c54b5d Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 18 Apr 2020 22:14:14 +0530 Subject: [PATCH 029/178] fix: Total amount field ordering in transactions (#21315) --- .../doctype/purchase_invoice/purchase_invoice.json | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 6 ++++-- erpnext/buying/doctype/purchase_order/purchase_order.json | 7 +++++-- erpnext/selling/doctype/sales_order/sales_order.json | 4 ++-- erpnext/stock/doctype/delivery_note/delivery_note.json | 4 ++-- .../stock/doctype/purchase_receipt/purchase_receipt.json | 4 ++-- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 7725994c6b0..c7dcf67a4df 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -73,9 +73,9 @@ "base_total", "base_net_total", "column_break_28", + "total_net_weight", "total", "net_total", - "total_net_weight", "taxes_section", "tax_category", "column_break_49", @@ -1290,7 +1290,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2019-12-30 19:13:49.610538", + "modified": "2020-04-17 13:05:25.199832", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 52a0f4e081d..df142a83ddc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", @@ -74,9 +75,9 @@ "base_total", "base_net_total", "column_break_32", + "total_net_weight", "total", "net_total", - "total_net_weight", "taxes_section", "taxes_and_charges", "column_break_38", @@ -1568,7 +1569,8 @@ "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, - "modified": "2020-02-10 04:57:11.221180", + "links": [], + "modified": "2020-04-17 12:38:41.435728", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index d82e128735e..0ccdb25c32f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -63,9 +64,9 @@ "base_total", "base_net_total", "column_break_26", + "total_net_weight", "total", "net_total", - "total_net_weight", "taxes_section", "tax_category", "column_break_50", @@ -171,6 +172,7 @@ }, { "depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))", + "description": "Fetch items based on Default Supplier.", "fieldname": "get_items_from_open_material_requests", "fieldtype": "Button", "label": "Get Items from Open Material Requests" @@ -1053,7 +1055,8 @@ "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, - "modified": "2020-01-14 18:54:39.694448", + "links": [], + "modified": "2020-04-17 13:04:28.185197", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 54e87f7a3b5..6462d3bc8c3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -60,9 +60,9 @@ "base_total", "base_net_total", "column_break_33", + "total_net_weight", "total", "net_total", - "total_net_weight", "taxes_section", "tax_category", "column_break_38", @@ -1196,7 +1196,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2019-12-30 19:15:28.605085", + "modified": "2020-04-17 12:50:39.640534", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 86200ba26b6..424a7f3c22f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -66,9 +66,9 @@ "base_total", "base_net_total", "column_break_33", + "total_net_weight", "total", "net_total", - "total_net_weight", "taxes_section", "tax_category", "column_break_39", @@ -1240,7 +1240,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2019-12-30 19:17:13.122644", + "modified": "2020-04-17 12:51:41.288600", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 00a50a88ef0..889d7326e90 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -59,9 +59,9 @@ "base_total", "base_net_total", "column_break_27", + "total_net_weight", "total", "net_total", - "total_net_weight", "taxes_charges_section", "tax_category", "shipping_col", @@ -1065,7 +1065,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-04-06 16:31:37.444891", + "modified": "2020-04-17 13:06:26.970288", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 4de4cb055fb0645e13fac8cda3b0f5c568009453 Mon Sep 17 00:00:00 2001 From: Anoop Date: Sun, 19 Apr 2020 19:26:31 +0530 Subject: [PATCH 030/178] fix: removed reference to method not in version-12 (#21335) removed scheduler event incorrectly added in this version. as reported on discuss - https://discuss.erpnext.com/t/internal-server-error-and-healthcare-module-missing/60479/6?u=akurungadam --- erpnext/hooks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index da180f86884..316c3392851 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -268,9 +268,7 @@ doc_events = { scheduler_events = { "all": [ - "erpnext.projects.doctype.project.project.project_status_update_reminder", - "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" - + "erpnext.projects.doctype.project.project.project_status_update_reminder" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', @@ -530,4 +528,4 @@ global_search_doctypes = { {'doctype': 'Hotel Room Package', 'index': 3}, {'doctype': 'Hotel Room Type', 'index': 4} ] -} \ No newline at end of file +} From 5817adbb64d7cf2dde4c30e1ea0012e2f7512d11 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 19 Apr 2020 19:28:32 +0530 Subject: [PATCH 031/178] fix: Valid warehouse in woocommerce syncing and other small fixes (#21332) * fix: Valid warehouse in woocommerce syncing * fix: dmall fixes in gross & net profit report * fix: company is required for getting party details * fix: None issue while getting raw material rate based on last purchase rate --- .../doctype/payment_entry/payment_entry.js | 2 +- .../gross_and_net_profit_report.py | 32 ++++++++++++------- .../connectors/woocommerce_connection.py | 6 +++- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index fbf11bca5de..4584063ed69 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -272,7 +272,7 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("contact_email", ""); frm.set_value("contact_person", ""); } - if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party) { + if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) { if(!frm.doc.posting_date) { frappe.msgprint(__("Please select Posting Date before selecting Party")) frm.set_value("party", ""); diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 6550981a146..260f35f270d 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -8,7 +8,6 @@ from frappe.utils import flt from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data) import copy - def execute(filters=None): period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.periodicity, filters.accumulated_values, filters.company) @@ -27,17 +26,19 @@ def execute(filters=None): gross_income = get_revenue(income, period_list) - gross_expense = get_revenue(expense, period_list) if(len(gross_income)==0 and len(gross_expense)== 0): - data.append({"account_name": "'" + _("Nothing is included in gross") + "'", - "account": "'" + _("Nothing is included in gross") + "'"}) - + data.append({ + "account_name": "'" + _("Nothing is included in gross") + "'", + "account": "'" + _("Nothing is included in gross") + "'" + }) return columns, data - data.append({"account_name": "'" + _("Included in Gross Profit") + "'", - "account": "'" + _("Included in Gross Profit") + "'"}) + data.append({ + "account_name": "'" + _("Included in Gross Profit") + "'", + "account": "'" + _("Included in Gross Profit") + "'" + }) data.append({}) data.extend(gross_income or []) @@ -111,7 +112,6 @@ def set_total(node, value, complete_list, totals): def get_profit(gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False): - profit_loss = { "account_name": "'" + _(profit_type) + "'", "account": "'" + _(profit_type) + "'", @@ -123,7 +123,9 @@ def get_profit(gross_income, gross_expense, period_list, company, profit_type, c for period in period_list: key = period if consolidated else period.key - profit_loss[key] = flt(gross_income[0].get(key, 0)) - flt(gross_expense[0].get(key, 0)) + gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0 + gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0 + profit_loss[key] = gross_income_for_period - gross_expense_for_period if profit_loss[key]: has_value=True @@ -143,12 +145,18 @@ def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expe for period in period_list: key = period if consolidated else period.key - total_income = flt(gross_income[0].get(key, 0)) + flt(non_gross_income[0].get(key, 0)) - total_expense = flt(gross_expense[0].get(key, 0)) + flt(non_gross_expense[0].get(key, 0)) + gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0 + non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0 + + gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0 + non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0 + + total_income = gross_income_for_period + non_gross_income_for_period + total_expense = gross_expense_for_period + non_gross_expense_for_period profit_loss[key] = flt(total_income) - flt(total_expense) if profit_loss[key]: has_value=True if has_value: - return profit_loss + return profit_loss \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 4422d23e380..618865200cf 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -144,6 +144,10 @@ def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang): company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr') + default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr) + if not frappe.db.exists("Warehouse", default_warehouse): + frappe.throw(_("Please set Warehouse in Woocommerce Settings")) + for item in order.get("line_items"): woocomm_item_id = item.get("product_id") found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id}) @@ -158,7 +162,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l "uom": woocommerce_settings.uom or _("Nos", sys_lang), "qty": item.get("quantity"), "rate": item.get("price"), - "warehouse": woocommerce_settings.warehouse or _("Stores - {0}", sys_lang).format(company_abbr) + "warehouse": woocommerce_settings.warehouse or default_warehouse }) add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 19d36f6e8c4..8d3aa9118c7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -183,7 +183,7 @@ class BOM(WebsiteGenerator): if self.rm_cost_as_per == 'Valuation Rate': rate = self.get_valuation_rate(arg) * (arg.get("conversion_factor") or 1) elif self.rm_cost_as_per == 'Last Purchase Rate': - rate = (arg.get('last_purchase_rate') \ + rate = flt(arg.get('last_purchase_rate') \ or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")) \ * (arg.get("conversion_factor") or 1) elif self.rm_cost_as_per == "Price List": From e80702b6c2ab7f215c5f6a08f8a6c7e06aa97f5f Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sun, 19 Apr 2020 16:46:18 +0200 Subject: [PATCH 032/178] fix(regional): backport DATEV fix (#21281) * fix: quote nonnumeric values * fix(DATEV Settings): restrict max length of IDs * fix: display Columns as Dynamic Link instead of as Data * fix: add column "Belegfeld 1" * fix: truncate column Buchungstext to 60 chars * fix: make header compatible to current DATEV Format 7.00 * fix: column names and descriptions --- .../datev_settings/datev_settings.json | 4 +- erpnext/regional/report/datev/datev.py | 175 ++++++++++++------ 2 files changed, 122 insertions(+), 57 deletions(-) diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index 6860ed3fdae..caed7367dc1 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -28,6 +28,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Client ID", + "length": 5, "reqd": 1 }, { @@ -42,6 +43,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Consultant ID", + "length": 7, "reqd": 1 }, { @@ -57,7 +59,7 @@ "fieldtype": "Column Break" } ], - "modified": "2019-08-14 00:03:26.616460", + "modified": "2020-04-15 12:59:57.786506", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a58e13a0947..a2d74b889ad 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -12,6 +12,7 @@ import datetime import json import six from six import string_types +from csv import QUOTE_NONNUMERIC import frappe from frappe import _ @@ -57,8 +58,8 @@ def get_columns(): "fieldtype": "Data", }, { - "label": "Kontonummer", - "fieldname": "Kontonummer", + "label": "Konto", + "fieldname": "Konto", "fieldtype": "Data", }, { @@ -71,6 +72,11 @@ def get_columns(): "fieldname": "Belegdatum", "fieldtype": "Date", }, + { + "label": "Belegfeld 1", + "fieldname": "Belegfeld 1", + "fieldtype": "Data", + }, { "label": "Buchungstext", "fieldname": "Buchungstext", @@ -79,22 +85,26 @@ def get_columns(): { "label": "Beleginfo - Art 1", "fieldname": "Beleginfo - Art 1", - "fieldtype": "Data", + "fieldtype": "Link", + "options": "DocType" }, { "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Data", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 1" }, { "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", - "fieldtype": "Data", + "fieldtype": "Link", + "options": "DocType" }, { "label": "Beleginfo - Inhalt 2", "fieldname": "Beleginfo - Inhalt 2", - "fieldtype": "Data", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 2" } ] @@ -122,13 +132,14 @@ def get_gl_entries(filters, as_dict): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', /* account number or, if empty, party account number */ - coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', + coalesce(acc.account_number, acc_pa.account_number) as 'Konto', /* against number or, if empty, party against number */ coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', gl.posting_date as 'Belegdatum', - gl.remarks as 'Buchungstext', + gl.voucher_no as 'Belegfeld 1', + LEFT(gl.remarks, 60) as 'Buchungstext', gl.voucher_type as 'Beleginfo - Art 1', gl.voucher_no as 'Beleginfo - Inhalt 1', gl.against_voucher_type as 'Beleginfo - Art 2', @@ -177,16 +188,19 @@ def get_datev_csv(data, filters): data -- array of dictionaries filters -- dict """ + coa = frappe.get_value("Company", filters.get("company"), "chart_of_accounts") + coa_used = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") + header = [ - # A = DATEV format + # A = DATEV-Format-KZ # DTVF = created by DATEV software, # EXTF = created by other software - "EXTF", + '"EXTF"', # B = version of the DATEV format # 141 = 1.41, # 510 = 5.10, # 720 = 7.20 - "510", + "700", # C = Data category # 21 = Transaction batch (Buchungsstapel), # 67 = Buchungstextkonstanten, @@ -200,23 +214,22 @@ def get_datev_csv(data, filters): # Kontenbeschriftungen "Buchungsstapel", # E = Format version (regarding format name) - "", + "9", # F = Generated on - datetime.datetime.now().strftime("%Y%m%d"), + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000', # G = Imported on -- stays empty "", - # H = Origin (SV = other (?), RE = KARE) - "SV", + # H = Herkunfts-Kennzeichen (Origin) + # Any two letters + '"EN"', # I = Exported by - frappe.session.user, + '"%s"' % frappe.session.user, # J = Imported by -- stays empty "", # K = Tax consultant number (Beraternummer) frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number") or "", - "", # L = Tax client number (Mandantennummer) frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "", - "", # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) @@ -226,10 +239,7 @@ def get_datev_csv(data, filters): # P = Transaction batch end date (YYYYMMDD) frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"), # Q = Description (for example, "January - February 2019 Transactions") - "{} - {} Buchungsstapel".format( - frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"), - frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy") - ), + "Buchungsstapel", # R = Diktatkürzel "", # S = Buchungstyp @@ -237,11 +247,29 @@ def get_datev_csv(data, filters): # 2 = Annual financial statement (Jahresabschluss) "1", # T = Rechnungslegungszweck - "", + "0", # vom Rechnungslegungszweck unabhängig # U = Festschreibung - "", + "0", # keine Festschreibung # V = Kontoführungs-Währungskennzeichen des Geldkontos - frappe.get_value("Company", filters.get("company"), "default_currency") + frappe.get_value("Company", filters.get("company"), "default_currency"), + # reserviert + '', + # Derivatskennzeichen + '', + # reserviert + '', + # reserviert + '', + # SKR + '"%s"' % coa_used, + # Branchen-Lösungs-ID + '', + # reserviert + '', + # reserviert + '', + # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) + '' ] columns = [ # All possible columns must tbe listed here, because DATEV requires them to @@ -255,24 +283,27 @@ def get_datev_csv(data, filters): "Basis-Umsatz", "WKZ Basis-Umsatz", # Konto/Gegenkonto - "Kontonummer", + "Konto", "Gegenkonto (ohne BU-Schlüssel)", "BU-Schlüssel", # Datum "Belegdatum", - # Belegfelder + # Rechnungs- / Belegnummer "Belegfeld 1", + # z.B. Fälligkeitsdatum Format: TTMMJJ "Belegfeld 2", - # Weitere Felder + # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig) "Skonto", + # Beschreibung des Buchungssatzes "Buchungstext", - # OPOS-Informationen + # Mahn- / Zahl-Sperre (1 = Postensperre) "Postensperre", "Diverse Adressnummer", "Geschäftspartnerbank", "Sachverhalt", + # Keine Mahnzinsen "Zinssperre", - # Digitaler Beleg + # Link auf den Buchungsbeleg (Programmkürzel + GUID) "Beleglink", # Beleginfo "Beleginfo - Art 1", @@ -291,22 +322,30 @@ def get_datev_csv(data, filters): "Beleginfo - Inhalt 7", "Beleginfo - Art 8", "Beleginfo - Inhalt 8", - # Kostenrechnung - "Kost 1 - Kostenstelle", - "Kost 2 - Kostenstelle", - "Kost-Menge", - # Steuerrechnung - "EU-Land u. UStID", + # Zuordnung des Geschäftsvorfalls für die Kostenrechnung + "KOST1 - Kostenstelle", + "KOST2 - Kostenstelle", + "KOST-Menge", + # USt-ID-Nummer (Beispiel: DE133546770) + "EU-Mitgliedstaat u. USt-IdNr.", + # Der im EU-Bestimmungsland gültige Steuersatz "EU-Steuersatz", + # I = Ist-Versteuerung, + # K = keine Umsatzsteuerrechnung + # P = Pauschalierung (z. B. für Land- und Forstwirtschaft), + # S = Soll-Versteuerung "Abw. Versteuerungsart", - # L+L Sachverhalt + # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG "Sachverhalt L+L", + # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%) "Funktionsergänzung L+L", - # Funktion Steuerschlüssel 49 + # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der + # steuerliche Sachverhalt mitgegeben werden "BU 49 Hauptfunktionstyp", "BU 49 Hauptfunktionsnummer", "BU 49 Funktionsergänzung", - # Zusatzinformationen + # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können + # frei erfasst werden. "Zusatzinformation - Art 1", "Zusatzinformation - Inhalt 1", "Zusatzinformation - Art 2", @@ -347,54 +386,76 @@ def get_datev_csv(data, filters): "Zusatzinformation - Inhalt 19", "Zusatzinformation - Art 20", "Zusatzinformation - Inhalt 20", - # Mengenfelder LuF + # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus, + # für andere SKR werden die Felder beim Import / Export überlesen bzw. + # leer exportiert. "Stück", "Gewicht", - # Forderungsart + # 1 = Lastschrift + # 2 = Mahnung + # 3 = Zahlung "Zahlweise", "Forderungsart", + # JJJJ "Veranlagungsjahr", + # TTMMJJJJ "Zugeordnete Fälligkeit", - # Weitere Felder + # 1 = Einkauf von Waren + # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen "Skontotyp", - # Anzahlungen + # Allgemeine Bezeichnung, des Auftrags / Projekts. "Auftragsnummer", + # AA = Angeforderte Anzahlung / Abschlagsrechnung + # AG = Erhaltene Anzahlung (Geldeingang) + # AV = Erhaltene Anzahlung (Verbindlichkeit) + # SR = Schlussrechnung + # SU = Schlussrechnung (Umbuchung) + # SG = Schlussrechnung (Geldeingang) + # SO = Sonstige "Buchungstyp", "USt-Schlüssel (Anzahlungen)", - "EU-Land (Anzahlungen)", + "EU-Mitgliedstaat (Anzahlungen)", "Sachverhalt L+L (Anzahlungen)", "EU-Steuersatz (Anzahlungen)", "Erlöskonto (Anzahlungen)", - # Stapelinformationen + # Wird beim Import durch SV (Stapelverarbeitung) ersetzt. "Herkunft-Kz", - # Technische Identifikation - "Buchungs GUID", - # Kostenrechnung - "Kost-Datum", - # OPOS-Informationen + # Wird von DATEV verwendet. + "Leerfeld", + # Format TTMMJJJJ + "KOST-Datum", + # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats + # (z.B. Rechnungs- oder Kundennummer). "SEPA-Mandatsreferenz", + # 1 = Skontosperre + # 0 = Keine Skontosperre "Skontosperre", # Gesellschafter und Sonderbilanzsachverhalt "Gesellschaftername", + # Amtliche Nummer aus der Feststellungserklärung "Beteiligtennummer", "Identifikationsnummer", "Zeichnernummer", - # OPOS-Informationen + # Format TTMMJJJJ "Postensperre bis", # Gesellschafter und Sonderbilanzsachverhalt "Bezeichnung SoBil-Sachverhalt", "Kennzeichen SoBil-Buchung", - # Stapelinformationen + # 0 = keine Festschreibung + # 1 = Festschreibung "Festschreibung", - # Datum + # Format TTMMJJJJ "Leistungsdatum", + # Format TTMMJJJJ "Datum Zuord. Steuerperiode", - # OPOS-Informationen + # OPOS-Informationen, Format TTMMJJJJ "Fälligkeit", - # Konto/Gegenkonto + # G oder 1 = Generalumkehr + # 0 = keine Generalumkehr "Generalumkehr (GU)", # Steuersatz für Steuerschlüssel "Steuersatz", + # Beispiel: DE für Deutschland "Land" ] @@ -419,7 +480,9 @@ def get_datev_csv(data, filters): # Do not number rows index=False, # Use all columns defined above - columns=columns + columns=columns, + # Quote most fields, even currency values with "," separator + quoting=QUOTE_NONNUMERIC ) if not six.PY2: From fa07e0fb9d4dc589b14e430bfab1304e1c8bb04f Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 14 Apr 2020 04:07:14 +0530 Subject: [PATCH 033/178] fix: handle errors in enqueued methods and update status --- .../tally_migration/tally_migration.js | 51 +++-- .../tally_migration/tally_migration.py | 198 +++++++++++------- 2 files changed, 159 insertions(+), 90 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js index 104ac570c69..682f78efb72 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js @@ -2,15 +2,40 @@ // For license information, please see license.txt frappe.ui.form.on('Tally Migration', { - onload: function(frm) { + onload: function (frm) { + let reload_status = true; frappe.realtime.on("tally_migration_progress_update", function (data) { + if (reload_status) { + frappe.model.with_doc(frm.doc.doctype, frm.doc.name, () => { + frm.refresh_header(); + }); + reload_status = false; + } frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); - if (data.count == data.total) { - window.setTimeout(title => frm.dashboard.hide_progress(title), 1500, data.title); + let error_occurred = data.count === -1; + if (data.count == data.total || error_occurred) { + window.setTimeout((title) => { + frm.dashboard.hide_progress(title) + frm.reload_doc(); + if (error_occurred) { + frappe.msgprint({ + message: __("An error has occurred during {0}. Check {1} for more details", + [ + repl("%(tally_document)s", { + tally_document: frm.docname + }), + "Error Log" + ] + ), + title: __("Tally Migration Error"), + indicator: "red" + }); + } + }, 2000, data.title); } }); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.master_data && !frm.doc.is_master_data_imported) { if (frm.doc.is_master_data_processed) { if (frm.doc.status != "Importing Master Data") { @@ -34,17 +59,17 @@ frappe.ui.form.on('Tally Migration', { } } }, - add_button: function(frm, label, method) { + add_button: function (frm, label, method) { frm.add_custom_button( label, - () => frm.call({ - doc: frm.doc, - method: method, - freeze: true, - callback: () => { - frm.remove_custom_button(label); - } - }) + () => { + frm.call({ + doc: frm.doc, + method: method, + freeze: true + }); + frm.reload_doc(); + } ); } }); diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 01eee5b61f2..ac42d385162 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -4,20 +4,25 @@ from __future__ import unicode_literals -from decimal import Decimal import json +import sys import re import traceback import zipfile +from decimal import Decimal + +from bs4 import BeautifulSoup as bs + import frappe +from erpnext import encode_company_abbr +from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification from frappe.model.document import Document from frappe.model.naming import getseries, revert_series_if_last from frappe.utils.data import format_datetime -from bs4 import BeautifulSoup as bs -from erpnext import encode_company_abbr -from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts + PRIMARY_ACCOUNT = "Primary" VOUCHER_CHUNK_SIZE = 500 @@ -39,13 +44,15 @@ class TallyMigration(Document): return string master_file = frappe.get_doc("File", {"file_url": data_file}) + master_file_path = master_file.get_full_path() - with zipfile.ZipFile(master_file.get_full_path()) as zf: - encoded_content = zf.read(zf.namelist()[0]) - try: - content = encoded_content.decode("utf-8-sig") - except UnicodeDecodeError: - content = encoded_content.decode("utf-16") + if zipfile.is_zipfile(master_file_path): + with zipfile.ZipFile(master_file_path) as zf: + encoded_content = zf.read(zf.namelist()[0]) + try: + content = encoded_content.decode("utf-8-sig") + except UnicodeDecodeError: + content = encoded_content.decode("utf-16") master = bs(sanitize(emptify(content)), "xml") collection = master.BODY.IMPORTDATA.REQUESTDATA @@ -58,7 +65,8 @@ class TallyMigration(Document): "file_name": key + ".json", "attached_to_doctype": self.doctype, "attached_to_name": self.name, - "content": json.dumps(value) + "content": json.dumps(value), + "is_private": True }).insert() setattr(self, key, f.file_url) @@ -168,8 +176,8 @@ class TallyMigration(Document): address = "\n".join([a.string for a in account.find_all("ADDRESS")]) addresses.append({ "doctype": "Address", - "address_line1": address[:140].strip(), - "address_line2": address[140:].strip(), + "address_line1": address[:140], + "address_line2": address[140:], "country": account.COUNTRYNAME.string if account.COUNTRYNAME else None, "state": account.LEDSTATENAME.string if account.LEDSTATENAME else None, "gst_state": account.LEDSTATENAME.string if account.LEDSTATENAME else None, @@ -188,37 +196,46 @@ class TallyMigration(Document): items = [] for item in collection.find_all("STOCKITEM"): + stock_uom = item.BASEUNITS.string if item.BASEUNITS else "Unit" #self.default_uom items.append({ "doctype": "Item", "item_code" : item.NAME.string, - "stock_uom": item.BASEUNITS.string, + "stock_uom": stock_uom, "is_stock_item": 0, "item_group": "All Item Groups", "item_defaults": [{"company": self.erpnext_company}] }) + return items, uoms + try: + self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5) + collection = self.get_collection(self.master_data) + company = get_company_name(collection) + self.tally_company = company + self.erpnext_company = company - self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5) - collection = self.get_collection(self.master_data) + self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5) + chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection) - company = get_company_name(collection) - self.tally_company = company - self.erpnext_company = company + self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5) + parties, addresses = get_parties_addresses(collection, customers, suppliers) - self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5) - chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection) - self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5) - parties, addresses = get_parties_addresses(collection, customers, suppliers) - self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5) - items, uoms = get_stock_items_uoms(collection) - data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms} - self.publish("Process Master Data", _("Done"), 5, 5) + self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5) + items, uoms = get_stock_items_uoms(collection) + data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms} - self.dump_processed_data(data) - self.is_master_data_processed = 1 - self.status = "" - self.save() + self.publish("Process Master Data", _("Done"), 5, 5) + self.dump_processed_data(data) + + self.is_master_data_processed = 1 + + except: + self.publish("Process Master Data", _("Process Failed"), -1, 5) + self.log() + + finally: + self.set_status() def publish(self, title, message, count, total): frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total}) @@ -256,7 +273,6 @@ class TallyMigration(Document): except: self.log(address) - def create_items_uoms(items_file_url, uoms_file_url): uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url}) for uom in json.loads(uoms_file.get_content()): @@ -273,16 +289,26 @@ class TallyMigration(Document): except: self.log(item) - self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4) - create_company_and_coa(self.chart_of_accounts) - self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4) - create_parties_and_addresses(self.parties, self.addresses) - self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4) - create_items_uoms(self.items, self.uoms) - self.publish("Import Master Data", _("Done"), 4, 4) - self.status = "" - self.is_master_data_imported = 1 - self.save() + try: + self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4) + create_company_and_coa(self.chart_of_accounts) + + self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4) + create_parties_and_addresses(self.parties, self.addresses) + + self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4) + create_items_uoms(self.items, self.uoms) + + self.publish("Import Master Data", _("Done"), 4, 4) + + self.is_master_data_imported = 1 + + except: + self.publish("Import Master Data", _("Process Failed"), -1, 5) + self.log() + + finally: + self.set_status() def _process_day_book_data(self): def get_vouchers(collection): @@ -373,12 +399,12 @@ class TallyMigration(Document): account_field = "expense_account" items = [] for entry in inventory_entries: - qty, uom = entry.ACTUALQTY.string.strip().split() + qty, uom = entry.ACTUALQTY.string.split() items.append({ "item_code": entry.STOCKITEMNAME.string, "description": entry.STOCKITEMNAME.string, - "qty": qty.strip(), - "uom": uom.strip(), + "qty": qty, + "uom": uom, "conversion_factor": 1, "price_list_rate": entry.RATE.string.split("/")[0], "cost_center": self.default_cost_center, @@ -408,15 +434,24 @@ class TallyMigration(Document): elif frappe.db.exists({"doctype": "Customer", "customer_name": party}): return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company) - self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3) - collection = self.get_collection(self.day_book_data) - self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3) - vouchers = get_vouchers(collection) - self.publish("Process Day Book Data", _("Done"), 3, 3) - self.dump_processed_data({"vouchers": vouchers}) - self.status = "" - self.is_day_book_data_processed = 1 - self.save() + try: + self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3) + collection = self.get_collection(self.day_book_data) + + self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3) + vouchers = get_vouchers(collection) + + self.publish("Process Day Book Data", _("Done"), 3, 3) + self.dump_processed_data({"vouchers": vouchers}) + + self.is_day_book_data_processed = 1 + + except: + self.publish("Process Day Book Data", _("Process Failed"), -1, 5) + self.log() + + finally: + self.set_status() def _import_day_book_data(self): def create_fiscal_years(vouchers): @@ -454,23 +489,31 @@ class TallyMigration(Document): "currency": "INR" }).insert() - frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable") - frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable") - frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account) + try: + frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable") + frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable") + frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account) - vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) - vouchers = json.loads(vouchers_file.get_content()) + vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) + vouchers = json.loads(vouchers_file.get_content()) - create_fiscal_years(vouchers) - create_price_list() - create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"]) + create_fiscal_years(vouchers) + create_price_list() + create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"]) - total = len(vouchers) - is_last = False - for index in range(0, total, VOUCHER_CHUNK_SIZE): - if index + VOUCHER_CHUNK_SIZE >= total: - is_last = True - frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last) + total = len(vouchers) + is_last = False + + for index in range(0, total, VOUCHER_CHUNK_SIZE): + if index + VOUCHER_CHUNK_SIZE >= total: + is_last = True + frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last) + + except: + self.log() + + finally: + self.set_status() def _import_vouchers(self, start, total, is_last=False): frappe.flags.in_migrate = True @@ -494,25 +537,26 @@ class TallyMigration(Document): frappe.flags.in_migrate = False def process_master_data(self): - self.status = "Processing Master Data" - self.save() + self.set_status("Processing Master Data") frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600) def import_master_data(self): - self.status = "Importing Master Data" - self.save() + self.set_status("Importing Master Data") frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600) def process_day_book_data(self): - self.status = "Processing Day Book Data" - self.save() + self.set_status("Processing Day Book Data") frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600) def import_day_book_data(self): - self.status = "Importing Day Book Data" - self.save() + self.set_status("Importing Day Book Data") frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600) def log(self, data=None): - message = "\n".join(["Data", json.dumps(data, default=str, indent=4), "Exception", traceback.format_exc()]) + data = data or self.status + message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) return frappe.log_error(title="Tally Migration Error", message=message) + + def set_status(self, status=""): + self.status = status + self.save() From 4171c5ceeb42e736bd59292ed28c90a3aad9ba5b Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 16 Apr 2020 16:12:40 +0530 Subject: [PATCH 034/178] fix(tally-migration): DocType improvement --- .../tally_migration/tally_migration.json | 26 +++++++++++++++++-- .../tally_migration/tally_migration.py | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json index 26415caf8d3..dc6f093ac9d 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json @@ -1,4 +1,5 @@ { + "actions": [], "beta": 1, "creation": "2019-02-01 14:27:09.485238", "doctype": "DocType", @@ -14,6 +15,7 @@ "tally_debtors_account", "company_section", "tally_company", + "default_uom", "column_break_8", "erpnext_company", "processed_files_section", @@ -43,6 +45,7 @@ "label": "Status" }, { + "description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs", "fieldname": "master_data", "fieldtype": "Attach", "in_list_view": 1, @@ -50,6 +53,7 @@ }, { "default": "Sundry Creditors", + "description": "Creditors Account set in Tally", "fieldname": "tally_creditors_account", "fieldtype": "Data", "label": "Tally Creditors Account", @@ -61,6 +65,7 @@ }, { "default": "Sundry Debtors", + "description": "Debtors Account set in Tally", "fieldname": "tally_debtors_account", "fieldtype": "Data", "label": "Tally Debtors Account", @@ -72,6 +77,7 @@ "fieldtype": "Section Break" }, { + "description": "Company Name as per Imported Tally Data", "fieldname": "tally_company", "fieldtype": "Data", "label": "Tally Company", @@ -82,9 +88,11 @@ "fieldtype": "Column Break" }, { + "description": "Your Company set in ERPNext", "fieldname": "erpnext_company", "fieldtype": "Data", - "label": "ERPNext Company" + "label": "ERPNext Company", + "read_only_depends_on": "eval:doc.is_master_data_processed == 1" }, { "fieldname": "processed_files_section", @@ -155,24 +163,28 @@ "options": "Cost Center" }, { + "default": "0", "fieldname": "is_master_data_processed", "fieldtype": "Check", "label": "Is Master Data Processed", "read_only": 1 }, { + "default": "0", "fieldname": "is_day_book_data_processed", "fieldtype": "Check", "label": "Is Day Book Data Processed", "read_only": 1 }, { + "default": "0", "fieldname": "is_day_book_data_imported", "fieldtype": "Check", "label": "Is Day Book Data Imported", "read_only": 1 }, { + "default": "0", "fieldname": "is_master_data_imported", "fieldtype": "Check", "label": "Is Master Data Imported", @@ -188,13 +200,23 @@ "fieldtype": "Column Break" }, { + "description": "Day Book Data exported from Tally that consists of all historic transactions", "fieldname": "day_book_data", "fieldtype": "Attach", "in_list_view": 1, "label": "Day Book Data" + }, + { + "default": "Unit", + "description": "UOM in case unspecified in imported data", + "fieldname": "default_uom", + "fieldtype": "Link", + "label": "Default UOM", + "options": "UOM" } ], - "modified": "2019-04-29 05:46:54.394967", + "links": [], + "modified": "2020-04-16 13:03:28.894919", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Tally Migration", diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index ac42d385162..9c8cb7ace71 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -196,7 +196,7 @@ class TallyMigration(Document): items = [] for item in collection.find_all("STOCKITEM"): - stock_uom = item.BASEUNITS.string if item.BASEUNITS else "Unit" #self.default_uom + stock_uom = item.BASEUNITS.string if item.BASEUNITS else self.default_uom items.append({ "doctype": "Item", "item_code" : item.NAME.string, From 77532b96b86d356d35c4072ad6e3b848657729bd Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 16 Apr 2020 15:51:12 +0530 Subject: [PATCH 035/178] fix: strip data fields of whitespaces --- .../tally_migration/tally_migration.py | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 9c8cb7ace71..98a30273e63 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -72,7 +72,7 @@ class TallyMigration(Document): def _process_master_data(self): def get_company_name(collection): - return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string + return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip() def get_coa_customers_suppliers(collection): root_type_map = { @@ -105,17 +105,17 @@ class TallyMigration(Document): # If Ledger doesn't have PARENT field then don't create Account # For example "Profit & Loss A/c" if account.PARENT: - yield account.PARENT.string, account["NAME"], 0 + yield account.PARENT.string.strip(), account["NAME"], 0 def get_parent(account): if account.PARENT: - return account.PARENT.string + return account.PARENT.string.strip() return { ("Yes", "No"): "Application of Funds (Assets)", ("Yes", "Yes"): "Expenses", ("No", "Yes"): "Income", ("No", "No"): "Source of Funds (Liabilities)", - }[(account.ISDEEMEDPOSITIVE.string, account.ISREVENUE.string)] + }[(account.ISDEEMEDPOSITIVE.string.strip(), account.ISREVENUE.string.strip())] def get_children_and_parent_dict(accounts): children, parents = {}, {} @@ -153,38 +153,38 @@ class TallyMigration(Document): parties, addresses = [], [] for account in collection.find_all("LEDGER"): party_type = None - if account.NAME.string in customers: + if account.NAME.string.strip() in customers: party_type = "Customer" parties.append({ "doctype": party_type, - "customer_name": account.NAME.string, - "tax_id": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None, + "customer_name": account.NAME.string.strip(), + "tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, "customer_group": "All Customer Groups", "territory": "All Territories", "customer_type": "Individual", }) - elif account.NAME.string in suppliers: + elif account.NAME.string.strip() in suppliers: party_type = "Supplier" parties.append({ "doctype": party_type, - "supplier_name": account.NAME.string, - "pan": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None, + "supplier_name": account.NAME.string.strip(), + "pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None, "supplier_group": "All Supplier Groups", "supplier_type": "Individual", }) if party_type: - address = "\n".join([a.string for a in account.find_all("ADDRESS")]) + address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")]) addresses.append({ "doctype": "Address", - "address_line1": address[:140], - "address_line2": address[140:], - "country": account.COUNTRYNAME.string if account.COUNTRYNAME else None, - "state": account.LEDSTATENAME.string if account.LEDSTATENAME else None, - "gst_state": account.LEDSTATENAME.string if account.LEDSTATENAME else None, - "pin_code": account.PINCODE.string if account.PINCODE else None, - "mobile": account.LEDGERPHONE.string if account.LEDGERPHONE else None, - "phone": account.LEDGERPHONE.string if account.LEDGERPHONE else None, - "gstin": account.PARTYGSTIN.string if account.PARTYGSTIN else None, + "address_line1": address[:140].strip(), + "address_line2": address[140:].strip(), + "country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None, + "state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, + "gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None, + "pin_code": account.PINCODE.string.strip() if account.PINCODE else None, + "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, + "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, + "gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None, "links": [{"link_doctype": party_type, "link_name": account["NAME"]}], }) return parties, addresses @@ -192,15 +192,15 @@ class TallyMigration(Document): def get_stock_items_uoms(collection): uoms = [] for uom in collection.find_all("UNIT"): - uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string}) + uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string.strip()}) items = [] for item in collection.find_all("STOCKITEM"): - stock_uom = item.BASEUNITS.string if item.BASEUNITS else self.default_uom + stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom items.append({ "doctype": "Item", - "item_code" : item.NAME.string, - "stock_uom": stock_uom, + "item_code" : item.NAME.string.strip(), + "stock_uom": stock_uom.strip(), "is_stock_item": 0, "item_group": "All Item Groups", "item_defaults": [{"company": self.erpnext_company}] @@ -314,10 +314,10 @@ class TallyMigration(Document): def get_vouchers(collection): vouchers = [] for voucher in collection.find_all("VOUCHER"): - if voucher.ISCANCELLED.string == "Yes": + if voucher.ISCANCELLED.string.strip() == "Yes": continue inventory_entries = voucher.find_all("INVENTORYENTRIES.LIST") + voucher.find_all("ALLINVENTORYENTRIES.LIST") + voucher.find_all("INVENTORYENTRIESIN.LIST") + voucher.find_all("INVENTORYENTRIESOUT.LIST") - if voucher.VOUCHERTYPENAME.string not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries: + if voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries: function = voucher_to_invoice else: function = voucher_to_journal_entry @@ -333,15 +333,15 @@ class TallyMigration(Document): accounts = [] ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST") for entry in ledger_entries: - account = {"account": encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company), "cost_center": self.default_cost_center} - if entry.ISPARTYLEDGER.string == "Yes": - party_details = get_party(entry.LEDGERNAME.string) + account = {"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company), "cost_center": self.default_cost_center} + if entry.ISPARTYLEDGER.string.strip() == "Yes": + party_details = get_party(entry.LEDGERNAME.string.strip()) if party_details: party_type, party_account = party_details account["party_type"] = party_type account["account"] = party_account - account["party"] = entry.LEDGERNAME.string - amount = Decimal(entry.AMOUNT.string) + account["party"] = entry.LEDGERNAME.string.strip() + amount = Decimal(entry.AMOUNT.string.strip()) if amount > 0: account["credit_in_account_currency"] = str(abs(amount)) else: @@ -350,21 +350,21 @@ class TallyMigration(Document): journal_entry = { "doctype": "Journal Entry", - "tally_guid": voucher.GUID.string, - "posting_date": voucher.DATE.string, + "tally_guid": voucher.GUID.string.strip(), + "posting_date": voucher.DATE.string.strip(), "company": self.erpnext_company, "accounts": accounts, } return journal_entry def voucher_to_invoice(voucher): - if voucher.VOUCHERTYPENAME.string in ["Sales", "Credit Note"]: + if voucher.VOUCHERTYPENAME.string.strip() in ["Sales", "Credit Note"]: doctype = "Sales Invoice" party_field = "customer" account_field = "debit_to" account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company) price_list_field = "selling_price_list" - elif voucher.VOUCHERTYPENAME.string in ["Purchase", "Debit Note"]: + elif voucher.VOUCHERTYPENAME.string.strip() in ["Purchase", "Debit Note"]: doctype = "Purchase Invoice" party_field = "supplier" account_field = "credit_to" @@ -377,10 +377,10 @@ class TallyMigration(Document): invoice = { "doctype": doctype, - party_field: voucher.PARTYNAME.string, - "tally_guid": voucher.GUID.string, - "posting_date": voucher.DATE.string, - "due_date": voucher.DATE.string, + party_field: voucher.PARTYNAME.string.strip(), + "tally_guid": voucher.GUID.string.strip(), + "posting_date": voucher.DATE.string.strip(), + "due_date": voucher.DATE.string.strip(), "items": get_voucher_items(voucher, doctype), "taxes": get_voucher_taxes(voucher), account_field: account_name, @@ -399,17 +399,17 @@ class TallyMigration(Document): account_field = "expense_account" items = [] for entry in inventory_entries: - qty, uom = entry.ACTUALQTY.string.split() + qty, uom = entry.ACTUALQTY.string.strip().split() items.append({ - "item_code": entry.STOCKITEMNAME.string, - "description": entry.STOCKITEMNAME.string, - "qty": qty, - "uom": uom, + "item_code": entry.STOCKITEMNAME.string.strip(), + "description": entry.STOCKITEMNAME.string.strip(), + "qty": qty.strip(), + "uom": uom.strip(), "conversion_factor": 1, - "price_list_rate": entry.RATE.string.split("/")[0], + "price_list_rate": entry.RATE.string.strip().split("/")[0], "cost_center": self.default_cost_center, "warehouse": self.default_warehouse, - account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string, self.erpnext_company), + account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(), self.erpnext_company), }) return items @@ -417,13 +417,13 @@ class TallyMigration(Document): ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST") taxes = [] for entry in ledger_entries: - if entry.ISPARTYLEDGER.string == "No": - tax_account = encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company) + if entry.ISPARTYLEDGER.string.strip() == "No": + tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company) taxes.append({ "charge_type": "Actual", "account_head": tax_account, "description": tax_account, - "tax_amount": entry.AMOUNT.string, + "tax_amount": entry.AMOUNT.string.strip(), "cost_center": self.default_cost_center, }) return taxes From fc078c1d455c52fef2a944b82138fe61c0d17979 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 16 Apr 2020 16:51:59 +0530 Subject: [PATCH 036/178] style: removed unused imports and updated formatting --- .../doctype/tally_migration/tally_migration.js | 2 +- .../doctype/tally_migration/tally_migration.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js index 682f78efb72..d84c8234efa 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Tally Migration', { let error_occurred = data.count === -1; if (data.count == data.total || error_occurred) { window.setTimeout((title) => { - frm.dashboard.hide_progress(title) + frm.dashboard.hide_progress(title); frm.reload_doc(); if (error_occurred) { frappe.msgprint({ diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 98a30273e63..13474e19ee1 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import json -import sys import re import traceback import zipfile @@ -18,7 +17,6 @@ from erpnext import encode_company_abbr from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification from frappe.model.document import Document from frappe.model.naming import getseries, revert_series_if_last from frappe.utils.data import format_datetime From 1a93977ef73ed9661caf8ad0f55a964981e15751 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 19 Apr 2020 12:47:48 +0530 Subject: [PATCH 037/178] fix: filters as dictionary --- erpnext/support/doctype/issue/issue.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index b748e3fa46e..11fbc82a356 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -338,8 +338,13 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = False if is_website_user(): - if not filters: filters = [] - filters.append(("Issue", "customer", "=", customer)) if customer else filters.append(("Issue", "raised_by", "=", user)) + if not filters: filters = {} + + if customer: + filters["customer"] = customer + else: + filters["raised_by"] = user + ignore_permissions = True return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) From 7ef57533ec9d226396b31666bc8bd77855020b09 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 21 Apr 2020 00:19:19 +0530 Subject: [PATCH 038/178] fix: free item quantity issue --- erpnext/public/js/controllers/transaction.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a326fcc299a..47249ea9495 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1396,6 +1396,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ for (let key in free_item_data) { row_to_modify[key] = free_item_data[key]; } + } if (items && items.length && free_item_data) { + items[0].qty = free_item_data.qty } }, From c62cf98d7eee5d2dc6e19996db19c166f0d5fb79 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 21 Apr 2020 11:26:49 +0530 Subject: [PATCH 039/178] fix: scrap items order not showing correctly in stock entry (#21347) --- .../stock/doctype/stock_entry/stock_entry.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 58131ea76be..f3737ac5f7d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -862,14 +862,6 @@ class StockEntry(StockController): self.add_to_stock_entry_detail(item_dict) - if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: - scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) - for item in itervalues(scrap_item_dict): - if self.pro_doc and self.pro_doc.scrap_warehouse: - item["to_warehouse"] = self.pro_doc.scrap_warehouse - - self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) - # fetch the serial_no of the first stock entry for the second stock entry if self.work_order and self.purpose == "Manufacture": self.set_serial_nos(self.work_order) @@ -880,9 +872,20 @@ class StockEntry(StockController): if self.purpose in ("Manufacture", "Repack"): self.load_items_from_bom() + self.set_scrap_items() self.set_actual_qty() self.calculate_rate_and_amount(raise_error_if_no_rate=False) + def set_scrap_items(self): + if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: + scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) + for item in itervalues(scrap_item_dict): + item.idx = '' + if self.pro_doc and self.pro_doc.scrap_warehouse: + item["to_warehouse"] = self.pro_doc.scrap_warehouse + + self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + def set_work_order_details(self): if not getattr(self, "pro_doc", None): self.pro_doc = frappe._dict() From 9453add3dd9ff14de0de7a9a9d5e7d8ee692f506 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 21 Apr 2020 12:53:17 +0530 Subject: [PATCH 040/178] fix: Re-order Item Error Email format (#21343) * fix: Re-order Item Error Email format * fix: Translated strings --- erpnext/stock/reorder_item.py | 20 +++++++++----------- erpnext/utilities/transaction_base.py | 4 +++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 97776739a87..4c721acdc12 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe import erpnext +import json from frappe.utils import flt, nowdate, add_days, cint from frappe import _ @@ -198,19 +199,16 @@ def send_email_notification(mr_list): subject=_('Auto Material Requests Generated'), message = msg) def notify_errors(exceptions_list): - subject = "[Important] [ERPNext] Auto Reorder Errors" - content = """Dear System Manager, + subject = _("[Important] [ERPNext] Auto Reorder Errors") + content = _("Dear System Manager,") + "
" + _("An error occured for certain Items while creating Material Requests based on Re-order level. \ + Please rectify these issues :") + "
" -An error occured for certain Items while creating Material Requests based on Re-order level. + for exception in exceptions_list: + exception = json.loads(exception) + error_message = """
{0}

""".format(_(exception.get("message"))) + content += error_message -Please rectify these issues: ---- -
-%s
-
---- -Regards, -Administrator""" % ("\n\n".join(exceptions_list),) + content += _("Regards,") + "
" + _("Administrator") from frappe.email import sendmail_to_system_managers sendmail_to_system_managers(subject, content) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 20998108467..2815e700974 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -176,4 +176,6 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): qty = d.get(f) if qty: if abs(cint(qty) - flt(qty)) > 0.0000001: - frappe.throw(_("Quantity ({0}) cannot be a fraction in row {1}").format(qty, d.idx), UOMMustBeIntegerError) + frappe.throw(_("Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.") \ + .format(qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))), + UOMMustBeIntegerError) From fb20982194d3a758d9a7f86758cae5ab3dc29c5c Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 21 Apr 2020 12:57:08 +0530 Subject: [PATCH 041/178] fix: on item change UOM not updated (#21254) * fix: on item change UOM not updated * removing uom from js --- erpnext/public/js/controllers/transaction.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 47249ea9495..85d9799ce56 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -499,7 +499,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ conversion_factor: item.conversion_factor, weight_per_unit: item.weight_per_unit, weight_uom: item.weight_uom, - uom : item.uom, manufacturer: item.manufacturer, stock_uom: item.stock_uom, pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '', From 08c782709aa919937c3ec69e4009e88c230d908c Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 21 Apr 2020 17:03:56 +0530 Subject: [PATCH 042/178] Better error message --- erpnext/utilities/transaction_base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 20998108467..f88ffd44e3c 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import frappe.share from frappe import _ -from frappe.utils import cstr, now_datetime, cint, flt, get_time +from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_link_to_form from erpnext.controllers.status_updater import StatusUpdater from six import string_types @@ -123,8 +123,11 @@ class TransactionBase(StatusUpdater): ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate") if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= .01: - frappe.throw(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ") + frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ") .format(d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate)) + frappe.throw(_("To allow different rates, disable the {0} checkbox in {1}.") + .format(frappe.bold("Maintain Same Rate Throughout Sales Cycle"), + get_link_to_form("Selling Settings", "Selling Settings", frappe.bold("Selling Settings")))) def get_link_filters(self, for_doctype): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): From 56e7887511542859d52bd8f10e5b622c5dffc0e2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 22 Apr 2020 02:45:42 +0530 Subject: [PATCH 043/178] fix: unsupported operand type issue in pricing rule --- erpnext/accounts/doctype/pricing_rule/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 100bb1d3e38..b358f56671f 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -330,9 +330,9 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): if pr_doc.mixed_conditions: amt = args.get('qty') * args.get("price_list_rate") if args.get("item_code") != row.get("item_code"): - amt = row.get('qty') * (row.get("price_list_rate") or args.get("rate")) + amt = flt(row.get('qty')) * flt(row.get("price_list_rate") or args.get("rate")) - sum_qty += row.get("stock_qty") or args.get("stock_qty") or args.get("qty") + sum_qty += flt(row.get("stock_qty")) or flt(args.get("stock_qty")) or flt(args.get("qty")) sum_amt += amt if pr_doc.is_cumulative: From 499f9198b9ff9b0af80b4dbdfb3b51d8ec36ba88 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Wed, 22 Apr 2020 11:37:09 +0530 Subject: [PATCH 044/178] fix: better error message due date (#21366) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 95fe6a2f3fa..d3eb2a82885 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -834,7 +834,7 @@ class AccountsController(TransactionBase): for d in self.get("payment_schedule"): if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date): - frappe.throw(_("Row {0}: Due Date cannot be before posting date").format(d.idx)) + frappe.throw(_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx)) elif d.due_date in dates: li.append(_("{0} in row {1}").format(d.due_date, d.idx)) dates.append(d.due_date) From 0578cf5a73fe8bc32464acd2fe4fd4f32031230b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 22 Apr 2020 11:50:06 +0530 Subject: [PATCH 045/178] fix: specify column width --- .../customer_acquisition_and_loyalty.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 28dd0564075..aa57665a815 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -53,10 +53,11 @@ def execute(filters=None): new[1], repeat[1], new[1] + repeat[1]]) return [ - _("Year"), _("Month"), - _("New Customers") + ":Int", - _("Repeat Customers") + ":Int", - _("Total") + ":Int", + _("Year") + "::100", + _("Month") + "::100", + _("New Customers") + ":Int:100", + _("Repeat Customers") + ":Int:100", + _("Total") + ":Int:100", _("New Customer Revenue") + ":Currency:150", _("Repeat Customer Revenue") + ":Currency:150", _("Total Revenue") + ":Currency:150" From f2c43ca81e30020df0fdc30d83c74d5956604652 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 22 Apr 2020 16:08:36 +0530 Subject: [PATCH 046/178] fix: patch and validation message to fix target warehouse issue (#21359) --- erpnext/controllers/selling_controller.py | 9 +++ erpnext/patches.txt | 3 +- ...ock_ledger_entries_for_target_warehouse.py | 71 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 9a9f3d1d319..de771a55f2c 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -46,6 +46,7 @@ class SellingController(StockController): set_default_income_account_for_item(self) self.set_customer_address() self.validate_for_duplicate_items() + self.validate_target_warehouse() def set_missing_values(self, for_validate=False): @@ -402,6 +403,14 @@ class SellingController(StockController): else: chk_dupl_itm.append(f) + def validate_target_warehouse(self): + items = self.get("items") + (self.get("packed_items") or []) + + for d in items: + if d.get("target_warehouse") and d.get("warehouse") == d.get("target_warehouse"): + warehouse = frappe.bold(d.get("target_warehouse")) + frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same") + .format(d.idx, warehouse, warehouse)) def validate_items(self): # validate items to see if they have is_sales_item enabled diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a7013ff109f..9461e2df839 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -656,4 +656,5 @@ erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.recalculate_requested_qty_in_bin erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_correct_status_for_expense_claim -erpnext.patches.v12_0.set_updated_purpose_in_pick_list \ No newline at end of file +erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py new file mode 100644 index 00000000000..13e935b2d39 --- /dev/null +++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py @@ -0,0 +1,71 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + warehouse_perm = frappe.get_all("User Permission", + fields=["count(*) as p_count", "is_default", "user"], filters={"allow": "Warehouse"}, group_by="user") + + if not warehouse_perm: + return + + execute_patch = False + for perm_data in warehouse_perm: + if perm_data.p_count == 1 or (perm_data.p_count > 1 and frappe.get_all("User Permission", + filters = {"user": perm_data.user, "allow": "warehouse", "is_default": 1}, limit=1)): + execute_patch = True + break + + if not execute_patch: return + + for doctype in ["Sales Invoice", "Delivery Note"]: + if not frappe.get_meta(doctype + ' Item').get_field("target_warehouse").hidden: continue + + cond = "" + if doctype == "Sales Invoice": + cond = " AND parent_doc.update_stock = 1" + + data = frappe.db.sql(""" SELECT parent_doc.name as name, child_doc.name as child_name + FROM + `tab{doctype}` parent_doc, `tab{doctype} Item` child_doc + WHERE + parent_doc.name = child_doc.parent AND parent_doc.docstatus < 2 + AND child_doc.target_warehouse is not null AND child_doc.target_warehouse != '' + AND child_doc.creation > '2020-04-16' {cond} + """.format(doctype=doctype, cond=cond), as_dict=1) + + if data: + names = [d.child_name for d in data] + frappe.db.sql(""" UPDATE `tab{0} Item` set target_warehouse = null + WHERE name in ({1}) """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + + frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null + WHERE parenttype = '{0}' and parent_detail_docname in ({1}) + """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + + parent_names = list(set([d.name for d in data])) + + for d in parent_names: + doc = frappe.get_doc(doctype, d) + if doc.docstatus != 1: continue + + doc.docstatus = 2 + doc.update_stock_ledger() + doc.make_gl_entries_on_cancel(repost_future_gle=False) + + # update stock & gl entries for submit state of PR + doc.docstatus = 1 + doc.update_stock_ledger() + doc.make_gl_entries() + + if frappe.get_meta('Sales Order Item').get_field("target_warehouse").hidden: + frappe.db.sql(""" UPDATE `tabSales Order Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 """) + + frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """) + + + From c5d108c954eee2841593eb4b0516ec7ba58a885e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Apr 2020 00:38:19 +0530 Subject: [PATCH 047/178] chore: Commonify autofilling warehouses in child tables --- .../doctype/work_order/work_order.js | 5 +++ erpnext/public/js/controllers/buying.js | 7 ++++ erpnext/public/js/controllers/transaction.js | 15 +++------ erpnext/selling/sales_common.js | 2 +- .../doctype/delivery_note/delivery_note.js | 8 +++++ .../stock/doctype/stock_entry/stock_entry.js | 32 +++---------------- 6 files changed, 31 insertions(+), 38 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 2d8d75d1f94..d14c8d82f11 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -122,6 +122,11 @@ frappe.ui.form.on("Work Order", { } }, + source_warehouse: function(frm) { + let transaction_controller = new erpnext.TransactionController(); + transaction_controller.autofill_warehouse(frm.doc.required_items, "source_warehouse", frm.doc.source_warehouse); + }, + refresh: function(frm) { erpnext.toggle_naming_series(); erpnext.work_order.set_custom_buttons(frm); diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index afbdbc661d3..802cc056c6a 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -253,6 +253,13 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ } }, + rejected_warehouse: function(doc, cdt) { + // trigger autofill_warehouse only if parent rejected_warehouse field is triggered + if (["Purchase Invoice", "Purchase Receipt"].includes(cdt)) { + this.autofill_warehouse(doc.items, "rejected_warehouse", doc.rejected_warehouse); + } + }, + category: function(doc, cdt, cdn) { // should be the category field of tax table if(cdt != doc.doctype) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 85d9799ce56..215282161d1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1843,21 +1843,16 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, set_reserve_warehouse: function() { - this.autofill_warehouse("reserve_warehouse"); + this.autofill_warehouse(this.frm.doc.supplied_items, "reserve_warehouse", this.frm.doc.set_reserve_warehouse); }, set_warehouse: function() { - this.autofill_warehouse("warehouse"); + this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); }, - autofill_warehouse : function (warehouse_field) { - // set warehouse in all child table rows - var me = this; - let warehouse = (warehouse_field === "warehouse") ? me.frm.doc.set_warehouse : me.frm.doc.set_reserve_warehouse; - let child_table = (warehouse_field === "warehouse") ? me.frm.doc.items : me.frm.doc.supplied_items; - let doctype = (warehouse_field === "warehouse") ? (me.frm.doctype + " Item") : (me.frm.doctype + " Item Supplied"); - - if(warehouse) { + autofill_warehouse : function (child_table, warehouse_field, warehouse) { + let doctype = child_table[0].doctype; + if (warehouse) { $.each(child_table || [], function(i, item) { frappe.model.set_value(doctype, item.name, warehouse_field, warehouse); }); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 095b7c3dffa..4a7dd5ad9b4 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -429,7 +429,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ if (doc.has_serial_no && doc.serial_no) { args['serial_no'] = doc.serial_no } - + return frappe.call({ method: 'erpnext.stock.doctype.batch.batch.get_batch_no', args: args, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 2ee68723789..62aebbaf504 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -267,6 +267,14 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( frappe.ui.form.is_saving = false; } }) + }, + + to_warehouse: function() { + let packed_items_table = this.frm.doc["packed_items"]; + this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse); + if (packed_items_table && packed_items_table.length) { + this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse); + } } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index c54e3cdfb87..b3923bc4437 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -799,39 +799,17 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; }, - source_mandatory: ["Material Issue", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"], - target_mandatory: ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"], - from_warehouse: function(doc) { - var me = this; - this.set_warehouse_if_different("s_warehouse", doc.from_warehouse, function(row) { - return me.source_mandatory.indexOf(me.frm.doc.purpose)!==-1; - }); + this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse); }, to_warehouse: function(doc) { - var me = this; - this.set_warehouse_if_different("t_warehouse", doc.to_warehouse, function(row) { - return me.target_mandatory.indexOf(me.frm.doc.purpose)!==-1; - }); + this.set_warehouse_in_children(doc.items, "t_warehouse", doc.to_warehouse); }, - set_warehouse_if_different: function(fieldname, value, condition) { - var changed = false; - for (var i=0, l=(this.frm.doc.items || []).length; i Date: Thu, 23 Apr 2020 09:44:54 +0530 Subject: [PATCH 048/178] fix: incorrect out value in stock balance due to precision issue (#21378) --- erpnext/stock/report/stock_balance/stock_balance.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index ff03381389c..ab87ee114d4 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -170,6 +170,8 @@ def get_item_warehouse_map(filters, sle): from_date = getdate(filters.get("from_date")) to_date = getdate(filters.get("to_date")) + float_precision = cint(frappe.db.get_default("float_precision")) or 3 + for d in sle: key = (d.company, d.item_code, d.warehouse) if key not in iwb_map: @@ -184,7 +186,7 @@ def get_item_warehouse_map(filters, sle): qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)] if d.voucher_type == "Stock Reconciliation": - qty_diff = flt(d.qty_after_transaction) - qty_dict.bal_qty + qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) else: qty_diff = flt(d.actual_qty) @@ -195,7 +197,7 @@ def get_item_warehouse_map(filters, sle): qty_dict.opening_val += value_diff elif d.posting_date >= from_date and d.posting_date <= to_date: - if qty_diff > 0: + if flt(qty_diff, float_precision) >= 0: qty_dict.in_qty += qty_diff qty_dict.in_val += value_diff else: @@ -206,16 +208,15 @@ def get_item_warehouse_map(filters, sle): qty_dict.bal_qty += qty_diff qty_dict.bal_val += value_diff - iwb_map = filter_items_with_no_transactions(iwb_map) + iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) return iwb_map -def filter_items_with_no_transactions(iwb_map): +def filter_items_with_no_transactions(iwb_map, float_precision): for (company, item, warehouse) in sorted(iwb_map): qty_dict = iwb_map[(company, item, warehouse)] no_transactions = True - float_precision = cint(frappe.db.get_default("float_precision")) or 3 for key, val in iteritems(qty_dict): val = flt(val, float_precision) qty_dict[key] = val From f49e66e72118324faf0269bb2c37d7827aa7693d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 Apr 2020 09:45:26 +0530 Subject: [PATCH 049/178] fix: BOM stock report (#21377) Co-authored-by: Nabin Hait --- .../report/bom_stock_report/bom_stock_report.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 65f4d08459a..75ebcbc971b 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -18,10 +18,10 @@ def get_columns(): """return columns""" columns = [ _("Item") + ":Link/Item:150", - _("Description") + "::500", - _("Qty per BOM Line") + ":Float:100", - _("Required Qty") + ":Float:100", - _("In Stock Qty") + ":Float:100", + _("Description") + "::300", + _("BOM Qty") + ":Float:160", + _("Required Qty") + ":Float:120", + _("In Stock Qty") + ":Float:120", _("Enough Parts to Build") + ":Float:200", ] @@ -59,13 +59,14 @@ def get_bom_stock(filters): bom_item.item_code, bom_item.description , bom_item.{qty_field}, - bom_item.{qty_field} * {qty_to_produce}, + bom_item.{qty_field} * {qty_to_produce} / bom.quantity, sum(ledger.actual_qty) as actual_qty, - sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce}))) + sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) FROM - {table} AS bom_item + `tabBOM` AS bom INNER JOIN {table} AS bom_item + ON bom.name = bom_item.parent LEFT JOIN `tabBin` AS ledger - ON bom_item.item_code = ledger.item_code + ON bom_item.item_code = ledger.item_code {conditions} WHERE bom_item.parent = '{bom}' and bom_item.parenttype='BOM' From 794fd75ca10b471a5490b3ad5db6c98506f5c6d9 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 Apr 2020 09:48:31 +0530 Subject: [PATCH 050/178] fix: bom update cost is not working (#21348) * fix: bom update cost is not working * added test case for bom cost --- erpnext/manufacturing/doctype/bom/bom.py | 3 +- .../bom_update_tool/bom_update_tool.py | 7 ++-- .../bom_update_tool/test_bom_update_tool.py | 32 ++++++++++++++++++- .../production_plan/test_production_plan.py | 6 ++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8d3aa9118c7..c898d378c3a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -236,12 +236,13 @@ class BOM(WebsiteGenerator): if rate: d.rate = rate d.amount = flt(d.rate) * flt(d.qty) + d.db_update() if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True self.calculate_cost() if save: - self.save() + self.db_update() self.update_exploded_items() # update parent BOMs diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 2758a423716..e6c10ad12b0 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -82,7 +82,7 @@ def enqueue_replace_bom(args): @frappe.whitelist() def enqueue_update_cost(): - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost") + frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000) frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) def update_latest_price_in_all_boms(): @@ -98,6 +98,9 @@ def replace_bom(args): doc.replace_bom() def update_cost(): + frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) \ No newline at end of file + frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + + frappe.db.auto_commit_on_many_writes = 0 \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 154addf14e8..ac9a409bcbe 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -5,6 +5,9 @@ from __future__ import unicode_literals import unittest import frappe +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost test_records = frappe.get_test_records('BOM') @@ -27,4 +30,31 @@ class TestBOMUpdateTool(unittest.TestCase): # reverse, as it affects other testcases update_tool.current_bom = bom_doc.name update_tool.new_bom = current_bom - update_tool.replace_bom() \ No newline at end of file + update_tool.replace_bom() + + def test_bom_cost(self): + for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: + item_doc = create_item(item, valuation_rate=100) + if item_doc.valuation_rate != 100.00: + frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100) + + bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name") + if not bom_no: + doc = make_bom(item = 'BOM Cost Test Item 1', + raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR") + else: + doc = frappe.get_doc("BOM", bom_no) + + self.assertEquals(doc.total_cost, 200) + + frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200) + update_cost() + + doc.load_from_db() + self.assertEquals(doc.total_cost, 300) + + frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100) + update_cost() + + doc.load_from_db() + self.assertEquals(doc.total_cost, 200) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f70c9cc43fc..26f580db339 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -192,9 +192,10 @@ def make_bom(**args): args = frappe._dict(args) bom = frappe.get_doc({ - 'doctype': "BOM", + 'doctype': 'BOM', 'is_default': 1, 'item': args.item, + 'currency': args.currency or 'USD', 'quantity': args.quantity or 1, 'company': args.company or '_Test Company' }) @@ -211,4 +212,5 @@ def make_bom(**args): }) bom.insert(ignore_permissions=True) - bom.submit() \ No newline at end of file + bom.submit() + return bom \ No newline at end of file From dd560d676e587fdb14f70ac0fea1de33009e6aa2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 23 Apr 2020 10:35:35 +0530 Subject: [PATCH 051/178] fix: Budget against accounting dimensions (#21269) * fix: Budget warning against custom accounting dimension * fix: Codacy --- erpnext/accounts/doctype/budget/budget.py | 66 +++++++++++-------- .../accounts/doctype/budget/test_budget.py | 37 ++++++----- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 084514cbfa7..d93b6ffbaf9 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -9,6 +9,7 @@ from frappe.utils import flt, getdate, add_months, get_last_day, fmt_money, nowd from frappe.model.naming import make_autoname from erpnext.accounts.utils import get_fiscal_year from frappe.model.document import Document +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class BudgetError(frappe.ValidationError): pass class DuplicateBudgetError(frappe.ValidationError): pass @@ -98,30 +99,32 @@ def validate_expense_against_budget(args): if not (args.get('account') and args.get('cost_center')) and args.item_code: args.cost_center, args.account = get_item_details(args) - if not (args.cost_center or args.project) and not args.account: + if not args.account: return - for budget_against in ['project', 'cost_center']: + for budget_against in ['project', 'cost_center'] + get_accounting_dimensions(): if (args.get(budget_against) and args.account and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})): - if args.project and budget_against == 'project': - condition = "and b.project=%s" % frappe.db.escape(args.project) - args.budget_against_field = "Project" + doctype = frappe.unscrub(budget_against) - elif args.cost_center and budget_against == 'cost_center': - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", args.cost_center, ["lft", "rgt"]) - condition = """and exists(select name from `tabCost Center` - where lft<=%s and rgt>=%s and name=b.cost_center)""" % (cc_lft, cc_rgt) - args.budget_against_field = "Cost Center" + if frappe.get_cached_value('DocType', doctype, 'is_tree'): + lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) + condition = """and exists(select name from `tab%s` + where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec + args.is_tree = True + else: + condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against))) + args.is_tree = False - args.budget_against = args.get(budget_against) + args.budget_against_field = budget_against + args.budget_against_doctype = doctype budget_records = frappe.db.sql(""" select b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution, ifnull(b.applicable_on_material_request, 0) as for_material_request, - ifnull(applicable_on_purchase_order,0) as for_purchase_order, + ifnull(applicable_on_purchase_order, 0) as for_purchase_order, ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses, b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded, b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr, @@ -132,9 +135,7 @@ def validate_expense_against_budget(args): b.name=ba.parent and b.fiscal_year=%s and ba.account=%s and b.docstatus=1 {condition} - """.format(condition=condition, - budget_against_field=frappe.scrub(args.get("budget_against_field"))), - (args.fiscal_year, args.account), as_dict=True) + """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec if budget_records: validate_budget_records(args, budget_records) @@ -230,10 +231,10 @@ def get_ordered_amount(args, budget): def get_other_condition(args, budget, for_doc): condition = "expense_account = '%s'" % (args.expense_account) - budget_against_field = frappe.scrub(args.get("budget_against_field")) + budget_against_field = args.get("budget_against_field") if budget_against_field and args.get(budget_against_field): - condition += " and child.%s = '%s'" %(budget_against_field, args.get(budget_against_field)) + condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field)) if args.get('fiscal_year'): date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date' @@ -246,19 +247,30 @@ def get_other_condition(args, budget, for_doc): return condition def get_actual_expense(args): + if not args.budget_against_doctype: + args.budget_against_doctype = frappe.unscrub(args.budget_against_field) + + budget_against_field = args.get('budget_against_field') condition1 = " and gle.posting_date <= %(month_end_date)s" \ if args.get("month_end_date") else "" - if args.budget_against_field == "Cost Center": - lft_rgt = frappe.db.get_value(args.budget_against_field, - args.budget_against, ["lft", "rgt"], as_dict=1) + + if args.is_tree: + lft_rgt = frappe.db.get_value(args.budget_against_doctype, + args.get(budget_against_field), ["lft", "rgt"], as_dict=1) + args.update(lft_rgt) - condition2 = """and exists(select name from `tabCost Center` - where lft>=%(lft)s and rgt<=%(rgt)s and name=gle.cost_center)""" - elif args.budget_against_field == "Project": - condition2 = "and exists(select name from `tabProject` where name=gle.project and gle.project = %(budget_against)s)" + condition2 = """and exists(select name from `tab{doctype}` + where lft>=%(lft)s and rgt<=%(rgt)s + and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec + budget_against_field=budget_against_field) + else: + condition2 = """and exists(select name from `tab{doctype}` + where name=gle.{budget_against} and + gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype, + budget_against = budget_against_field) - return flt(frappe.db.sql(""" + amount = flt(frappe.db.sql(""" select sum(gle.debit) - sum(gle.credit) from `tabGL Entry` gle where gle.account=%(account)s @@ -267,7 +279,9 @@ def get_actual_expense(args): and gle.company=%(company)s and gle.docstatus=1 {condition2} - """.format(condition1=condition1, condition2=condition2), (args))[0][0]) + """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec + + return amount def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): distribution = {} diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 33aefd67d1c..9c19791d29a 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -13,7 +13,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ class TestBudget(unittest.TestCase): def test_monthly_budget_crossed_ignore(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -26,7 +26,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_crossed_stop1(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -41,7 +41,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_exception_approver_role(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -114,7 +114,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_crossed_stop2(self): - set_total_expense_zero("2013-02-28", "Project") + set_total_expense_zero("2013-02-28", "project") budget = make_budget(budget_against="Project") @@ -129,7 +129,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_yearly_budget_crossed_stop1(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -141,7 +141,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_yearly_budget_crossed_stop2(self): - set_total_expense_zero("2013-02-28", "Project") + set_total_expense_zero("2013-02-28", "project") budget = make_budget(budget_against="Project") @@ -153,7 +153,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_on_cancellation1(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -177,7 +177,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_on_cancellation2(self): - set_total_expense_zero("2013-02-28", "Project") + set_total_expense_zero("2013-02-28", "project") budget = make_budget(budget_against="Project") @@ -201,8 +201,8 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_against_group_cost_center(self): - set_total_expense_zero("2013-02-28", "Cost Center") - set_total_expense_zero("2013-02-28", "Cost Center", "_Test Cost Center 2 - _TC") + set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero("2013-02-28", "cost_center", "_Test Cost Center 2 - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") @@ -241,25 +241,30 @@ class TestBudget(unittest.TestCase): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): - if budget_against_field == "Project": + if budget_against_field == "project": budget_against = "_Test Project" else: budget_against = budget_against_CC or "_Test Cost Center - _TC" - existing_expense = get_actual_expense(frappe._dict({ + + args = frappe._dict({ "account": "_Test Account Cost for Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", "monthly_end_date": posting_date, "company": "_Test Company", "fiscal_year": "_Test Fiscal Year 2013", "budget_against_field": budget_against_field, - "budget_against": budget_against - })) + }) + + if not args.get(budget_against_field): + args[budget_against_field] = budget_against + + existing_expense = get_actual_expense(args) if existing_expense: - if budget_against_field == "Cost Center": + if budget_against_field == "cost_center": make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True) - elif budget_against_field == "Project": + elif budget_against_field == "project": make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date="2013-02-28") From 097c643a59899b8cb2c913befb71162585290ec0 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 22 Apr 2020 23:38:42 +0530 Subject: [PATCH 052/178] setting end date in email campaign --- .../doctype/email_campaign/email_campaign.py | 2 +- erpnext/patches.txt | 1 + .../update_end_date_and_status_in_email.py | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/update_end_date_and_status_in_email.py diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 00a4bd1a322..8f60ecf6219 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -27,7 +27,7 @@ class EmailCampaign(Document): for entry in campaign.get("campaign_schedules"): send_after_days.append(entry.send_after_days) try: - end_date = add_days(getdate(self.start_date), max(send_after_days)) + self.end_date = add_days(getdate(self.start_date), max(send_after_days)) except ValueError: frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9461e2df839..dd2705ab6b2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -658,3 +658,4 @@ erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_correct_status_for_expense_claim erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse +erpnext.patches.v12_0.update_end_date_and_status_in_email diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email.py new file mode 100644 index 00000000000..b3753a5e8db --- /dev/null +++ b/erpnext/patches/v12_0/update_end_date_and_status_in_email.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import add_days, getdate, today + +def execute(): + email_campaign = frappe.get_all('Email Campaign') + if not email_campaign: + return + for campaign in email_campaign: + doc = frappe.get_doc("Email Campaign",campaign["name"]) + send_after_days = [] + + camp = frappe.get_doc("Campaign", doc.campaign_name) + for entry in camp.get("campaign_schedules"): + send_after_days.append(entry.send_after_days) + if send_after_days: + end_date = add_days(getdate(doc.start_date), max(send_after_days)) + doc.db_set("end_date", end_date) + today_date = getdate(today()) + if doc.start_date > today_date: + doc.db_set("status", "Scheduled") + elif end_date >= today_date: + doc.db_set("status", "In Progress") + elif end_date < today_date: + doc.db_set("status", "Completed") + frappe.db.commit() \ No newline at end of file From ef7f9c6ecc3557528679dd6a7daea43f95b18ea9 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 23 Apr 2020 13:45:19 +0530 Subject: [PATCH 053/178] Review changes --- erpnext/patches.txt | 2 +- .../update_end_date_and_status_in_email.py | 26 ------------------- ...e_end_date_and_status_in_email_campaign.py | 24 +++++++++++++++++ 3 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 erpnext/patches/v12_0/update_end_date_and_status_in_email.py create mode 100644 erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index dd2705ab6b2..ad02a68faaa 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -658,4 +658,4 @@ erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_correct_status_for_expense_claim erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse -erpnext.patches.v12_0.update_end_date_and_status_in_email +erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email.py deleted file mode 100644 index b3753a5e8db..00000000000 --- a/erpnext/patches/v12_0/update_end_date_and_status_in_email.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe.utils import add_days, getdate, today - -def execute(): - email_campaign = frappe.get_all('Email Campaign') - if not email_campaign: - return - for campaign in email_campaign: - doc = frappe.get_doc("Email Campaign",campaign["name"]) - send_after_days = [] - - camp = frappe.get_doc("Campaign", doc.campaign_name) - for entry in camp.get("campaign_schedules"): - send_after_days.append(entry.send_after_days) - if send_after_days: - end_date = add_days(getdate(doc.start_date), max(send_after_days)) - doc.db_set("end_date", end_date) - today_date = getdate(today()) - if doc.start_date > today_date: - doc.db_set("status", "Scheduled") - elif end_date >= today_date: - doc.db_set("status", "In Progress") - elif end_date < today_date: - doc.db_set("status", "Completed") - frappe.db.commit() \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py new file mode 100644 index 00000000000..f6561471de4 --- /dev/null +++ b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import add_days, getdate, today + +def execute(): + if frappe.db.exists('DocType', 'Email Campaign'): + email_campaign = frappe.get_all('Email Campaign') + for campaign in email_campaign: + doc = frappe.get_doc("Email Campaign",campaign["name"]) + send_after_days = [] + + camp = frappe.get_doc("Campaign", doc.campaign_name) + for entry in camp.get("campaign_schedules"): + send_after_days.append(entry.send_after_days) + if send_after_days: + end_date = add_days(getdate(doc.start_date), max(send_after_days)) + doc.db_set("end_date", end_date) + today_date = getdate(today()) + if doc.start_date > today_date: + doc.db_set("status", "Scheduled") + elif end_date >= today_date: + doc.db_set("status", "In Progress") + elif end_date < today_date: + doc.db_set("status", "Completed") \ No newline at end of file From bd4b5da11b1e393111805e442c36577229d7c09f Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 23 Apr 2020 16:09:08 +0530 Subject: [PATCH 054/178] feat: Payment allocation based on payment terms (#20946) * feat: Payment allocation based on payment terms * fix: Add desccription for checkbox Co-authored-by: Nabin Hait --- .../doctype/payment_entry/payment_entry.js | 17 +- .../doctype/payment_entry/payment_entry.py | 78 ++++- .../payment_entry/test_payment_entry.py | 67 +++- .../payment_entry_reference.json | 298 ++--------------- .../payment_schedule/payment_schedule.json | 305 +++++------------- .../payment_terms_template.json | 220 ++++--------- .../accounts_receivable.py | 17 +- erpnext/controllers/accounts_controller.py | 2 +- 8 files changed, 326 insertions(+), 678 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4584063ed69..74be4c4ef0f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -104,6 +104,21 @@ frappe.ui.form.on('Payment Entry', { }; }); + frm.set_query('payment_term', 'references', function(frm, cdt, cdn) { + const child = locals[cdt][cdn]; + if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) { + let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name}); + + payment_term_list = payment_term_list.map(pt => pt.payment_term); + + return { + filters: { + 'name': ['in', payment_term_list] + } + } + } + }); + frm.set_query("reference_name", "references", function(doc, cdt, cdn) { const child = locals[cdt][cdn]; const filters = {"docstatus": 1, "company": doc.company}; @@ -1018,4 +1033,4 @@ frappe.ui.form.on('Payment Entry', { }); } }, -}) +}) \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c89742cfb7f..e0258f32d36 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -71,9 +71,9 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_advance_paid() self.update_expense_claim() + self.update_payment_schedule() self.set_status() - def on_cancel(self): self.setup_party_account_field() self.make_gl_entries(cancel=1) @@ -81,6 +81,7 @@ class PaymentEntry(AccountsController): self.update_advance_paid() self.update_expense_claim() self.delink_advance_entry_references() + self.update_payment_schedule(cancel=1) self.set_payment_req_status() self.set_status() @@ -94,10 +95,10 @@ class PaymentEntry(AccountsController): def validate_duplicate_entry(self): reference_names = [] for d in self.get("references"): - if (d.reference_doctype, d.reference_name) in reference_names: + if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}") .format(d.idx, d.reference_doctype, d.reference_name)) - reference_names.append((d.reference_doctype, d.reference_name)) + reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) def set_bank_account_data(self): if self.bank_account: @@ -285,6 +286,36 @@ class PaymentEntry(AccountsController): frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry") .format(d.reference_name, dr_or_cr)) + def update_payment_schedule(self, cancel=0): + invoice_payment_amount_map = {} + invoice_paid_amount_map = {} + + for reference in self.get('references'): + if reference.payment_term and reference.reference_name: + key = (reference.payment_term, reference.reference_name) + invoice_payment_amount_map.setdefault(key, 0.0) + invoice_payment_amount_map[key] += reference.allocated_amount + + if not invoice_paid_amount_map.get(reference.reference_name): + payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, + fields=['paid_amount', 'payment_amount', 'payment_term']) + for term in payment_schedule: + invoice_key = (term.payment_term, reference.reference_name) + invoice_paid_amount_map.setdefault(invoice_key, {}) + invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount + + for key, amount in iteritems(invoice_payment_amount_map): + if cancel: + frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s + WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + else: + outstanding = invoice_paid_amount_map.get(key)['outstanding'] + if amount > outstanding: + frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) + + frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s + WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + def set_status(self): if self.docstatus == 2: self.status = 'Cancelled' @@ -1012,15 +1043,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): frappe.msgprint(_('{0} is on hold till {1}'.format(doc.name, doc.release_date))) else: - pe.append("references", { - 'reference_doctype': dt, - 'reference_name': dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - 'total_amount': grand_total, - 'outstanding_amount': outstanding_amount, - 'allocated_amount': outstanding_amount - }) + if (doc.doctype in ('Sales Invoice', 'Purchase Invoice') + and frappe.get_value('Payment Terms Template', + {'name': doc.payment_terms_template}, 'allocate_payment_based_on_payment_terms')): + + for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount): + pe.append('references', reference) + else: + pe.append("references", { + 'reference_doctype': dt, + 'reference_name': dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + 'total_amount': grand_total, + 'outstanding_amount': outstanding_amount, + 'allocated_amount': outstanding_amount + }) pe.setup_party_account_field() pe.set_missing_values() @@ -1029,6 +1067,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.set_amounts() return pe +def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): + references = [] + for payment_term in payment_schedule: + references.append({ + 'reference_doctype': dt, + 'reference_name': dn, + 'bill_no': doc.get('bill_no'), + 'due_date': doc.get('due_date'), + 'total_amount': grand_total, + 'outstanding_amount': outstanding_amount, + 'payment_term': payment_term.payment_term, + 'allocated_amount': flt(payment_term.payment_amount - payment_term.paid_amount, + payment_term.precision('payment_amount')) + }) + + return references def get_paid_amount(dt, dn, party_type, party, account, due_date): if party_type=="Customer": diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index a25e0e32c86..80a11b1bdff 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -149,6 +149,30 @@ class TestPaymentEntry(unittest.TestCase): outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 0) + def test_payment_entry_against_payment_terms(self): + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + create_payment_terms_template() + si.payment_terms_template = 'Test Receivable Template' + + si.append('taxes', { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18 + }) + si.save() + + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + pe.submit() + si.load_from_db() + + self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable') + self.assertEqual(pe.references[1].payment_term, 'Tax Receivable') + self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) + self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) def test_payment_against_sales_invoice_to_check_status(self): si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", @@ -175,13 +199,6 @@ class TestPaymentEntry(unittest.TestCase): pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", currency="USD", conversion_rate=50) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC") - pe.reference_no = "1" - pe.reference_date = "2016-01-01" - pe.source_exchange_rate = 50 - pe.insert() - pe.submit() - outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"]) self.assertEqual(flt(outstanding_amount), 0) self.assertEqual(status, 'Paid') @@ -609,4 +626,38 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(expected_party_account_balance, party_account_balance) accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() \ No newline at end of file + accounts_settings.save() + +def create_payment_terms_template(): + + create_payment_term('Basic Amount Receivable') + create_payment_term('Tax Receivable') + + if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'): + payment_term_template = frappe.get_doc({ + 'doctype': 'Payment Terms Template', + 'template_name': 'Test Receivable Template', + 'allocate_payment_based_on_payment_terms': 1, + 'terms': [{ + 'doctype': 'Payment Terms Template Detail', + 'payment_term': 'Basic Amount Receivable', + 'invoice_portion': 84.746, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 1 + }, + { + 'doctype': 'Payment Terms Template Detail', + 'payment_term': 'Tax Receivable', + 'invoice_portion': 15.254, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 2 + }] + }).insert() + + +def create_payment_term(name): + if not frappe.db.exists('Payment Term', name): + frappe.get_doc({ + 'doctype': 'Payment Term', + 'payment_term_name': name + }).insert() diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index b6a664393e8..8f5e9fbc286 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -1,343 +1,107 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2016-06-01 16:55:32.196722", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "reference_name", + "due_date", + "bill_no", + "payment_term", + "column_break_4", + "total_amount", + "outstanding_amount", + "allocated_amount", + "exchange_rate" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "reference_doctype", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "reference_name", "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, "in_list_view": 1, - "in_standard_filter": 0, "label": "Name", - "length": 0, - "no_copy": 0, "options": "reference_doctype", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "due_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Due Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "bill_no", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Supplier Invoice No", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "total_amount", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Total Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "outstanding_amount", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Outstanding", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "allocated_amount", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Allocated", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Allocated" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:(doc.reference_doctype=='Purchase Invoice')", - "fetch_if_empty": 0, "fieldname": "exchange_rate", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Exchange Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 + }, + { + "fieldname": "payment_term", + "fieldtype": "Link", + "label": "Payment Term", + "options": "Payment Term" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-05-01 13:24:56.586677", + "links": [], + "modified": "2020-03-13 12:07:19.362539", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", - "name_case": "", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index 1b38904f6da..d363cf161b5 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -1,243 +1,82 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-08-10 15:38:00.080575", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-08-10 15:38:00.080575", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_term", + "description", + "due_date", + "invoice_portion", + "payment_amount", + "mode_of_payment", + "paid_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "payment_term", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payment Term", - "length": 0, - "no_copy": 0, - "options": "Payment Term", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "payment_term", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Term", + "options": "Payment Term", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_from": "", - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "due_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Due Date", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "due_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Due Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_from": "", - "fieldname": "invoice_portion", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Invoice Portion", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Invoice Portion", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "payment_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payment Amount", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "payment_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Payment Amount", + "options": "currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mode of Payment", - "length": 0, - "no_copy": 0, - "options": "Mode of Payment", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "fieldname": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-09-06 17:35:44.580209", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Schedule", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-03-13 17:58:24.729526", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json index 7a3483d6c32..c4a2a888182 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json @@ -1,164 +1,84 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:template_name", - "beta": 0, - "creation": "2017-08-10 15:34:28.058054", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:template_name", + "creation": "2017-08-10 15:34:28.058054", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "template_name", + "allocate_payment_based_on_payment_terms", + "terms" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "template_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Template Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "template_name", + "fieldtype": "Data", + "label": "Template Name", + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "terms", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payment Terms", - "length": 0, - "no_copy": 0, - "options": "Payment Terms Template Detail", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "terms", + "fieldtype": "Table", + "label": "Payment Terms", + "options": "Payment Terms Template Detail", + "reqd": 1 + }, + { + "default": "0", + "description": "If this checkbox is checked, paid amount will be splitted and allocated as per the amounts in payment schedule against each payment term", + "fieldname": "allocate_payment_based_on_payment_terms", + "fieldtype": "Check", + "label": "Allocate Payment Based On Payment Terms" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-01-24 11:13:31.158613", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Terms Template", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-04-01 15:35:18.112619", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Terms Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 240b0d83817..e9c286fcf0d 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -344,26 +344,28 @@ class ReceivablePayableReport(object): def allocate_outstanding_based_on_payment_terms(self, row): self.get_payment_terms(row) for term in row.payment_terms: - term.outstanding = term.invoiced # update "paid" and "oustanding" for this term - self.allocate_closing_to_term(row, term, 'paid') + if not term.paid: + self.allocate_closing_to_term(row, term, 'paid') # update "credit_note" and "oustanding" for this term if term.outstanding: self.allocate_closing_to_term(row, term, 'credit_note') + row.payment_terms = sorted(row.payment_terms, key=lambda x: x['due_date']) + def get_payment_terms(self, row): # build payment_terms for row payment_terms_details = frappe.db.sql(""" select si.name, si.party_account_currency, si.currency, si.conversion_rate, - ps.due_date, ps.payment_amount, ps.description + ps.due_date, ps.payment_amount, ps.description, ps.paid_amount from `tab{0}` si, `tabPayment Schedule` ps where si.name = ps.parent and si.name = %s - order by ps.due_date + order by ps.paid_amount desc, due_date """.format(row.voucher_type), row.voucher_no, as_dict = 1) @@ -389,11 +391,14 @@ class ReceivablePayableReport(object): "invoiced": invoiced, "invoice_grand_total": row.invoiced, "payment_term": d.description, - "paid": 0.0, + "paid": d.paid_amount, "credit_note": 0.0, - "outstanding": 0.0 + "outstanding": invoiced - d.paid_amount })) + if d.paid_amount: + row['paid'] -= d.paid_amount + def allocate_closing_to_term(self, row, term, key): if row[key]: if row[key] > term.outstanding: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d3eb2a82885..e5073917b8d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -819,7 +819,7 @@ class AccountsController(TransactionBase): else: for d in self.get("payment_schedule"): if d.invoice_portion: - d.payment_amount = grand_total * flt(d.invoice_portion) / 100 + d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount')) def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] From fda451f3e38f97c302542a08f09db8c9a8ece9e7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Apr 2020 20:18:52 +0530 Subject: [PATCH 055/178] fix: Issues on qty trigger in Stock Entry Detail --- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index c54e3cdfb87..039aa777818 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -425,9 +425,10 @@ frappe.ui.form.on('Stock Entry', { item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item)); - item.valuation_rate = flt(flt(item.basic_rate) - + (flt(item.additional_cost) / flt(item.transfer_qty)), - precision("valuation_rate", item)); + if (flt(item.transfer_qty)) { + item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)), + precision("valuation_rate", item)); + } } refresh_field('items'); @@ -453,9 +454,8 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { - frm.events.set_serial_no(frm, cdt, cdn, () => { - frm.events.set_basic_rate(frm, cdt, cdn); - }); + frm.events.set_basic_rate(frm, cdt, cdn); + frm.events.set_serial_no(frm, cdt, cdn); }, conversion_factor: function(frm, cdt, cdn) { From 6256ca81b1848835a813d1293e74f1d241b278d8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 13 Apr 2020 16:40:13 +0530 Subject: [PATCH 056/178] fix: show positive taxes in credit notes --- erpnext/controllers/taxes_and_totals.py | 3 +-- erpnext/templates/includes/itemised_tax_breakup.html | 12 ++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b52a07dbdf0..82f820c4252 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -643,8 +643,7 @@ def get_itemised_tax_breakup_html(doc): itemised_tax=itemised_tax, itemised_taxable_amount=itemised_taxable_amount, tax_accounts=tax_accounts, - conversion_rate=doc.conversion_rate, - currency=doc.currency + doc=doc ) ) diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html index c27e4cede0d..c2f13539cdf 100644 --- a/erpnext/templates/includes/itemised_tax_breakup.html +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -16,7 +16,11 @@ {{ item }} - {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, currency) }} + {% if doc.get('is_return') %} + {{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }} + {% else %} + {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, doc.currency) }} + {% endif %} {% for tax_account in tax_accounts %} {% set tax_details = taxes.get(tax_account) %} @@ -25,7 +29,11 @@ {% if tax_details.tax_rate or not tax_details.tax_amount %} ({{ tax_details.tax_rate }}%) {% endif %} - {{ frappe.utils.fmt_money(tax_details.tax_amount / conversion_rate, None, currency) }} + {% if doc.get('is_return') %} + {{ frappe.utils.fmt_money((tax_details.tax_amount / doc.conversion_rate)|abs, None, doc.currency) }} + {% else %} + {{ frappe.utils.fmt_money(tax_details.tax_amount / doc.conversion_rate, None, doc.currency) }} + {% endif %} {% else %} From 424dbef1398cc28f3fd7534cb7f92865edec8411 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 24 Apr 2020 16:40:03 +0530 Subject: [PATCH 057/178] fix: rate gets overwritten when pricing rule is set --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 1 + erpnext/public/js/controllers/transaction.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index fc0d8b16f71..086f42b293f 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -241,6 +241,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: item_details.update({ 'apply_rule_on_other_items': json.dumps(pricing_rule.apply_rule_on_other_items), + 'price_or_product_discount': pricing_rule.price_or_product_discount, 'apply_rule_on': (frappe.scrub(pricing_rule.apply_rule_on_other) if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on'))) }) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 85d9799ce56..6def5cb51f3 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1374,7 +1374,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.frm.doc.items.forEach(d => { if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { for(var k in data) { - if (in_list(fields, k) && data[k]) { + if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) { frappe.model.set_value(d.doctype, d.name, k, data[k]); } } From be97087d32b0f4400b825fab4920da7a1697ebc2 Mon Sep 17 00:00:00 2001 From: Diksha Jadhav Date: Fri, 24 Apr 2020 21:09:59 +0530 Subject: [PATCH 058/178] feat(accounting): show actual qty for warehouse in sales invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7a7d25aadcc..d8ba1794039 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -32,6 +32,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.script_manager.trigger("is_pos"); me.frm.refresh_fields(); } + erpnext.queries.setup_warehouse_query(this.frm); }, refresh: function(doc, dt, dn) { From 7fedff260db7c25c1494da921e58115be4f6d9e8 Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 26 Apr 2020 09:41:02 +0530 Subject: [PATCH 059/178] fix: (ux) set jv voucher type depending on mode of payment (#21412) --- .../hr/doctype/employee_advance/employee_advance.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index f10e3b6ce26..f0663aefa83 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -136,9 +136,18 @@ def make_bank_entry(dt, dn): def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None): return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) + + mode_of_payment_type = '' + if mode_of_payment: + mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') + if mode_of_payment_type not in ["Cash", "Bank"]: + # if mode of payment is General then it unset the type + mode_of_payment_type = None + je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() - je.voucher_type = 'Bank Entry' + # if mode of payment is Bank then voucher type is Bank Entry + je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry' je.company = company je.remark = 'Return against Employee Advance: ' + employee_advance_name From 1d5ea4feee336ed24e9cf0eb6b0a48de99102652 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 12:37:52 +0530 Subject: [PATCH 060/178] feat: Income tax slab (#21406) * Feat: Multiple tax as per new taxation rule * patch:for multiple tax slab, fix: payroll and exemption validation * Test: Fixture * feat: income tax slab with other charges and tax exempted deduction components * fix: added missing init file * fix: Patch fixed * fix: Patch fixed * fix: test fixes * fix: validate duplicate exemption declaration * fix: payment entry test case Co-authored-by: Anurag Mishra --- .../payment_entry/test_payment_entry.py | 7 + .../doctype/sales_invoice/sales_invoice.js | 10 +- .../doctype/employee_other_income/__init__.py | 0 .../employee_other_income.js | 8 + .../employee_other_income.json | 138 +++ .../employee_other_income.py | 10 + .../test_employee_other_income.py | 10 + .../employee_tax_exemption_declaration.json | 746 +++----------- .../employee_tax_exemption_declaration.py | 20 +- ...test_employee_tax_exemption_declaration.py | 2 +- ...ployee_tax_exemption_proof_submission.json | 731 +++----------- ...employee_tax_exemption_proof_submission.py | 6 +- .../hr/doctype/income_tax_slab/__init__.py | 0 .../income_tax_slab/income_tax_slab.js | 6 + .../income_tax_slab/income_tax_slab.json | 160 +++ .../income_tax_slab/income_tax_slab.py | 10 + .../income_tax_slab/test_income_tax_slab.py | 10 + .../income_tax_slab_other_charges/__init__.py | 0 .../income_tax_slab_other_charges.json | 75 ++ .../income_tax_slab_other_charges.py | 10 + .../payroll_period/payroll_period.json | 467 ++------- .../doctype/payroll_period/payroll_period.py | 5 +- .../salary_component/salary_component.json | 525 +++++----- .../salary_component/test_records.json | 4 - .../salary_component/test_salary_component.py | 1 - .../doctype/salary_detail/salary_detail.json | 931 ++++-------------- erpnext/hr/doctype/salary_slip/salary_slip.py | 142 ++- .../doctype/salary_slip/test_salary_slip.py | 76 +- .../salary_structure/salary_structure.js | 1 + .../salary_structure/salary_structure.py | 23 +- .../salary_structure/test_salary_structure.py | 20 +- .../salary_structure_assignment.js | 10 + .../salary_structure_assignment.json | 13 +- erpnext/hr/utils.py | 13 + erpnext/patches.txt | 1 + .../v11_0/set_salary_component_properties.py | 3 +- erpnext/patches/v13_0/__init__.py | 0 ..._from_payroll_period_to_income_tax_slab.py | 99 ++ erpnext/regional/india/setup.py | 18 +- 39 files changed, 1598 insertions(+), 2713 deletions(-) create mode 100644 erpnext/hr/doctype/employee_other_income/__init__.py create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.js create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.json create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.py create mode 100644 erpnext/hr/doctype/employee_other_income/test_employee_other_income.py create mode 100644 erpnext/hr/doctype/income_tax_slab/__init__.py create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.js create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.json create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.py create mode 100644 erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py create mode 100644 erpnext/patches/v13_0/__init__.py create mode 100644 erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 80a11b1bdff..756cc8ec547 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -199,6 +199,13 @@ class TestPaymentEntry(unittest.TestCase): pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", currency="USD", conversion_rate=50) + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC") + pe.reference_no = "1" + pe.reference_date = "2016-01-01" + pe.source_exchange_rate = 50 + pe.insert() + pe.submit() + outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"]) self.assertEqual(flt(outstanding_amount), 0) self.assertEqual(status, 'Paid') diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d8ba1794039..18a791d38ce 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -587,7 +587,9 @@ frappe.ui.form.on('Sales Invoice', { frm.set_query("account_for_change_amount", function() { return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + company: frm.doc.company, + is_group: 0 } }; }); @@ -668,7 +670,8 @@ frappe.ui.form.on('Sales Invoice', { frm.fields_dict["loyalty_redemption_account"].get_query = function() { return { filters:{ - "company": frm.doc.company + "company": frm.doc.company, + "is_group": 0 } } }; @@ -677,7 +680,8 @@ frappe.ui.form.on('Sales Invoice', { frm.fields_dict["loyalty_redemption_cost_center"].get_query = function() { return { filters:{ - "company": frm.doc.company + "company": frm.doc.company, + "is_group": 0 } } }; diff --git a/erpnext/hr/doctype/employee_other_income/__init__.py b/erpnext/hr/doctype/employee_other_income/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.js b/erpnext/hr/doctype/employee_other_income/employee_other_income.js new file mode 100644 index 00000000000..c1a74e863ba --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Other Income', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json new file mode 100644 index 00000000000..2dd6c10988d --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "HR-INCOME-.######", + "creation": "2020-03-18 15:04:40.767434", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "payroll_period", + "column_break_3", + "company", + "source", + "amount", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "source", + "fieldtype": "Data", + "label": "Source" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Other Income", + "print_hide": 1, + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-19 18:06:45.361830", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Other Income", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.py b/erpnext/hr/doctype/employee_other_income/employee_other_income.py new file mode 100644 index 00000000000..ab63c0de623 --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EmployeeOtherIncome(Document): + pass diff --git a/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py new file mode 100644 index 00000000000..2eeca7a23de --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEmployeeOtherIncome(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index e102ff8d705..18fad85c4b3 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -1,620 +1,180 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "HR-TAX-DEC-.YYYY.-.#####", - "beta": 0, - "creation": "2018-04-13 16:53:36.175504", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "HR-TAX-DEC-.YYYY.-.#####", + "creation": "2018-04-13 16:53:36.175504", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "column_break_2", + "payroll_period", + "company", + "amended_from", + "section_break_8", + "declarations", + "section_break_10", + "total_declared_amount", + "column_break_12", + "total_exemption_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fetch_if_empty": 0, - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fetch_if_empty": 0, - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "payroll_period", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payroll Period", - "length": 0, - "no_copy": 0, - "options": "Payroll Period", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.company", - "fetch_if_empty": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Employee Tax Exemption Declaration", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Tax Exemption Declaration", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "declarations", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Declarations", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Declaration Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "declarations", + "fieldtype": "Table", + "label": "Declarations", + "options": "Employee Tax Exemption Declaration Category" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_declared_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Declared Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "total_declared_amount", + "fieldtype": "Currency", + "label": "Total Declared Amount", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_exemption_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "other_incomes_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Other Incomes", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "income_from_other_sources", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Income From Other Sources", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "total_exemption_amount", + "fieldtype": "Currency", + "label": "Total Exemption Amount", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-05-11 16:13:50.472670", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Declaration", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-18 14:56:25.625717", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Tax Exemption Declaration", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index f2bba7afed7..fb71a2877a1 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -8,31 +8,17 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import flt from frappe.model.mapper import get_mapped_doc -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_annual_eligible_hra_exemption - -class DuplicateDeclarationError(frappe.ValidationError): pass +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ + calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionDeclaration(Document): def validate(self): validate_tax_declaration(self.declarations) - self.validate_duplicate() + validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) self.set_total_declared_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() - def validate_duplicate(self): - duplicate = frappe.db.get_value("Employee Tax Exemption Declaration", - filters = { - "employee": self.employee, - "payroll_period": self.payroll_period, - "name": ["!=", self.name], - "docstatus": ["!=", 2] - } - ) - if duplicate: - frappe.throw(_("Duplicate Tax Declaration of {0} for period {1}") - .format(self.employee, self.payroll_period), DuplicateDeclarationError) - def set_total_declared_amount(self): self.total_declared_amount = 0.0 for d in self.declarations: diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 9c87bbd1f30..9549fd1b757 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext import unittest from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration import DuplicateDeclarationError +from erpnext.hr.utils import DuplicateDeclarationError class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): def setUp(self): diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index 9792bd1db61..e13b1ac8888 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -1,635 +1,140 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "HR-TAX-PRF-.YYYY.-.#####", - "beta": 0, - "creation": "2018-04-13 17:24:11.456132", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "HR-TAX-PRF-.YYYY.-.#####", + "creation": "2018-04-13 17:24:11.456132", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "column_break_2", + "submission_date", + "payroll_period", + "company", + "section_break_5", + "tax_exemption_proofs", + "section_break_10", + "total_actual_amount", + "column_break_12", + "exemption_amount", + "attachment_section", + "attachments", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fetch_if_empty": 0, - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fetch_if_empty": 0, - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "submission_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Submission Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "submission_date", + "fieldtype": "Date", + "label": "Submission Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "payroll_period", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payroll Period", - "length": 0, - "no_copy": 0, - "options": "Payroll Period", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.company", - "fetch_if_empty": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "tax_exemption_proofs", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tax Exemption Proofs", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Proof Submission Detail", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "tax_exemption_proofs", + "fieldtype": "Table", + "label": "Tax Exemption Proofs", + "options": "Employee Tax Exemption Proof Submission Detail" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_actual_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Actual Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "total_actual_amount", + "fieldtype": "Currency", + "label": "Total Actual Amount", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "exemption_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "exemption_amount", + "fieldtype": "Currency", + "label": "Total Exemption Amount", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "other_incomes_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Other Incomes", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "attachment_section", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "income_from_other_sources", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Income From Other Sources", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "attachments", + "fieldtype": "Attach", + "label": "Attachments" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "attachment_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "attachments", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Attachments", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Employee Tax Exemption Proof Submission", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Tax Exemption Proof Submission", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-05-13 12:17:18.045171", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Proof Submission", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-18 14:55:51.420016", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Tax Exemption Proof Submission", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 97ceb63476b..5bc33a65f2c 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -7,7 +7,8 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import flt -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_hra_exemption_for_period +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ + calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionProofSubmission(Document): def validate(self): @@ -15,6 +16,7 @@ class EmployeeTaxExemptionProofSubmission(Document): self.set_total_actual_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() + validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) def set_total_actual_amount(self): self.total_actual_amount = flt(self.get("house_rent_payment_amount")) @@ -32,4 +34,4 @@ class EmployeeTaxExemptionProofSubmission(Document): self.exemption_amount += hra_exemption["total_eligible_hra_exemption"] self.monthly_hra_exemption = hra_exemption["monthly_exemption"] self.monthly_house_rent = hra_exemption["monthly_house_rent"] - self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] \ No newline at end of file + self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] diff --git a/erpnext/hr/doctype/income_tax_slab/__init__.py b/erpnext/hr/doctype/income_tax_slab/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js new file mode 100644 index 00000000000..73a54eb8dd9 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js @@ -0,0 +1,6 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Income Tax Slab', { + +}); diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json new file mode 100644 index 00000000000..6d89b197d27 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-03-17 16:50:35.564915", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "effective_from", + "company", + "column_break_3", + "allow_tax_exemption", + "standard_tax_exemption_amount", + "disabled", + "amended_from", + "taxable_salary_slabs_section", + "slabs", + "taxes_and_charges_on_income_tax_section", + "other_taxes_and_charges" + ], + "fields": [ + { + "fieldname": "effective_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Effective from", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If enabled, Tax Exemption Declaration will be considered for income tax calculation.", + "fieldname": "allow_tax_exemption", + "fieldtype": "Check", + "label": "Allow Tax Exemption" + }, + { + "fieldname": "taxable_salary_slabs_section", + "fieldtype": "Section Break", + "label": "Taxable Salary Slabs" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Income Tax Slab", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "slabs", + "fieldtype": "Table", + "label": "Taxable Salary Slabs", + "options": "Taxable Salary Slab", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "allow_tax_exemption", + "fieldname": "standard_tax_exemption_amount", + "fieldtype": "Currency", + "label": "Standard Tax Exemption Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "collapsible": 1, + "fieldname": "taxes_and_charges_on_income_tax_section", + "fieldtype": "Section Break", + "label": "Taxes and Charges on Income Tax" + }, + { + "fieldname": "other_taxes_and_charges", + "fieldtype": "Table", + "label": "Other Taxes and Charges", + "options": "Income Tax Slab Other Charges" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-24 12:28:36.805904", + "modified_by": "Administrator", + "module": "HR", + "name": "Income Tax Slab", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py new file mode 100644 index 00000000000..253f023f68b --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class IncomeTaxSlab(Document): + pass diff --git a/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py new file mode 100644 index 00000000000..deaaf650a96 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestIncomeTaxSlab(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py b/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json new file mode 100644 index 00000000000..b23fb3dc317 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "creation": "2020-04-24 11:46:59.041180", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "description", + "column_break_2", + "percent", + "conditions_section", + "min_taxable_income", + "column_break_7", + "max_taxable_income" + ], + "fields": [ + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "min_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Min Taxable Income", + "options": "Company:company:default_currency" + }, + { + "columns": 4, + "fieldname": "description", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "percent", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Percent", + "reqd": 1 + }, + { + "fieldname": "conditions_section", + "fieldtype": "Section Break", + "label": "Conditions" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "max_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Max Taxable Income", + "options": "Company:company:default_currency" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-24 13:27:43.598967", + "modified_by": "Administrator", + "module": "HR", + "name": "Income Tax Slab Other Charges", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py new file mode 100644 index 00000000000..b4098ecbf3e --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class IncomeTaxSlabOtherCharges(Document): + pass diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.json b/erpnext/hr/doctype/payroll_period/payroll_period.json index c9bac095f9f..c0fa506e7f0 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.json +++ b/erpnext/hr/doctype/payroll_period/payroll_period.json @@ -1,401 +1,102 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 15:18:53.698553", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "Prompt", + "creation": "2018-04-13 15:18:53.698553", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "column_break_2", + "start_date", + "end_date", + "section_break_5", + "periods" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payroll Periods", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Payroll Periods" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "periods", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payroll Periods", - "length": 0, - "no_copy": 0, - "options": "Payroll Period Date", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Taxable Salary Slabs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "taxable_salary_slabs", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Taxable Salary Slabs", - "length": 0, - "no_copy": 0, - "options": "Taxable Salary Slab", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "standard_tax_exemption_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Standard Tax Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "periods", + "fieldtype": "Table", + "label": "Payroll Periods", + "options": "Payroll Period Date" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-26 01:45:03.160929", - "modified_by": "Administrator", - "module": "HR", - "name": "Payroll Period", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-03-18 18:13:23.859980", + "modified_by": "Administrator", + "module": "HR", + "name": "Payroll Period", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.py b/erpnext/hr/doctype/payroll_period/payroll_period.py index c1769591eaf..6956c382854 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.py +++ b/erpnext/hr/doctype/payroll_period/payroll_period.py @@ -45,8 +45,9 @@ class PayrollPeriod(Document): + _(") for {0}").format(self.company) frappe.throw(msg) -def get_payroll_period_days(start_date, end_date, employee): - company = frappe.db.get_value("Employee", employee, "company") +def get_payroll_period_days(start_date, end_date, employee, company=None): + if not company: + company = frappe.db.get_value("Employee", employee, "company") payroll_period = frappe.db.sql(""" select name, start_date, end_date from `tabPayroll Period` diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json index 986030d8c58..5487e1dee85 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/hr/doctype/salary_component/salary_component.json @@ -1,264 +1,263 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:salary_component", - "creation": "2016-06-30 15:42:43.631931", - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "salary_component", - "salary_component_abbr", - "type", - "description", - "column_break_4", - "is_payable", - "depends_on_payment_days", - "is_tax_applicable", - "deduct_full_tax_on_selected_payroll_date", - "round_to_the_nearest_integer", - "statistical_component", - "do_not_include_in_total", - "disabled", - "flexible_benefits", - "is_flexible_benefit", - "max_benefit_amount", - "column_break_9", - "pay_against_benefit_claim", - "only_tax_impact", - "create_separate_payment_entry_against_benefit_claim", - "section_break_11", - "variable_based_on_taxable_salary", - "section_break_5", - "accounts", - "condition_and_formula", - "condition", - "amount", - "amount_based_on_formula", - "formula", - "column_break_28", - "help" - ], - "fields": [ - { - "fieldname": "salary_component", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "salary_component_abbr", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Abbr", - "print_width": "120px", - "reqd": 1, - "width": "120px" - }, - { - "fieldname": "type", - "fieldtype": "Select", - "in_standard_filter": 1, - "label": "Type", - "options": "Earning\nDeduction", - "reqd": 1 - }, - { - "default": "1", - "depends_on": "eval:doc.type == \"Earning\"", - "fieldname": "is_tax_applicable", - "fieldtype": "Check", - "label": "Is Tax Applicable" - }, - { - "default": "1", - "fieldname": "is_payable", - "fieldtype": "Check", - "label": "Is Payable" - }, - { - "default": "1", - "fieldname": "depends_on_payment_days", - "fieldtype": "Check", - "label": "Depends on Payment Days", - "print_hide": 1 - }, - { - "default": "0", - "fieldname": "do_not_include_in_total", - "fieldtype": "Check", - "label": "Do Not Include in Total" - }, - { - "default": "0", - "depends_on": "is_tax_applicable", - "fieldname": "deduct_full_tax_on_selected_payroll_date", - "fieldtype": "Check", - "label": "Deduct Full Tax on Selected Payroll Date" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "disabled", - "fieldtype": "Check", - "label": "Disabled" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Description" - }, - { - "default": "0", - "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", - "fieldname": "statistical_component", - "fieldtype": "Check", - "label": "Statistical Component" - }, - { - "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1", - "fieldname": "flexible_benefits", - "fieldtype": "Section Break", - "label": "Flexible Benefits" - }, - { - "default": "0", - "fieldname": "is_flexible_benefit", - "fieldtype": "Check", - "label": "Is Flexible Benefit" - }, - { - "depends_on": "is_flexible_benefit", - "fieldname": "max_benefit_amount", - "fieldtype": "Currency", - "label": "Max Benefit Amount (Yearly)" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "is_flexible_benefit", - "fieldname": "pay_against_benefit_claim", - "fieldtype": "Check", - "label": "Pay Against Benefit Claim" - }, - { - "default": "0", - "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1", - "fieldname": "only_tax_impact", - "fieldtype": "Check", - "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)" - }, - { - "default": "0", - "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1", - "fieldname": "create_separate_payment_entry_against_benefit_claim", - "fieldtype": "Check", - "label": "Create Separate Payment Entry Against Benefit Claim" - }, - { - "depends_on": "eval:doc.type=='Deduction'", - "fieldname": "section_break_11", - "fieldtype": "Section Break" - }, - { - "default": "0", - "fieldname": "variable_based_on_taxable_salary", - "fieldtype": "Check", - "label": "Variable Based On Taxable Salary" - }, - { - "depends_on": "eval:doc.statistical_component != 1", - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "label": "Accounts" - }, - { - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Accounts", - "options": "Salary Component Account" - }, - { - "collapsible": 1, - "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1", - "fieldname": "condition_and_formula", - "fieldtype": "Section Break", - "label": "Condition and Formula" - }, - { - "fieldname": "condition", - "fieldtype": "Code", - "label": "Condition" - }, - { - "default": "0", - "fieldname": "amount_based_on_formula", - "fieldtype": "Check", - "label": "Amount based on formula" - }, - { - "depends_on": "amount_based_on_formula", - "fieldname": "formula", - "fieldtype": "Code", - "label": "Formula" - }, - { - "depends_on": "eval:doc.amount_based_on_formula!==1", - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount" - }, - { - "fieldname": "column_break_28", - "fieldtype": "Column Break" - }, - { - "fieldname": "help", - "fieldtype": "HTML", - "label": "Help", - "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" - }, - { - "default": "0", - "fieldname": "round_to_the_nearest_integer", - "fieldtype": "Check", - "label": "Round to the Nearest Integer" - } - ], - "icon": "fa fa-flag", - "modified": "2019-06-05 11:34:14.231228", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Component", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "share": 1, - "write": 1 - }, - { - "read": 1, - "role": "Employee" - } - ], - "sort_field": "modified", - "sort_order": "DESC" - } \ No newline at end of file + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:salary_component", + "creation": "2016-06-30 15:42:43.631931", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salary_component", + "salary_component_abbr", + "type", + "description", + "column_break_4", + "depends_on_payment_days", + "is_tax_applicable", + "deduct_full_tax_on_selected_payroll_date", + "variable_based_on_taxable_salary", + "exempted_from_income_tax", + "round_to_the_nearest_integer", + "statistical_component", + "do_not_include_in_total", + "disabled", + "flexible_benefits", + "is_flexible_benefit", + "max_benefit_amount", + "column_break_9", + "pay_against_benefit_claim", + "only_tax_impact", + "create_separate_payment_entry_against_benefit_claim", + "section_break_5", + "accounts", + "condition_and_formula", + "condition", + "amount", + "amount_based_on_formula", + "formula", + "column_break_28", + "help" + ], + "fields": [ + { + "fieldname": "salary_component", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "salary_component_abbr", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Abbr", + "print_width": "120px", + "reqd": 1, + "width": "120px" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Type", + "options": "Earning\nDeduction", + "reqd": 1 + }, + { + "default": "1", + "depends_on": "eval:doc.type == \"Earning\"", + "fieldname": "is_tax_applicable", + "fieldtype": "Check", + "label": "Is Tax Applicable" + }, + { + "default": "1", + "fieldname": "depends_on_payment_days", + "fieldtype": "Check", + "label": "Depends on Payment Days", + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "do_not_include_in_total", + "fieldtype": "Check", + "label": "Do Not Include in Total" + }, + { + "default": "0", + "depends_on": "eval:doc.is_tax_applicable && doc.type=='Earning'", + "fieldname": "deduct_full_tax_on_selected_payroll_date", + "fieldtype": "Check", + "label": "Deduct Full Tax on Selected Payroll Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, + { + "default": "0", + "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", + "fieldname": "statistical_component", + "fieldtype": "Check", + "label": "Statistical Component" + }, + { + "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1", + "fieldname": "flexible_benefits", + "fieldtype": "Section Break", + "label": "Flexible Benefits" + }, + { + "default": "0", + "fieldname": "is_flexible_benefit", + "fieldtype": "Check", + "label": "Is Flexible Benefit" + }, + { + "depends_on": "is_flexible_benefit", + "fieldname": "max_benefit_amount", + "fieldtype": "Currency", + "label": "Max Benefit Amount (Yearly)" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "is_flexible_benefit", + "fieldname": "pay_against_benefit_claim", + "fieldtype": "Check", + "label": "Pay Against Benefit Claim" + }, + { + "default": "0", + "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1", + "fieldname": "only_tax_impact", + "fieldtype": "Check", + "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)" + }, + { + "default": "0", + "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1", + "fieldname": "create_separate_payment_entry_against_benefit_claim", + "fieldtype": "Check", + "label": "Create Separate Payment Entry Against Benefit Claim" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\"", + "fieldname": "variable_based_on_taxable_salary", + "fieldtype": "Check", + "label": "Variable Based On Taxable Salary" + }, + { + "depends_on": "eval:doc.statistical_component != 1", + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Accounts" + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Salary Component Account" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1", + "fieldname": "condition_and_formula", + "fieldtype": "Section Break", + "label": "Condition and Formula" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" + }, + { + "default": "0", + "fieldname": "amount_based_on_formula", + "fieldtype": "Check", + "label": "Amount based on formula" + }, + { + "depends_on": "amount_based_on_formula", + "fieldname": "formula", + "fieldtype": "Code", + "label": "Formula" + }, + { + "depends_on": "eval:doc.amount_based_on_formula!==1", + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "help", + "fieldtype": "HTML", + "label": "Help", + "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "default": "0", + "fieldname": "round_to_the_nearest_integer", + "fieldtype": "Check", + "label": "Round to the Nearest Integer" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\" && !doc.variable_based_on_taxable_salary", + "description": "If checked, the full amount will be deducted from taxable income before calculating income tax. Otherwise, it can be exempted via Employee Tax Exemption Declaration.", + "fieldname": "exempted_from_income_tax", + "fieldtype": "Check", + "label": "Exempted from Income Tax" + } + ], + "icon": "fa fa-flag", + "links": [], + "modified": "2020-04-24 14:50:28.994054", + "modified_by": "Administrator", + "module": "HR", + "name": "Salary Component", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Employee" + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_component/test_records.json b/erpnext/hr/doctype/salary_component/test_records.json index 7b22b481f36..104b44ffa1b 100644 --- a/erpnext/hr/doctype/salary_component/test_records.json +++ b/erpnext/hr/doctype/salary_component/test_records.json @@ -3,14 +3,12 @@ "doctype": "Salary Component", "salary_component": "_Test Basic Salary", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { "doctype": "Salary Component", "salary_component": "_Test Allowance", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { @@ -27,14 +25,12 @@ "doctype": "Salary Component", "salary_component": "Basic", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { "doctype": "Salary Component", "salary_component": "Leave Encashment", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 } ] \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_component/test_salary_component.py b/erpnext/hr/doctype/salary_component/test_salary_component.py index 965cc9e9ffd..4f7db0c71ca 100644 --- a/erpnext/hr/doctype/salary_component/test_salary_component.py +++ b/erpnext/hr/doctype/salary_component/test_salary_component.py @@ -18,6 +18,5 @@ def create_salary_component(component_name, **args): "doctype": "Salary Component", "salary_component": component_name, "type": args.get("type") or "Earning", - "is_payable": args.get("is_payable") or 1, "is_tax_applicable": args.get("is_tax_applicable") or 1 }).insert() diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index edf2786821e..545f56a0b60 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -1,765 +1,216 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-06-30 15:32:36.385111", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-06-30 15:32:36.385111", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salary_component", + "abbr", + "statistical_component", + "column_break_3", + "deduct_full_tax_on_selected_payroll_date", + "depends_on_payment_days", + "is_tax_applicable", + "exempted_from_income_tax", + "is_flexible_benefit", + "variable_based_on_taxable_salary", + "section_break_2", + "condition", + "amount_based_on_formula", + "formula", + "amount", + "do_not_include_in_total", + "default_amount", + "additional_amount", + "tax_on_flexible_benefit", + "tax_on_additional_salary", + "section_break_11", + "condition_and_formula_help" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "salary_component", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Component", - "length": 0, - "no_copy": 0, - "options": "Salary Component", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "salary_component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Component", + "options": "Salary Component", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "default": "", - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fetch_from": "salary_component.salary_component_abbr", - "fetch_if_empty": 0, - "fieldname": "abbr", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Abbr", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 1, + "depends_on": "eval:doc.parenttype=='Salary Structure'", + "fetch_from": "salary_component.salary_component_abbr", + "fieldname": "abbr", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Abbr", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", - "fetch_from": "salary_component.statistical_component", - "fetch_if_empty": 0, - "fieldname": "statistical_component", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Statistical Component", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", + "fetch_from": "salary_component.statistical_component", + "fieldname": "statistical_component", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Statistical Component" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "salary_component.is_tax_applicable", - "fetch_if_empty": 0, - "fieldname": "is_tax_applicable", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Tax Applicable", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:doc.parentfield=='earnings'", + "fetch_from": "salary_component.is_tax_applicable", + "fieldname": "is_tax_applicable", + "fieldtype": "Check", + "label": "Is Tax Applicable", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "salary_component.is_flexible_benefit", - "fetch_if_empty": 0, - "fieldname": "is_flexible_benefit", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Flexible Benefit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:doc.parentfield=='earnings'", + "fetch_from": "salary_component.is_flexible_benefit", + "fieldname": "is_flexible_benefit", + "fieldtype": "Check", + "label": "Is Flexible Benefit", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_from": "salary_component.variable_based_on_taxable_salary", - "fetch_if_empty": 0, - "fieldname": "variable_based_on_taxable_salary", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Variable Based On Taxable Salary", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:doc.parentfield=='deductions'", + "fetch_from": "salary_component.variable_based_on_taxable_salary", + "fieldname": "variable_based_on_taxable_salary", + "fieldtype": "Check", + "label": "Variable Based On Taxable Salary", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_from": "salary_component.depends_on_payment_days", - "fetch_if_empty": 0, - "fieldname": "depends_on_payment_days", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Depends on Payment Days", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fetch_from": "salary_component.depends_on_payment_days", + "fieldname": "depends_on_payment_days", + "fieldtype": "Check", + "label": "Depends on Payment Days", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "deduct_full_tax_on_selected_payroll_date", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Deduct Full Tax on Selected Payroll Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "deduct_full_tax_on_selected_payroll_date", + "fieldtype": "Check", + "label": "Deduct Full Tax on Selected Payroll Date", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.is_flexible_benefit != 1", - "fetch_if_empty": 0, - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.is_flexible_benefit != 1", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fetch_if_empty": 0, - "fieldname": "condition", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Condition", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:doc.parenttype=='Salary Structure'", + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fetch_from": "", - "fetch_if_empty": 0, - "fieldname": "amount_based_on_formula", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amount based on formula", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:doc.parenttype=='Salary Structure'", + "fieldname": "amount_based_on_formula", + "fieldtype": "Check", + "label": "Amount based on formula" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "depends_on": "eval:doc.amount_based_on_formula!==0 && doc.parenttype==='Salary Structure'", - "description": "", - "fetch_if_empty": 0, - "fieldname": "formula", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Formula", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:doc.amount_based_on_formula!==0 && doc.parenttype==='Salary Structure'", + "fieldname": "formula", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Formula" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.amount_based_on_formula!==1 || doc.parenttype==='Salary Slip'", - "fetch_if_empty": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.amount_based_on_formula!==1 || doc.parenttype==='Salary Slip'", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "do_not_include_in_total", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Do not include in total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "do_not_include_in_total", + "fieldtype": "Check", + "label": "Do not include in total" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fetch_if_empty": 0, - "fieldname": "default_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.parenttype=='Salary Structure'", + "fieldname": "default_amount", + "fieldtype": "Currency", + "label": "Default Amount", + "options": "Company:company:default_currency", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_from": "", - "fetch_if_empty": 0, - "fieldname": "additional_amount", - "fieldtype": "Currency", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Additional Amount", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "additional_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Additional Amount", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1", - "fetch_if_empty": 0, - "fieldname": "tax_on_flexible_benefit", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tax on flexible benefit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1", + "fieldname": "tax_on_flexible_benefit", + "fieldtype": "Currency", + "label": "Tax on flexible benefit", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1", - "fetch_if_empty": 0, - "fieldname": "tax_on_additional_salary", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tax on additional salary", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='deductions' && doc.variable_based_on_taxable_salary == 1", + "fieldname": "tax_on_additional_salary", + "fieldtype": "Currency", + "label": "Tax on additional salary", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fetch_if_empty": 0, - "fieldname": "section_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.parenttype=='Salary Structure'", + "fieldname": "section_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fetch_if_empty": 0, - "fieldname": "condition_and_formula_help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Condition and Formula Help", - "length": 0, - "no_copy": 0, - "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "depends_on": "eval:doc.parenttype=='Salary Structure'", + "fieldname": "condition_and_formula_help", + "fieldtype": "HTML", + "label": "Condition and Formula Help", + "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "default": "0", + "depends_on": "eval:doc.parentfield=='deductions'", + "fetch_from": "salary_component.exempted_from_income_tax", + "fieldname": "exempted_from_income_tax", + "fieldtype": "Check", + "label": "Exempted from Income Tax", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-05-11 17:33:08.508653", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Detail", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-04-24 20:00:16.475295", + "modified_by": "Administrator", + "module": "HR", + "name": "Salary Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 6157daff1e0..dda15014166 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -450,7 +450,8 @@ class SalarySlip(TransactionBase): 'is_flexible_benefit': struct_row.is_flexible_benefit, 'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary, 'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date, - 'additional_amount': amount if struct_row.get("is_additional_component") else 0 + 'additional_amount': amount if struct_row.get("is_additional_component") else 0, + 'exempted_from_income_tax': struct_row.exempted_from_income_tax }) else: if struct_row.get("is_additional_component"): @@ -481,10 +482,12 @@ class SalarySlip(TransactionBase): return self.calculate_variable_tax(payroll_period, tax_component) def calculate_variable_tax(self, payroll_period, tax_component): + # get Tax slab from salary structure assignment for the employee and payroll period + tax_slab = self.get_income_tax_slabs(payroll_period) + # get remaining numbers of sub-period (period for which one salary is processed) remaining_sub_periods = get_period_factor(self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] - # get taxable_earnings, paid_taxes for previous period previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date) previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) @@ -506,23 +509,27 @@ class SalarySlip(TransactionBase): unclaimed_taxable_benefits += current_taxable_earnings_for_payment_days.flexi_benefits # Total exemption amount based on tax exemption declaration - total_exemption_amount, other_incomes = self.get_total_exemption_amount_and_other_incomes(payroll_period) + total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab) + + #Employee Other Incomes + other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0 # Total taxable earnings including additional and other incomes total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \ + current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount - + # Total taxable earnings without additional earnings with full tax total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax # Structured tax amount - total_structured_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings_without_full_tax_addl_components) + total_structured_tax_amount = self.calculate_tax_by_tax_slab( + total_taxable_earnings_without_full_tax_addl_components, tax_slab) current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods - + # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 if current_additional_earnings_with_full_tax: - total_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings) + total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab) full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings @@ -531,12 +538,30 @@ class SalarySlip(TransactionBase): return current_tax_amount + def get_income_tax_slabs(self, payroll_period): + income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", + {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name']) + + if not income_tax_slab: + frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name)) + + income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab) + if income_tax_slab_doc.disabled: + frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab)) + + if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date): + frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}") + .format(payroll_period.start_date)) + + return income_tax_slab_doc + + def get_taxable_earnings_for_prev_period(self, start_date, end_date): taxable_earnings = frappe.db.sql(""" select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name - where + where sd.parentfield='earnings' and sd.is_tax_applicable=1 and is_flexible_benefit=0 @@ -549,7 +574,28 @@ class SalarySlip(TransactionBase): "from_date": start_date, "to_date": end_date }) - return flt(taxable_earnings[0][0]) if taxable_earnings else 0 + taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 + + exempted_amount = frappe.db.sql(""" + select sum(sd.amount) + from + `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name + where + sd.parentfield='deductions' + and sd.exempted_from_income_tax=1 + and is_flexible_benefit=0 + and ss.docstatus=1 + and ss.employee=%(employee)s + and ss.start_date between %(from_date)s and %(to_date)s + and ss.end_date between %(from_date)s and %(to_date)s + """, { + "employee": self.employee, + "from_date": start_date, + "to_date": end_date + }) + exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 + + return taxable_earnings - exempted_amount def get_tax_paid_in_period(self, start_date, end_date, tax_component): # find total_tax_paid, tax paid for benefit, additional_salary @@ -609,6 +655,13 @@ class SalarySlip(TransactionBase): else: taxable_earnings += amount + for ded in self.deductions: + if ded.exempted_from_income_tax: + amount = ded.amount + if based_on_payment_days: + amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] + taxable_earnings -= flt(amount) + return frappe._dict({ "taxable_earnings": taxable_earnings, "additional_income": additional_income, @@ -671,40 +724,63 @@ class SalarySlip(TransactionBase): return total_benefits_paid - total_benefits_claimed - def get_total_exemption_amount_and_other_incomes(self, payroll_period): - total_exemption_amount, other_incomes = 0, 0 - if self.deduct_tax_for_unsubmitted_tax_exemption_proof: - exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", - {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["exemption_amount", "income_from_other_sources"]) - if exemption_proof: - total_exemption_amount, other_incomes = exemption_proof - else: - declaration = frappe.db.get_value("Employee Tax Exemption Declaration", - {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["total_exemption_amount", "income_from_other_sources"]) - if declaration: - total_exemption_amount, other_incomes = declaration + def get_total_exemption_amount(self, payroll_period, tax_slab): + total_exemption_amount = 0 + if tax_slab.allow_tax_exemption: + if self.deduct_tax_for_unsubmitted_tax_exemption_proof: + exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", + {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, + ["exemption_amount"]) + if exemption_proof: + total_exemption_amount = exemption_proof + else: + declaration = frappe.db.get_value("Employee Tax Exemption Declaration", + {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, + ["total_exemption_amount"]) + if declaration: + total_exemption_amount = declaration - return total_exemption_amount, other_incomes + total_exemption_amount += flt(tax_slab.standard_tax_exemption_amount) - def calculate_tax_by_tax_slab(self, payroll_period, annual_taxable_earning): - payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period) - annual_taxable_earning -= flt(payroll_period_obj.standard_tax_exemption_amount) + return total_exemption_amount + + def get_income_form_other_sources(self, payroll_period): + return frappe.get_all("Employee Other Income", + filters={ + "employee": self.employee, + "payroll_period": payroll_period.name, + "company": self.company, + "docstatus": 1 + }, + fields="SUM(amount) as total_amount" + )[0].total_amount + + def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): data = self.get_data_for_eval() data.update({"annual_taxable_earning": annual_taxable_earning}) - taxable_amount = 0 - for slab in payroll_period_obj.taxable_salary_slabs: + tax_amount = 0 + for slab in tax_slab.slabs: if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): continue if not slab.to_amount and annual_taxable_earning > slab.from_amount: - taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 continue if annual_taxable_earning > slab.from_amount and annual_taxable_earning < slab.to_amount: - taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 elif annual_taxable_earning > slab.from_amount and annual_taxable_earning > slab.to_amount: - taxable_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 - return taxable_amount + tax_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 + + # other taxes and charges on income tax + for d in tax_slab.other_taxes_and_charges: + if flt(d.min_taxable_income) and flt(d.min_taxable_income) > tax_amount: + continue + + if flt(d.max_taxable_income) and flt(d.max_taxable_income) < tax_amount: + continue + + tax_amount += tax_amount * flt(d.percent) / 100 + + return tax_amount def eval_tax_slab_condition(self, condition, data): try: diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 16a75f473f4..7572e00359a 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -47,10 +47,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, no_of_days[0]) self.assertEqual(ss.earnings[0].amount, 50000) self.assertEqual(ss.earnings[1].amount, 3000) - self.assertEqual(ss.deductions[0].amount, 5000) - self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 67418.0) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() @@ -67,10 +64,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[0].amount, 50000) self.assertEqual(ss.earnings[0].default_amount, 50000) self.assertEqual(ss.earnings[1].amount, 3000) - self.assertEqual(ss.deductions[0].amount, 5000) - self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 67418.0) def test_payment_days(self): no_of_days = self.get_no_of_days() @@ -80,8 +74,8 @@ class TestSalarySlip(unittest.TestCase): # set joinng date in the same month make_employee("test_employee@salary.com") if getdate(nowdate()).day >= 15: - date_of_joining = getdate(add_days(nowdate(),-10)) relieving_date = getdate(add_days(nowdate(),-10)) + date_of_joining = getdate(add_days(nowdate(),-10)) elif getdate(nowdate()).day < 15 and getdate(nowdate()).day >= 5: date_of_joining = getdate(add_days(nowdate(),-3)) relieving_date = getdate(add_days(nowdate(),-3)) @@ -131,9 +125,7 @@ class TestSalarySlip(unittest.TestCase): def test_email_salary_slip(self): frappe.db.sql("delete from `tabEmail Queue`") - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.email_salary_slip_to_employee = 1 - hr_settings.save() + frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 1) make_employee("test_employee@salary.com") ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") @@ -183,8 +175,11 @@ class TestSalarySlip(unittest.TestCase): # as per assigned salary structure 40500 in monthly salary so 236000*5/100/12 frappe.db.sql("""delete from `tabPayroll Period`""") frappe.db.sql("""delete from `tabSalary Component`""") + payroll_period = create_payroll_period() - create_tax_slab(payroll_period) + + create_tax_slab(payroll_period, allow_tax_exemption=True) + employee = make_employee("test_tax@salary.slip") delete_docs = [ "Salary Slip", @@ -210,8 +205,7 @@ class TestSalarySlip(unittest.TestCase): payroll_period, deduct_random=False) tax_paid = get_tax_paid_in_period(employee) - # total taxable income 586000, 250000 @ 5%, 86000 @ 20% ie. 12500 + 17200 - annual_tax = 113568 + annual_tax = 113589.0 try: self.assertEqual(tax_paid, annual_tax) except AssertionError: @@ -235,8 +229,7 @@ class TestSalarySlip(unittest.TestCase): raise # Submit proof for total 120000 - data["proof-1"] = create_proof_submission(employee, payroll_period, 50000) - data["proof-2"] = create_proof_submission(employee, payroll_period, 70000) + data["proof"] = create_proof_submission(employee, payroll_period, 120000) # Submit benefit claim for total 50000 data["benefit-1"] = create_benefit_claim(employee, payroll_period, 15000, "Medical Allowance") @@ -250,7 +243,7 @@ class TestSalarySlip(unittest.TestCase): # total taxable income 416000, 166000 @ 5% ie. 8300 try: - self.assertEqual(tax_paid, 88608) + self.assertEqual(tax_paid, 82389.0) except AssertionError: print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n") raise @@ -265,7 +258,7 @@ class TestSalarySlip(unittest.TestCase): # total taxable income 566000, 250000 @ 5%, 66000 @ 20%, 12500 + 13200 tax_paid = get_tax_paid_in_period(employee) try: - self.assertEqual(tax_paid, 121211) + self.assertEqual(tax_paid, annual_tax) except AssertionError: print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n") raise @@ -307,6 +300,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", {"user_id": user}) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) @@ -431,17 +425,15 @@ def make_deduction_salary_component(setup=False, test_tax=False): { "salary_component": 'Professional Tax', "abbr":'PT', - "condition": 'base > 10000', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1 + "amount": 200, + "exempted_from_income_tax": 1 + }, { "salary_component": 'TDS', "abbr":'T', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1, "depends_on_payment_days": 0, "variable_based_on_taxable_salary": 1, "round_to_the_nearest_integer": 1 @@ -452,9 +444,7 @@ def make_deduction_salary_component(setup=False, test_tax=False): "salary_component": 'TDS', "abbr":'T', "condition": 'employment_type=="Intern"', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1, "round_to_the_nearest_integer": 1 }) if setup or test_tax: @@ -510,29 +500,47 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period): - data = [ +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False): + if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name): + return + + slabs = [ { "from_amount": 250000, "to_amount": 500000, - "percent_deduction": 5.2, + "percent_deduction": 5, "condition": "annual_taxable_earning > 500000" }, { "from_amount": 500001, "to_amount": 1000000, - "percent_deduction": 20.8 + "percent_deduction": 20 }, { "from_amount": 1000001, - "percent_deduction": 31.2 + "percent_deduction": 30 } ] - payroll_period.taxable_salary_slabs = [] - for item in data: - payroll_period.append("taxable_salary_slabs", item) - payroll_period.standard_tax_exemption_amount = 52500 - payroll_period.save() + + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + + if allow_tax_exemption: + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = 50000 + + for item in slabs: + income_tax_slab.append("slabs", item) + + income_tax_slab.append("other_taxes_and_charges", { + "description": "cess", + "percent": 4 + }) + + income_tax_slab.save() + if not dont_submit: + income_tax_slab.submit() def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): deducted_dates = [] diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.js b/erpnext/hr/doctype/salary_structure/salary_structure.js index 672be0c2fc2..2f2c815d70c 100755 --- a/erpnext/hr/doctype/salary_structure/salary_structure.js +++ b/erpnext/hr/doctype/salary_structure/salary_structure.js @@ -82,6 +82,7 @@ frappe.ui.form.on('Salary Structure', { {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, {fieldname:'base_variable', fieldtype:'Section Break'}, {fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1}, + {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'}, {fieldname:'base_col_br', fieldtype:'Column Break'}, {fieldname:'base', fieldtype:'Currency', label: __('Base')}, {fieldname:'variable', fieldtype:'Currency', label: __('Variable')} diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index 568277f8a73..df76458fe02 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -16,6 +16,7 @@ class SalaryStructure(Document): self.validate_amount() self.strip_condition_and_formula_fields() self.validate_max_benefits_with_flexi() + self.validate_component_based_on_tax_slab() def set_missing_values(self): overwritten_fields = ["depends_on_payment_days", "variable_based_on_taxable_salary", "is_tax_applicable", "is_flexible_benefit"] @@ -34,6 +35,12 @@ class SalaryStructure(Document): for fieldname in overwritten_fields_if_missing: d.set(fieldname, component_default_value.get(fieldname)) + def validate_component_based_on_tax_slab(self): + for row in self.deductions: + if row.variable_based_on_taxable_salary and (row.amount or row.formula): + frappe.throw(_("Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary") + .format(row.idx, row.salary_component)) + def validate_amount(self): if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet: frappe.throw(_("Net pay cannot be negative")) @@ -82,21 +89,23 @@ class SalaryStructure(Document): @frappe.whitelist() def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None, - from_date=None, base=None,variable=None): + from_date=None, base=None, variable=None, income_tax_slab=None): employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee) if employees: if len(employees) > 20: frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self,from_date=from_date, base=base,variable=variable) + employees=employees, salary_structure=self,from_date=from_date, + base=base, variable=variable, income_tax_slab=income_tax_slab) else: - assign_salary_structure_for_employees(employees, self, from_date=from_date, base=base,variable=variable) + assign_salary_structure_for_employees(employees, self, from_date=from_date, + base=base, variable=variable, income_tax_slab=income_tax_slab) else: frappe.msgprint(_("No Employee Found")) -def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None,variable=None): +def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) count=0 @@ -105,7 +114,8 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date continue count +=1 - salary_structures_assignment = create_salary_structures_assignment(employee, salary_structure, from_date, base, variable) + salary_structures_assignment = create_salary_structures_assignment(employee, + salary_structure, from_date, base, variable, income_tax_slab) salary_structures_assignments.append(salary_structures_assignment) frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) @@ -113,7 +123,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable): +def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None): assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee assignment.salary_structure = salary_structure.name @@ -121,6 +131,7 @@ def create_salary_structures_assignment(employee, salary_structure, from_date, b assignment.from_date = from_date assignment.base = base assignment.variable = variable + assignment.income_tax_slab = income_tax_slab assignment.save(ignore_permissions = True) assignment.submit() return assignment.name diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py index f67a189ee71..3e55a1a068d 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py @@ -9,8 +9,9 @@ from frappe.utils.make_random import get_random from frappe.utils import nowdate, add_days, add_years, getdate, add_months from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.hr.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ - make_deduction_salary_component, make_employee_salary_slip + make_deduction_salary_component, make_employee_salary_slip, create_tax_slab from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_payroll_period test_dependencies = ["Fiscal Year"] @@ -70,10 +71,8 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(sal_slip.get("earnings")[1].amount, 3000) self.assertEqual(sal_slip.get("earnings")[2].amount, 25000) self.assertEqual(sal_slip.get("gross_pay"), 78000) - self.assertEqual(sal_slip.get("deductions")[0].amount, 5000) - self.assertEqual(sal_slip.get("deductions")[1].amount, 5000) - self.assertEqual(sal_slip.get("total_deduction"), 10000) - self.assertEqual(sal_slip.get("net_pay"), 68000) + self.assertEqual(sal_slip.get("deductions")[0].amount, 200) + self.assertEqual(sal_slip.get("net_pay"), 78000 - sal_slip.get("total_deduction")) def test_whitespaces_in_formula_conditions_fields(self): salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", dont_submit=True) @@ -111,6 +110,7 @@ class TestSalaryStructure(unittest.TestCase): def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) + if not frappe.db.exists('Salary Structure', salary_structure): details = { "doctype": "Salary Structure", @@ -123,7 +123,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do } if other_details and isinstance(other_details, dict): details.update(other_details) - salary_structure_doc = frappe.get_doc(details).insert() + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() if not dont_submit: salary_structure_doc.submit() else: @@ -138,13 +139,18 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do def create_salary_structure_assignment(employee, salary_structure, from_date=None): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) + + payroll_period = create_payroll_period() + create_tax_slab(payroll_period, allow_tax_exemption=True) + salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee salary_structure_assignment.base = 50000 salary_structure_assignment.variable = 5000 - salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1) + salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure salary_structure_assignment.company = erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) + salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.submit() return salary_structure_assignment diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js index 56a05e04956..818e853154d 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -20,6 +20,16 @@ frappe.ui.form.on('Salary Structure Assignment', { } } }); + + frm.set_query("income_tax_slab", function() { + return { + filters: { + company: frm.doc.company, + docstatus: 1, + disabled: 0 + } + } + }); }, employee: function(frm) { if(frm.doc.employee){ diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json index 136eb0f8053..0098aa8ec80 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -10,11 +10,12 @@ "employee", "employee_name", "department", - "designation", + "company", "column_break_6", + "designation", "salary_structure", "from_date", - "company", + "income_tax_slab", "section_break_7", "base", "column_break_9", @@ -113,11 +114,17 @@ "options": "Salary Structure Assignment", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "income_tax_slab", + "fieldtype": "Link", + "label": "Income Tax Slab", + "options": "Income Tax Slab" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-31 17:05:28.637510", + "modified": "2020-04-25 18:24:23.617088", "modified_by": "Administrator", "module": "HR", "name": "Salary Structure Assignment", diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 4f5653dc67e..5c95e000f9c 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -9,6 +9,8 @@ from frappe.model.document import Document from frappe.desk.form import assign_to from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +class DuplicateDeclarationError(frappe.ValidationError): pass + class EmployeeBoardingController(Document): ''' Create the project and the task for the boarding process @@ -226,6 +228,17 @@ def get_employee_leave_policy(employee): else: frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee)) +def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): + existing_record = frappe.db.exists(doctype, { + "payroll_period": payroll_period, + "employee": employee, + 'docstatus': ['<', 2], + 'name': ['!=', docname] + }) + if existing_record: + frappe.throw(_("{0} already exists for employee {1} and period {2}") + .format(doctype, employee, payroll_period), DuplicateDeclarationError) + def validate_tax_declaration(declarations): subcategories = [] for d in declarations: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ad02a68faaa..d8ef786a15f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -659,3 +659,4 @@ erpnext.patches.v12_0.set_correct_status_for_expense_claim erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign +erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index fa3605ba5f1..83fb53d2a73 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -5,8 +5,7 @@ def execute(): frappe.reload_doc('hr', 'doctype', 'salary_detail') frappe.reload_doc('hr', 'doctype', 'salary_component') - frappe.db.sql("update `tabSalary Component` set is_payable=1, is_tax_applicable=1 where type='Earning'") - frappe.db.sql("update `tabSalary Component` set is_payable=0 where type='Deduction'") + frappe.db.sql("update `tabSalary Component` set is_tax_applicable=1 where type='Earning'") frappe.db.sql("""update `tabSalary Component` set variable_based_on_taxable_salary=1 where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""") diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py new file mode 100644 index 00000000000..a6aefac12ad --- /dev/null +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -0,0 +1,99 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if not frappe.db.table_exists("Payroll Period"): + return + + for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income"): + frappe.reload_doc("hr", "doctype", doctype) + + + for company in frappe.get_all("Company"): + payroll_periods = frappe.db.sql(""" + SELECT + name, start_date, end_date, standard_tax_exemption_amount + FROM + `tabPayroll Period` + WHERE company=%s + ORDER BY start_date DESC + """, company.name, as_dict = 1) + + for i, period in enumerate(payroll_periods): + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab:" + period.name + + if i == 0: + income_tax_slab.disabled = 0 + else: + income_tax_slab.disabled = 1 + + income_tax_slab.effective_from = period.start_date + income_tax_slab.company = company.name + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount + + income_tax_slab.flags.ignore_mandatory = True + income_tax_slab.submit() + + frappe.db.sql( + """ UPDATE `tabTaxable Salary Slab` + SET parent = %s , parentfield = 'slabs' , parenttype = "Income Tax Slab" + WHERE parent = %s + """, (income_tax_slab.name, period.name), as_dict = 1) + + if i == 0: + frappe.db.sql(""" + UPDATE + `tabSalary Structure Assignment` + set + income_tax_slab = %s + where + company = %s + and from_date >= %s + and docstatus < 2 + """, (income_tax_slab.name, company.name, period.start_date)) + + # move other incomes to separate document + migrated = [] + proofs = frappe.get_all("Employee Tax Exemption Proof Submission", + filters = {'docstatus': 1}, + fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + ) + for proof in proofs: + if proof.income_from_other_sources: + employee_other_income = frappe.new_doc("Employee Other Income") + employee_other_income.employee = proof.employee + employee_other_income.payroll_period = proof.payroll_period + employee_other_income.company = proof.company + employee_other_income.amount = proof.income_from_other_sources + + try: + employee_other_income.submit() + migrated.append([proof.employee, proof.payroll_period]) + except: + pass + + declerations = frappe.get_all("Employee Tax Exemption Declaration", + filters = {'docstatus': 1}, + fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + ) + + for declaration in declerations: + if declaration.income_from_other_sources \ + and [declaration.employee, declaration.payroll_period] not in migrated: + employee_other_income = frappe.new_doc("Employee Other Income") + employee_other_income.employee = declaration.employee + employee_other_income.payroll_period = declaration.payroll_period + employee_other_income.company = declaration.company + employee_other_income.amount = declaration.income_from_other_sources + + try: + employee_other_income.submit() + except: + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 1957e7fecca..da43cc65553 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -530,12 +530,18 @@ def make_fixtures(company=None): def set_salary_components(docs): docs.extend([ - {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', 'description': 'Professional Tax', 'type': 'Deduction'}, - {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', 'description': 'Provident fund', 'type': 'Deduction'}, - {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', 'description': 'House Rent Allowance', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Basic', 'description': 'Basic', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Arrear', 'description': 'Arrear', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', 'description': 'Leave Encashment', 'type': 'Earning'} + {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', + 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', + 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', + 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Basic', + 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Arrear', + 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', + 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1} ]) def set_tax_withholding_category(company): From b724fec6c96033547add180d3da95226b0dafe8e Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 26 Apr 2020 17:16:21 +0530 Subject: [PATCH 061/178] fix: Remove callback outside if condition --- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 039aa777818..67baee242f2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -305,10 +305,9 @@ frappe.ui.form.on('Stock Entry', { callback: function(r) { if (!r.exe && r.message){ frappe.model.set_value(cdt, cdn, "serial_no", r.message); - - if (callback) { - callback(); - } + } + if (callback) { + callback(); } } }); @@ -454,8 +453,9 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { - frm.events.set_basic_rate(frm, cdt, cdn); - frm.events.set_serial_no(frm, cdt, cdn); + frm.events.set_serial_no(frm, cdt, cdn, () => { + frm.events.set_basic_rate(frm, cdt, cdn); + }); }, conversion_factor: function(frm, cdt, cdn) { From 5bfdf0af4d2225bd54cb4182bbfb60e92dbc3657 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 26 Apr 2020 20:10:16 +0530 Subject: [PATCH 062/178] feat: Allow tax withholding category selection at invoice level (#20871) * feat: Allow tax withholding category selection at invoice level * fix: Linitng fixes * feat: TDS calculation using common PAN * fix: Add provision to deduct Lower TDS in purchase invoice * fix: Consider only ref docs company while computing TDS * fix: Default permission fixes * fix: Add validation for dates in fiscal year * fix: Undefined variable --- .../purchase_invoice/purchase_invoice.js | 15 +- .../purchase_invoice/purchase_invoice.json | 11 +- .../purchase_invoice/purchase_invoice.py | 2 +- .../tax_withholding_category.py | 128 +++++++++++++--- .../tds_computation_summary.py | 11 +- erpnext/patches.txt | 1 + .../add_permission_in_lower_deduction.py | 13 ++ .../lower_deduction_certificate/__init__.py | 0 .../lower_deduction_certificate.js | 8 + .../lower_deduction_certificate.json | 138 ++++++++++++++++++ .../lower_deduction_certificate.py | 26 ++++ .../test_lower_deduction_certificate.py | 10 ++ erpnext/regional/india/setup.py | 2 +- 13 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 erpnext/patches/v12_0/add_permission_in_lower_deduction.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/__init__.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 9292b633fc3..3cf4d5994a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -261,12 +261,25 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ price_list: this.frm.doc.buying_price_list }, function() { me.apply_pricing_rule(); - me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0; + me.frm.doc.tax_withholding_category = me.frm.supplier_tds; me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1); + me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1); }) }, + apply_tds: function(frm) { + var me = this; + + if (!me.frm.doc.apply_tds) { + me.frm.set_value("tax_withholding_category", ''); + me.frm.set_df_property("tax_withholding_category", "hidden", 1); + } else { + me.frm.set_value("tax_withholding_category", me.frm.supplier_tds); + me.frm.set_df_property("tax_withholding_category", "hidden", 0); + } + }, + credit_to: function() { var me = this; if(this.frm.doc.credit_to) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index c7dcf67a4df..a1a20de0503 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -16,6 +16,7 @@ "is_paid", "is_return", "apply_tds", + "tax_withholding_category", "column_break1", "company", "posting_date", @@ -1284,13 +1285,21 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "tax_withholding_category", + "fieldtype": "Link", + "hidden": 1, + "label": "Tax Withholding Category", + "options": "Tax Withholding Category", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-04-17 13:05:25.199832", + "modified": "2020-04-18 13:05:25.199832", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3a0c1239927..3654f6ef032 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -959,7 +959,7 @@ class PurchaseInvoice(BuyingController): if not self.apply_tds: return - tax_withholding_details = get_party_tax_withholding_details(self) + tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) if not tax_withholding_details: return 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 6c31e9efed1..dd6b4fdc603 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -6,23 +6,42 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt +from frappe.utils import flt, getdate from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): pass -def get_party_tax_withholding_details(ref_doc): - tax_withholding_category = frappe.db.get_value('Supplier', ref_doc.supplier, 'tax_withholding_category') +def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): + + pan_no = '' + suppliers = [] + + if not tax_withholding_category: + tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan']) + if not tax_withholding_category: return + if not pan_no: + pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan') + + # Get others suppliers with the same PAN No + if pan_no: + suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})] + + if not suppliers: + suppliers.append(ref_doc.supplier) + fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') .format(tax_withholding_category, ref_doc.company)) - tds_amount = get_tds_amount(ref_doc, tax_details, fy) + + tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company, + tax_details, fy, ref_doc.posting_date, pan_no) + tax_row = get_tax_row(tax_details, tds_amount) return tax_row @@ -51,6 +70,7 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year): frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) def get_tax_row(tax_details, tds_amount): + return { "category": "Total", "add_deduct_tax": "Deduct", @@ -60,25 +80,36 @@ def get_tax_row(tax_details, tds_amount): "tax_amount": tds_amount } -def get_tds_amount(ref_doc, tax_details, fiscal_year_details): +def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None): fiscal_year, year_start_date, year_end_date = fiscal_year_details tds_amount = 0 tds_deducted = 0 - def _get_tds(amount): + def _get_tds(amount, rate): if amount <= 0: return 0 - return amount * tax_details.rate / 100 + return amount * rate / 100 + + ldc_name = frappe.db.get_value('Lower Deduction Certificate', + { + 'pan_no': pan_no, + 'fiscal_year': fiscal_year + }, 'name') + ldc = '' + + if ldc_name: + ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name) entries = frappe.db.sql(""" select voucher_no, credit from `tabGL Entry` - where party=%s and fiscal_year=%s and credit > 0 - """, (ref_doc.supplier, fiscal_year), as_dict=1) + where company = %s and + party in %s and fiscal_year=%s and credit > 0 + """, (company, tuple(suppliers), fiscal_year), as_dict=1) vouchers = [d.voucher_no for d in entries] - advance_vouchers = get_advance_vouchers(ref_doc.supplier, fiscal_year) + advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company) tds_vouchers = vouchers + advance_vouchers @@ -93,7 +124,20 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details): tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0 if tds_deducted: - tds_amount = _get_tds(ref_doc.net_total) + if ldc: + limit_consumed = frappe.db.get_value('Purchase Invoice', + { + 'supplier': ('in', suppliers), + 'apply_tds': 1, + 'docstatus': 1 + }, 'sum(net_total)') + + if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, + ldc.certificate_limit): + + tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + else: + tds_amount = _get_tds(net_total, tax_details.rate) else: supplier_credit_amount = frappe.get_all('Purchase Invoice Item', fields = ['sum(net_amount)'], @@ -106,43 +150,79 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details): fields = ['sum(credit_in_account_currency)'], filters = { 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ref_doc.supplier, + 'party': ('in', suppliers), 'reference_type': ('not in', ['Purchase Invoice']) }, as_list=1) supplier_credit_amount += (jv_supplier_credit_amt[0][0] if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0) - supplier_credit_amount += ref_doc.net_total + supplier_credit_amount += net_total - debit_note_amount = get_debit_note_amount(ref_doc.supplier, year_start_date, year_end_date) + 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) or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): - tds_amount = _get_tds(supplier_credit_amount) + + if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, + ldc.certificate_limit): + tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate, + tax_details) + else: + tds_amount = _get_tds(supplier_credit_amount, tax_details.rate) return tds_amount -def get_advance_vouchers(supplier, fiscal_year=None, company=None, from_date=None, to_date=None): +def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None): condition = "fiscal_year=%s" % fiscal_year + + if company: + condition += "and company =%s" % (company) if from_date and to_date: - condition = "company=%s and posting_date between %s and %s" % (company, from_date, to_date) + condition += "and posting_date between %s and %s" % (company, from_date, to_date) + + ## Appending the same supplier again if length of suppliers list is 1 + ## since tuple of single element list contains None, For example ('Test Supplier 1', ) + ## and the below query fails + if len(suppliers) == 1: + suppliers.append(suppliers[0]) return frappe.db.sql_list(""" select distinct voucher_no from `tabGL Entry` - where party=%s and %s and debit > 0 - """, (supplier, condition)) or [] + where party in %s and %s and debit > 0 + """, (tuple(suppliers), condition)) or [] -def get_debit_note_amount(supplier, year_start_date, year_end_date, company=None): - condition = "" +def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): + condition = "and 1=1" if company: condition = " and company=%s " % company + if len(suppliers) == 1: + suppliers.append(suppliers[0]) + return flt(frappe.db.sql(""" select abs(sum(net_total)) from `tabPurchase Invoice` - where supplier=%s %s and is_return=1 and docstatus=1 - and posting_date between %s and %s - """, (supplier, condition, year_start_date, year_end_date))) \ No newline at end of file + where supplier in %s and is_return=1 and docstatus=1 + and posting_date between %s and %s %s + """, (tuple(suppliers), year_start_date, year_end_date, condition))) + +def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): + if current_amount < (certificate_limit - deducted_amount): + return current_amount * rate/100 + else: + ltds_amount = (certificate_limit - deducted_amount) + tds_amount = current_amount - ltds_amount + + return ltds_amount * rate/100 + tds_amount * tax_details.rate/100 + +def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit): + valid = False + + if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and + certificate_limit > deducted_amount): + valid = True + + return valid \ No newline at end of file diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 2e805f8d3f5..c7cfee74cb0 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -44,9 +44,14 @@ def get_result(filters): out = [] for supplier in filters.supplier: tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category) - rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year][0] + rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year] + + if rate: + rate = rate[0] + try: account = [d.account for d in tds.accounts if d.company == filters.company][0] + except IndexError: account = [] total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account, @@ -76,7 +81,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): supplier_credit_amount = flt(sum([d.credit for d in entries])) vouchers = [d.voucher_no for d in entries] - vouchers += get_advance_vouchers(supplier, company=company, + vouchers += get_advance_vouchers([supplier], company=company, from_date=from_date, to_date=to_date) tds_deducted = 0 @@ -89,7 +94,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): """.format(', '.join(["'%s'" % d for d in vouchers])), (account, from_date, to_date, company))[0][0]) - debit_note_amount = get_debit_note_amount(supplier, from_date, to_date, company=company) + debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company) total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d8ef786a15f..dd3bc240917 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -651,6 +651,7 @@ erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.add_export_type_field_in_party_master erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 erpnext.patches.v12_0.create_irs_1099_field_united_states +erpnext.patches.v12_0.add_permission_in_lower_deduction erpnext.patches.v12_0.set_permission_einvoicing erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.recalculate_requested_qty_in_bin diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py new file mode 100644 index 00000000000..af9bf74f30e --- /dev/null +++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py @@ -0,0 +1,13 @@ +import frappe +from frappe.permissions import add_permission, update_permission_property + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + frappe.reload_doc('regional', 'doctype', 'Lower Deduction Certificate') + + add_permission('Lower Deduction Certificate', 'Accounts Manager', 0) + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1) + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/__init__.py b/erpnext/regional/doctype/lower_deduction_certificate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js new file mode 100644 index 00000000000..8257bf8a969 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Lower Deduction Certificate', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json new file mode 100644 index 00000000000..f48fe6f4763 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "field:certificate_no", + "creation": "2020-03-10 23:12:10.072631", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "certificate_details_section", + "section_code", + "fiscal_year", + "column_break_3", + "certificate_no", + "section_break_3", + "supplier", + "column_break_7", + "pan_no", + "validity_details_section", + "valid_from", + "column_break_10", + "valid_upto", + "section_break_9", + "rate", + "column_break_14", + "certificate_limit" + ], + "fields": [ + { + "fieldname": "certificate_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Certificate No", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_code", + "fieldtype": "Select", + "label": "Section Code", + "options": "192\n193\n194\n194A\n194C\n194D\n194H\n194I\n194J\n194LA\n194LBB\n194LBC\n195", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Deductee Details" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "reqd": 1 + }, + { + "fetch_from": "supplier.pan", + "fetch_if_empty": 1, + "fieldname": "pan_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "PAN No", + "reqd": 1 + }, + { + "fieldname": "validity_details_section", + "fieldtype": "Section Break", + "label": "Validity Details" + }, + { + "fieldname": "valid_upto", + "fieldtype": "Date", + "label": "Valid Upto", + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "fieldname": "rate", + "fieldtype": "Percent", + "label": "Rate Of TDS As Per Certificate", + "reqd": 1 + }, + { + "fieldname": "certificate_limit", + "fieldtype": "Currency", + "label": "Certificate Limit", + "reqd": 1 + }, + { + "fieldname": "certificate_details_section", + "fieldtype": "Section Break", + "label": "Certificate Details" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "valid_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Valid From", + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "fieldtype": "Link", + "label": "Fiscal Year", + "options": "Fiscal Year", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-23 23:04:41.203721", + "modified_by": "Administrator", + "module": "Regional", + "name": "Lower Deduction Certificate", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py new file mode 100644 index 00000000000..e8a8ed87505 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import getdate +from frappe.model.document import Document +from erpnext.accounts.utils import get_fiscal_year + +class LowerDeductionCertificate(Document): + def validate(self): + if getdate(self.valid_upto) < getdate(self.valid_from): + frappe.throw(_("Valid Upto date cannot be before Valid From date")) + + fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) + + if not (fiscal_year.year_start_date <= getdate(self.valid_from) \ + <= fiscal_year.year_end_date): + frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + + if not (fiscal_year.year_start_date <= getdate(self.valid_upto) \ + <= fiscal_year.year_end_date): + frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + diff --git a/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py new file mode 100644 index 00000000000..7e950206fcc --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLowerDeductionCertificate(unittest.TestCase): + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index da43cc65553..77a466fdff7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -77,7 +77,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) From 1f3584b9a8162fd432f7ad3832ae1aaf2e3357c3 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Sun, 26 Apr 2020 21:05:09 +0530 Subject: [PATCH 063/178] refactor: employee leave balance report v12 (#21282) * fix: Employee Balance repor fixes * refactor: Employee Leave Balance report For Version-12 Co-authored-by: Nabin Hait --- .../employee_leave_balance.py | 104 +++++++++++++++--- .../employee_leave_balance_summary.py | 54 ++++----- 2 files changed, 112 insertions(+), 46 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 35c8630e8e2..d20018eef34 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -8,9 +8,6 @@ from frappe.utils import flt from erpnext.hr.doctype.leave_application.leave_application \ import get_leave_balance_on, get_leaves_for_period -from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary \ - import get_department_leave_approver_map - def execute(filters=None): leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") @@ -28,6 +25,8 @@ def get_columns(leave_types): for leave_type in leave_types: columns.append(_(leave_type) + " " + _("Opening") + ":Float:160") + columns.append(_(leave_type) + " " + _("Allocated") + ":Float:160") + columns.append(_(leave_type) + " " + _("Expired") + ":Float:160") columns.append(_(leave_type) + " " + _("Taken") + ":Float:160") columns.append(_(leave_type) + " " + _("Balance") + ":Float:160") @@ -68,18 +67,97 @@ def get_data(filters, leave_types): row = [employee.name, employee.employee_name, employee.department] for leave_type in leave_types: - # leaves taken - leaves_taken = get_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) * -1 - # opening balance - opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) - # closing balance - closing = max(opening - leaves_taken, 0) - - row += [opening, leaves_taken, closing] + row += calculate_leaves_details(filters, leave_type, employee) data.append(row) + return data - return data \ No newline at end of file +def calculate_leaves_details(filters, leave_type, employee): + ledger_entries = get_leave_ledger_entries(filters.from_date, filters.to_date, employee.name, leave_type) + + #Leaves Deducted consist of both expired and leaves taken + leaves_deducted = get_leaves_for_period(employee.name, leave_type, + filters.from_date, filters.to_date) * -1 + + # removing expired leaves + leaves_taken = leaves_deducted - remove_expired_leave(ledger_entries) + + opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) + + new_allocation , expired_allocation = get_allocated_and_expired_leaves(ledger_entries, filters.from_date, filters.to_date) + + #removing leaves taken from expired_allocation + expired_leaves = max(expired_allocation - leaves_taken, 0) + + #Formula for calculating closing balance + closing = max(opening + new_allocation - (leaves_taken + expired_leaves), 0) + + return [opening, new_allocation, expired_leaves, leaves_taken, closing] + + +def remove_expired_leave(records): + expired_within_period = 0 + for record in records: + if record.is_expired: + expired_within_period += record.leaves + return expired_within_period * -1 + + +def get_allocated_and_expired_leaves(records, from_date, to_date): + + from frappe.utils import getdate + + new_allocation = 0 + expired_leaves = 0 + + for record in records: + if record.to_date <= getdate(to_date) and record.leaves>0: + expired_leaves += record.leaves + + if record.from_date >= getdate(from_date) and record.leaves>0: + new_allocation += record.leaves + + return new_allocation, expired_leaves + +def get_leave_ledger_entries(from_date, to_date, employee, leave_type): + records= frappe.db.sql(""" + SELECT + employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type + is_carry_forward, is_expired + FROM `tabLeave Ledger Entry` + WHERE employee=%(employee)s AND leave_type=%(leave_type)s + AND docstatus=1 + AND (from_date between %(from_date)s AND %(to_date)s + OR to_date between %(from_date)s AND %(to_date)s + OR (from_date < %(from_date)s AND to_date > %(to_date)s)) + """, { + "from_date": from_date, + "to_date": to_date, + "employee": employee, + "leave_type": leave_type + }, as_dict=1) + + return records + +def get_department_leave_approver_map(department=None): + conditions='' + if department: + conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} + + # get current department and all its child + department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec + + # retrieve approvers list from current department and from its subsequent child departments + approver_list = frappe.get_all('Department Approver', filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, fields=['parent', 'approver'], as_list=1) + + approvers = {} + + for k, v in approver_list: + approvers.setdefault(k, []).append(v) + + return approvers \ No newline at end of file diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 777de022387..53f8f41915e 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -6,6 +6,7 @@ import frappe from frappe.utils import flt from frappe import _ from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on +from erpnext.hr.report.employee_leave_balance.employee_leave_balance import calculate_leaves_details , get_department_leave_approver_map def execute(filters=None): if filters.to_date <= filters.from_date: @@ -38,17 +39,27 @@ def get_columns(): 'label': _('Opening Balance'), 'fieldtype': 'float', 'fieldname': 'opening_balance', - 'width': 160, + 'width': 120, + }, { + 'label': _('New Allocation'), + 'fieldtype': 'Float', + 'fieldname': 'new_allocation', + 'width': 120, + }, { + 'label': _('Expired Leaves'), + 'fieldtype': 'Float', + 'fieldname': 'expired_leaves', + 'width': 120, }, { 'label': _('Leaves Taken'), 'fieldtype': 'float', 'fieldname': 'leaves_taken', - 'width': 160, + 'width': 120, }, { 'label': _('Closing Balance'), 'fieldtype': 'float', 'fieldname': 'closing_balance', - 'width': 160, + 'width': 120, }] return columns @@ -72,7 +83,7 @@ def get_data(filters): 'leave_type': leave_type }) for employee in active_employees: - + leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ @@ -82,16 +93,13 @@ def get_data(filters): 'employee_name': employee.employee_name }) - leaves_taken = get_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) * -1 + leave_details = calculate_leaves_details(filters, leave_type, employee) + row.opening_balance = flt(leave_details[0]) + row.new_allocation = flt(leave_details[1]) + row.expired_leaves = flt(leave_details[2]) + row.leaves_taken = flt(leave_details[3]) + row.closing_balance = flt(leave_details[4]) - opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) - closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) - - row.opening_balance = opening - row.leaves_taken = leaves_taken - row.closing_balance = closing - row.indent = 1 data.append(row) return data @@ -108,23 +116,3 @@ def get_conditions(filters): return conditions -def get_department_leave_approver_map(department=None): - conditions='' - if department: - conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} - - # get current department and all its child - department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec - - # retrieve approvers list from current department and from its subsequent child departments - approver_list = frappe.get_all('Department Approver', filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, fields=['parent', 'approver'], as_list=1) - - approvers = {} - - for k, v in approver_list: - approvers.setdefault(k, []).append(v) - - return approvers From de6f0f7a0591234d0bc9e6e12005b794128816ac Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sun, 26 Apr 2020 23:27:29 +0530 Subject: [PATCH 064/178] fix: add quality inspection template (#21424) --- .../quality_inspection_template_dashboard.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py new file mode 100644 index 00000000000..db459575f3e --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template_dashboard.py @@ -0,0 +1,12 @@ +from frappe import _ + +def get_data(): + return { + 'fieldname': 'quality_inspection_template', + 'transactions': [ + { + 'label': _('Quality Inspection'), + 'items': ['Quality Inspection'] + } + ] + } \ No newline at end of file From 2b5bca4ce68c5cefd0df9d96b9112fe8de294028 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 23:28:54 +0530 Subject: [PATCH 065/178] fix: Procurement tracker report (#21422) * fix: procurement report data was not coming * fix: leave allocation minor issue --- .../procurement_tracker.py | 74 +++++++++---------- .../leave_allocation/leave_allocation.py | 6 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 866bf0c7332..39668795cba 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -141,19 +141,18 @@ def get_conditions(filters): conditions = "" if filters.get("company"): - conditions += " AND company=%s"% frappe.db.escape(filters.get('company')) + conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company')) if filters.get("cost_center") or filters.get("project"): conditions += """ - AND (cost_center=%s - OR project=%s) - """% (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) + AND (child.`cost_center`=%s OR child.`project`=%s) + """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) if filters.get("from_date"): - conditions += " AND transaction_date>=%s"% filters.get('from_date') + conditions += " AND par.transaction_date>='%s'" % filters.get('from_date') if filters.get("to_date"): - conditions += " AND transaction_date<=%s"% filters.get('to_date') + conditions += " AND par.transaction_date<='%s'" % filters.get('to_date') return conditions def get_data(filters): @@ -162,7 +161,6 @@ def get_data(filters): mr_records, procurement_record_against_mr = get_mapped_mr_details(conditions) pr_records = get_mapped_pr_records() pi_records = get_mapped_pi_records() - print(pi_records) procurement_record=[] if procurement_record_against_mr: @@ -198,16 +196,16 @@ def get_mapped_mr_details(conditions): mr_records = {} mr_details = frappe.db.sql(""" SELECT - mr.transaction_date, - mr.per_ordered, - mr_item.name, - mr_item.parent, - mr_item.amount - FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item + par.transaction_date, + par.per_ordered, + child.name, + child.parent, + child.amount + FROM `tabMaterial Request` par, `tabMaterial Request Item` child WHERE - mr.per_ordered>=0 - AND mr.name=mr_item.parent - AND mr.docstatus=1 + par.per_ordered>=0 + AND par.name=child.parent + AND par.docstatus=1 {conditions} """.format(conditions=conditions), as_dict=1) #nosec @@ -254,29 +252,29 @@ def get_mapped_pr_records(): def get_po_entries(conditions): return frappe.db.sql(""" SELECT - po_item.name, - po_item.parent, - po_item.cost_center, - po_item.project, - po_item.warehouse, - po_item.material_request, - po_item.material_request_item, - po_item.description, - po_item.stock_uom, - po_item.qty, - po_item.amount, - po_item.base_amount, - po_item.schedule_date, - po.transaction_date, - po.supplier, - po.status, - po.owner - FROM `tabPurchase Order` po, `tabPurchase Order Item` po_item + child.name, + child.parent, + child.cost_center, + child.project, + child.warehouse, + child.material_request, + child.material_request_item, + child.description, + child.stock_uom, + child.qty, + child.amount, + child.base_amount, + child.schedule_date, + par.transaction_date, + par.supplier, + par.status, + par.owner + FROM `tabPurchase Order` par, `tabPurchase Order Item` child WHERE - po.docstatus = 1 - AND po.name = po_item.parent - AND po.status not in ("Closed","Completed","Cancelled") + par.docstatus = 1 + AND par.name = child.parent + AND par.status not in ("Closed","Completed","Cancelled") {conditions} GROUP BY - po.name,po_item.item_code + par.name, child.item_code """.format(conditions=conditions), as_dict=1) #nosec \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index d13bb4577cd..03fe3fa035c 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -30,16 +30,16 @@ class LeaveAllocation(Document): def validate_leave_allocation_days(self): company = frappe.db.get_value("Employee", self.employee, "company") leave_period = get_leave_period(self.from_date, self.to_date, company) - max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")) if max_leaves_allowed > 0: leave_allocated = 0 if leave_period: leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date) - leave_allocated += self.new_leaves_allocated + leave_allocated += flt(self.new_leaves_allocated) if leave_allocated > max_leaves_allowed: frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period") - .format(self.leave_type, self.employee)) + .format(self.leave_type, self.employee)) def on_submit(self): self.create_leave_ledger_entry() From c2f952045cd83fd54a24a16ad4ebbcee16746b01 Mon Sep 17 00:00:00 2001 From: Anoop Date: Mon, 27 Apr 2020 00:33:26 +0530 Subject: [PATCH 066/178] fix: name 'patient' is not defined possible escape while refactoring, fixed in develop added here. Reported via - https://discuss.erpnext.com/t/healthcare-outpatient-sms-alert-error/60812 --- .../doctype/patient_appointment/patient_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 8f3602af67e..4336fe51606 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -352,7 +352,7 @@ def send_message(doc, message): # jinja to string convertion happens here message = frappe.render_template(message, context) - number = [patient.mobile] + number = [patient_mobile] send_sms(number, message) From 093720b8703177cae771f76511932bff286653b4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 10:36:11 +0530 Subject: [PATCH 067/178] fix: add hook for sending appointment reminders in v12 (#21432) --- erpnext/hooks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 316c3392851..d387f19692e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -268,7 +268,8 @@ doc_events = { scheduler_events = { "all": [ - "erpnext.projects.doctype.project.project.project_status_update_reminder" + "erpnext.projects.doctype.project.project.project_status_update_reminder", + "erpnext.healthcare.doctype.patient_appointment.patient_appointment.set_appointment_reminder" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', From 6b082b5edbdc9cbe9fa84910f80a907294d9fe34 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:03 +0530 Subject: [PATCH 068/178] fix: E-way bill fix in sales invoice --- .../doctype/sales_invoice/regional/india.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index 7941a59ac36..1ed4b92e7a4 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -25,18 +25,26 @@ frappe.ui.form.on("Sales Invoice", { if(frm.doc.docstatus == 1 && !frm.is_dirty() && !frm.doc.is_return && !frm.doc.ewaybill) { - frm.add_custom_button('e-Way Bill JSON', () => { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(frm.doc.doctype) - + "&dn=" + encodeURIComponent(frm.doc.name) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } - }, __("Make")); + frm.add_custom_button('E-Way Bill JSON', () => { + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': frm.doc.doctype, + 'dn': [frm.doc.name] + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } + } + }); + + }, __("Create")); } }, From 93e8b5d87ef96be154f325575addac1cb5656996 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:17 +0530 Subject: [PATCH 069/178] fix: E-way bill fix in List view --- .../sales_invoice/regional/india_list.js | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js index 66d74b4b06a..05c4f1f1dd3 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js @@ -16,17 +16,23 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) { } } - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(doclist.doctype) - + "&dn=" + encodeURIComponent(docnames) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } - + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': doclist.doctype, + 'dn': docnames + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: docnames + }; + open_url_post(frappe.request.url, args); + } + } + }); }; doclist.page.add_actions_menu_item(__('Generate e-Way Bill JSON'), action, false); From 68d7b773145aa3798e06cd22e91054e46fd22eef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:40 +0530 Subject: [PATCH 070/178] fix: Utils messsage cleanup --- erpnext/regional/india/utils.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 831985fc7cc..59c6436d906 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -360,8 +360,6 @@ def get_ewb_data(dt, dn): if dt != 'Sales Invoice': frappe.throw(_('e-Way Bill JSON can only be generated from Sales Invoice')) - dn = dn.split(',') - ewaybills = [] for doc_name in dn: doc = frappe.get_doc(dt, doc_name) @@ -439,18 +437,24 @@ def get_ewb_data(dt, dn): @frappe.whitelist() def generate_ewb_json(dt, dn): + dn = json.loads(dn) + return get_ewb_data(dt, dn) - data = get_ewb_data(dt, dn) +@frappe.whitelist() +def download_ewb_json(): + data = frappe._dict(frappe.local.form_dict) - frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True) + frappe.local.response.filecontent = json.dumps(data['data'], indent=4, sort_keys=True) frappe.local.response.type = 'download' - if len(data['billLists']) > 1: + billList = json.loads(data['data'])['billLists'] + + if len(billList) > 1: doc_name = 'Bulk' else: - doc_name = dn + doc_name = data['docname'] - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5)) + frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(data['docname'], frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): From 5aa224143986e07062859e9556bf1c8ed3ee18f0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 14:05:45 +0530 Subject: [PATCH 071/178] fix: Test --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- erpnext/regional/india/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ced829ee6ee..0b06342f309 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1834,7 +1834,7 @@ class TestSalesInvoice(unittest.TestCase): si.submit() - data = get_ewb_data("Sales Invoice", si.name) + data = get_ewb_data("Sales Invoice", [si.name]) self.assertEqual(data['version'], '1.0.1118') self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 59c6436d906..e0996c1800d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -454,7 +454,7 @@ def download_ewb_json(): else: doc_name = data['docname'] - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(data['docname'], frappe.utils.random_string(5)) + frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): From 95f7807b78afd6d11fc0237db9955d6cc5900e50 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 28 Apr 2020 11:15:57 +0530 Subject: [PATCH 072/178] fix: Income tax slab patch (#21448) * fix: reload income_tax_slab_other_charges in patch * fix: reload lower_deduction_certificate in patch --- erpnext/patches/v11_0/add_permissions_in_gst_settings.py | 1 + .../move_tax_slabs_from_payroll_period_to_income_tax_slab.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index 121a20288c3..d7936110edb 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -6,4 +6,5 @@ def execute(): if not company: return + frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") add_permissions() \ No newline at end of file diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index a6aefac12ad..179be2cfde5 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -10,7 +10,7 @@ def execute(): if not frappe.db.table_exists("Payroll Period"): return - for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income"): + for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): frappe.reload_doc("hr", "doctype", doctype) From 558c3284a00d54ad443726ee64cb0313d6cdf284 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 11:17:45 +0530 Subject: [PATCH 073/178] fix: Default column width in Gross profit report --- .../report/gross_profit/gross_profit.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 6ef6d6eea03..4e22b05a81d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -55,27 +55,27 @@ def get_columns(group_wise_columns, filters): columns = [] column_map = frappe._dict({ "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date", - "posting_time": _("Posting Time"), - "item_code": _("Item Code") + ":Link/Item", - "item_name": _("Item Name"), - "item_group": _("Item Group") + ":Link/Item Group", - "brand": _("Brand"), - "description": _("Description"), - "warehouse": _("Warehouse") + ":Link/Warehouse", - "qty": _("Qty") + ":Float", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency", - "buying_rate": _("Valuation Rate") + ":Currency/currency", - "base_amount": _("Selling Amount") + ":Currency/currency", - "buying_amount": _("Buying Amount") + ":Currency/currency", - "gross_profit": _("Gross Profit") + ":Currency/currency", - "gross_profit_percent": _("Gross Profit %") + ":Percent", - "project": _("Project") + ":Link/Project", + "posting_date": _("Posting Date") + ":Date:100", + "posting_time": _("Posting Time") + ":Data:100", + "item_code": _("Item Code") + ":Link/Item:100", + "item_name": _("Item Name") + ":Data:100", + "item_group": _("Item Group") + ":Link/Item Group:100", + "brand": _("Brand") + ":Link/Brand:100", + "description": _("Description") +":Data:100", + "warehouse": _("Warehouse") + ":Link/Warehouse:100", + "qty": _("Qty") + ":Float:80", + "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", + "buying_rate": _("Valuation Rate") + ":Currency/currency:100", + "base_amount": _("Selling Amount") + ":Currency/currency:100", + "buying_amount": _("Buying Amount") + ":Currency/currency:100", + "gross_profit": _("Gross Profit") + ":Currency/currency:100", + "gross_profit_percent": _("Gross Profit %") + ":Percent:100", + "project": _("Project") + ":Link/Project:100", "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency", - "customer": _("Customer") + ":Link/Customer", - "customer_group": _("Customer Group") + ":Link/Customer Group", - "territory": _("Territory") + ":Link/Territory" + "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", + "customer": _("Customer") + ":Link/Customer:100", + "customer_group": _("Customer Group") + ":Link/Customer Group:100", + "territory": _("Territory") + ":Link/Territory:100" }) for col in group_wise_columns.get(scrub(filters.group_by)): @@ -85,7 +85,8 @@ def get_columns(group_wise_columns, filters): "fieldname": "currency", "label" : _("Currency"), "fieldtype": "Link", - "options": "Currency" + "options": "Currency", + "hidden": 1 }) return columns @@ -277,7 +278,7 @@ class GrossProfitGenerator(object): from `tabPurchase Invoice Item` a where a.item_code = %s and a.docstatus=1 and modified <= %s - order by a.modified desc limit 1""", (item_code,self.filters.to_date)) + order by a.modified desc limit 1""", (item_code, self.filters.to_date)) else: last_purchase_rate = frappe.db.sql(""" select (a.base_rate / a.conversion_factor) From fa7d49641363cf6fc8e1ed5625ffd08fa333cd6c Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 28 Apr 2020 13:00:51 +0530 Subject: [PATCH 074/178] fix: Blanket Order in SO/PO child tables (#21444) --- .../doctype/purchase_order/purchase_order.js | 9 --------- erpnext/controllers/queries.py | 13 +++++++++++++ erpnext/public/js/controllers/transaction.js | 14 ++++++++++++++ erpnext/selling/doctype/sales_order/sales_order.js | 9 --------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 7b1f1354d79..fa4c76f364f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -27,15 +27,6 @@ frappe.ui.form.on("Purchase Order", { frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) - frm.set_query("blanket_order", "items", function() { - return { - filters: { - "company": frm.doc.company, - "docstatus": 1 - } - } - }); - frm.set_query("expense_account", "items", function() { return { query: "erpnext.controllers.queries.get_expense_account", diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 163ef72ee10..c58dd7a8e36 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -364,6 +364,19 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) +def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): + return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date + from `tabBlanket Order` bo, `tabBlanket Order Item` boi + where + boi.parent = bo.name + and boi.item_code = {item_code} + and bo.blanket_order_type = '{blanket_order_type}' + and bo.company = {company} + and bo.docstatus = 1""" + .format(item_code = frappe.db.escape(filters.get("item")), + blanket_order_type = filters.get("blanket_order_type"), + company = frappe.db.escape(filters.get("company")) + )) @frappe.whitelist() def get_income_account(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6def5cb51f3..3efb53933e1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -175,6 +175,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }; } + if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) { + this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + query: "erpnext.controllers.queries.get_blanket_orders", + filters: { + "company": doc.company, + "blanket_order_type": doc.doctype === "Sales Order" ? "Selling" : "Purchasing", + "item": item.item_code + } + } + }); + } + }, onload: function() { var me = this; diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fa765dfaad9..b4e151b2e30 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -65,15 +65,6 @@ frappe.ui.form.on("Sales Order", { } }); - frm.set_query("blanket_order", "items", function() { - return { - filters: { - "company": frm.doc.company, - "docstatus": 1 - } - } - }); - erpnext.queries.setup_warehouse_query(frm); }, From dd8566ecde94887c44cbac8b8a15d51ecc391d70 Mon Sep 17 00:00:00 2001 From: Anoop Date: Tue, 28 Apr 2020 14:52:10 +0530 Subject: [PATCH 075/178] fix: clinical procedure - set stock entry type clinical procedure - complete and consume - set stock entry type to "Material Issue" report - https://discuss.erpnext.com/t/cant-complete-and-consume/60927 --- .../healthcare/doctype/clinical_procedure/clinical_procedure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index 7c6f4d5999e..a98ae25e43b 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -158,7 +158,7 @@ def get_item_dict(table, parent, parenttype): def create_stock_entry(doc): stock_entry = frappe.new_doc("Stock Entry") stock_entry = set_stock_items(stock_entry, doc.name, "Clinical Procedure") - stock_entry.purpose = "Material Issue" + stock_entry.stock_entry_type = "Material Issue" stock_entry.from_warehouse = doc.warehouse stock_entry.company = doc.company expense_account = get_account(None, "expense_account", "Healthcare Settings", doc.company) From 7c67c38bc555402c33f232d70458a24048a33673 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 28 Apr 2020 16:08:52 +0530 Subject: [PATCH 076/178] fix: update lead if contact details are changed --- erpnext/crm/utils.py | 24 ++++++++++++++++++++++++ erpnext/hooks.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 erpnext/crm/utils.py diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py new file mode 100644 index 00000000000..72d778e9568 --- /dev/null +++ b/erpnext/crm/utils.py @@ -0,0 +1,24 @@ +import frappe + + +def update_lead_phone_numbers(contact, method): + if contact.phone_nos: + contact_lead = contact.get_link_for("Lead") + if contact_lead: + phone = mobile_no = contact.phone_nos[0].phone + + if len(contact.phone_nos) > 1: + # get the default phone number + primary_phones = [phone.phone for phone in contact.phone_nos if phone.is_primary_phone] + if primary_phones: + phone = primary_phones[0] + + # get the default mobile number + primary_mobile_nos = [phone.phone for phone in contact.phone_nos if phone.is_primary_mobile_no] + if primary_mobile_nos: + mobile_no = primary_mobile_nos[0] + + lead = frappe.get_doc("Lead", contact_lead) + lead.phone = phone + lead.mobile_no = mobile_no + lead.save() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index d387f19692e..6712842948e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -256,7 +256,8 @@ doc_events = { }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", - "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" + "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", + "validate": "erpnext.crm.utils.update_lead_phone_numbers" }, "Lead": { "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" From c832291bd69b5cbf01afb7c0ddd417eea1717c24 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Apr 2020 19:18:45 +0530 Subject: [PATCH 077/178] fix: Handle empty child table --- erpnext/public/js/controllers/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 215282161d1..7b817f66afa 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1851,8 +1851,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, autofill_warehouse : function (child_table, warehouse_field, warehouse) { - let doctype = child_table[0].doctype; - if (warehouse) { + if (warehouse && child_table && child_table.length) { + let doctype = child_table[0].doctype; $.each(child_table || [], function(i, item) { frappe.model.set_value(doctype, item.name, warehouse_field, warehouse); }); From bd9625d150a6a904138f6805f035bb3df1b6fc83 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 20:19:26 +0530 Subject: [PATCH 078/178] fix: Remove duplicate code from accounting dimension --- .../public/js/utils/dimension_tree_filter.js | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 75c5a820b46..b223fc557be 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -24,7 +24,7 @@ doctypes_with_dimensions.forEach((doctype) => { onload: function(frm) { erpnext.dimension_filters.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { - if (frappe.meta.has_field(dimension['document_type'], 'is_group')) { + if(frappe.meta.has_field(dimension['document_type'], 'is_group')) { frm.set_query(dimension['fieldname'], { "is_group": 0 }); @@ -42,19 +42,21 @@ doctypes_with_dimensions.forEach((doctype) => { update_dimension: function(frm) { erpnext.dimension_filters.forEach((dimension) => { - if (frm.is_new()) { - if (frm.doc.company && Object.keys(default_dimensions || {}).length > 0 + if(frm.is_new()) { + if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0 && default_dimensions[frm.doc.company]) { - if (frappe.meta.has_field(doctype, dimension['fieldname'])) { - frm.set_value(dimension['fieldname'], - default_dimensions[frm.doc.company][dimension['document_type']]); - } + let default_dimension = default_dimensions[frm.doc.company][dimension['document_type']]; - $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { - frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], - default_dimensions[frm.doc.company][dimension['document_type']]) - }); + if(default_dimension) { + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + frm.set_value(dimension['fieldname'], default_dimension); + } + + $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { + frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], default_dimension); + }); + } } } }); @@ -71,20 +73,6 @@ child_docs.forEach((doctype) => { }); }, - accounts_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]); - }); - }, - - items_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]); - }); - }, - accounts_add: function(frm, cdt, cdn) { erpnext.dimension_filters.forEach((dimension) => { var row = frappe.get_doc(cdt, cdn); From 3d2dcd8c599b30909c11b6a99d9ab9795f7d784d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 29 Apr 2020 02:27:47 +0530 Subject: [PATCH 079/178] fix: payment request not able to make against fees --- erpnext/accounts/doctype/payment_request/payment_request.py | 6 +++--- erpnext/education/doctype/fees/fees.js | 2 ++ erpnext/education/doctype/fees/fees.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f2b0d3db7a1..4e4eac0610f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -326,7 +326,7 @@ def make_payment_request(**args): "reference_doctype": args.dt, "reference_name": args.dn, "party_type": args.get("party_type") or "Customer", - "party": args.get("party") or ref_doc.customer, + "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account }) @@ -420,7 +420,7 @@ def make_payment_entry(docname): def update_payment_req_status(doc, method): from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details - + for ref in doc.references: payment_request_name = frappe.db.get_value("Payment Request", {"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name, @@ -430,7 +430,7 @@ def update_payment_req_status(doc, method): ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency) pay_req_doc = frappe.get_doc('Payment Request', payment_request_name) status = pay_req_doc.status - + if status != "Paid" and not ref_details.outstanding_amount: status = 'Paid' elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index e2c6f1d8565..17ef44954b1 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -112,6 +112,8 @@ frappe.ui.form.on("Fees", { args: { "dt": frm.doc.doctype, "dn": frm.doc.name, + "party_type": "Student", + "party": frm.doc.student, "recipient_id": frm.doc.student_email }, callback: function(r) { diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index aa616e6206a..f31003bf326 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -75,7 +75,8 @@ class Fees(AccountsController): self.make_gl_entries() if self.send_payment_request and self.student_email: - pr = make_payment_request(dt="Fees", dn=self.name, recipient_id=self.student_email, + pr = make_payment_request(party_type="Student", party=self.student, dt="Fees", + dn=self.name, recipient_id=self.student_email, submit_doc=True, use_dummy_message=True) frappe.msgprint(_("Payment request {0} created").format(getlink("Payment Request", pr.name))) From e33c44ae33b617e73e605c9d06c3803dee6c40f0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 29 Apr 2020 12:13:26 +0530 Subject: [PATCH 080/178] fix: Permission issue Employee Tax exemption (#21492) --- erpnext/hr/doctype/salary_structure/salary_structure.py | 4 ++-- erpnext/regional/india/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index df76458fe02..5ba7f1c4327 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -149,7 +149,7 @@ def get_existing_assignments(employees, salary_structure, from_date): return salary_structures_assignments @frappe.whitelist() -def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0): +def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0, ignore_permissions=False): def postprocess(source, target): if employee: employee_details = frappe.db.get_value("Employee", employee, @@ -169,7 +169,7 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = "name": "salary_structure" } } - }, target_doc, postprocess, ignore_child_tables=True) + }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) if cint(as_print): doc.name = 'Preview for {0}'.format(employee) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e0996c1800d..e518c14a8a0 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -273,7 +273,7 @@ def calculate_annual_eligible_hra_exemption(doc): }) def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): - salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1) + salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: From c099a410c88ae7fbef0584448158dfd560fc00d3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Apr 2020 12:05:56 +0530 Subject: [PATCH 081/178] fix: Group by filter fix in item wise sales and purchase register --- .../item_wise_purchase_register/item_wise_purchase_register.py | 2 +- .../report/item_wise_sales_register/item_wise_sales_register.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 127f3133f5b..1f78c7a006f 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -102,7 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data.append(row) - if filters.get('group_by'): + if filters.get('group_by') and item_list: total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) data.append(total_row) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 0c8957ae441..92a22e62f14 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -111,7 +111,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data.append(row) - if filters.get('group_by'): + if filters.get('group_by') and item_list: total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) data.append(total_row) From bd969477b1752a95943dae54cf09f706024390bd Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 29 Apr 2020 13:09:16 +0530 Subject: [PATCH 082/178] fix: AttributeError --- erpnext/crm/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 72d778e9568..38bf79e5fcb 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -9,12 +9,12 @@ def update_lead_phone_numbers(contact, method): if len(contact.phone_nos) > 1: # get the default phone number - primary_phones = [phone.phone for phone in contact.phone_nos if phone.is_primary_phone] + primary_phones = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_phone] if primary_phones: phone = primary_phones[0] # get the default mobile number - primary_mobile_nos = [phone.phone for phone in contact.phone_nos if phone.is_primary_mobile_no] + primary_mobile_nos = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_mobile_no] if primary_mobile_nos: mobile_no = primary_mobile_nos[0] From fd1ab37bc86f1a10b83385f1a2ccd8806a7a4414 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 29 Apr 2020 15:19:08 +0530 Subject: [PATCH 083/178] chore: validate and warn payment against paid invoices --- .../doctype/payment_entry/payment_entry.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c89742cfb7f..6130a1401fc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -60,6 +60,7 @@ class PaymentEntry(AccountsController): self.set_remarks() self.validate_duplicate_entry() self.validate_allocated_amount() + self.validate_paid_invoices() self.ensure_supplier_is_not_blocked() self.set_status() @@ -264,6 +265,25 @@ class PaymentEntry(AccountsController): frappe.throw(_("{0} {1} must be submitted") .format(d.reference_doctype, d.reference_name)) + def validate_paid_invoices(self): + no_oustanding_refs = {} + + for d in self.get("references"): + if not d.allocated_amount: + continue + + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): + outstanding_amount = frappe.get_cached_value(d.reference_doctype, d.reference_name, "outstanding_amount") + if outstanding_amount <= 0: + no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) + + for k, v in no_oustanding_refs.items(): + frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry. \n \ + If this is undesirable please cancel the corresponding Payment Entry.") + .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")), + title=_("Warning"), indicator="orange") + + def validate_journal_entry(self): for d in self.get("references"): if d.allocated_amount and d.reference_doctype == "Journal Entry": From 61584c86011ca87fa05797caa8203d599da9d402 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 10:58:01 +0530 Subject: [PATCH 084/178] fix: print heading field shown in gst section for india region (#21500) Co-authored-by: Nabin Hait --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index df142a83ddc..ea78c27333e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -149,9 +149,9 @@ "edit_printing_settings", "letter_head", "group_same_items", - "language", - "column_break_84", "select_print_heading", + "column_break_84", + "language", "more_information", "inter_company_invoice_reference", "customer_group", @@ -1570,7 +1570,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-04-17 12:38:41.435728", + "modified": "2020-04-29 13:37:09.355300", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From ae5414fced02861c68ba4724b8fbb4a5b50db9c1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 30 Apr 2020 11:02:45 +0530 Subject: [PATCH 085/178] fix: Consider any kind of exemptions only if tax exemptions are allowed on tax slab (#21475) --- .../income_tax_slab/income_tax_slab.json | 21 ++----- .../salary_component/salary_component.json | 4 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 62 ++++++++++--------- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json index 6d89b197d27..bc6bc5ed370 100644 --- a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -93,13 +93,15 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-24 12:28:36.805904", + "modified": "2020-04-27 20:10:42.755762", "modified_by": "Administrator", "module": "HR", "name": "Income Tax Slab", "owner": "Administrator", "permissions": [ { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -109,9 +111,11 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 }, { + "amend": 1, "cancel": 1, "create": 1, "delete": 1, @@ -126,6 +130,7 @@ "write": 1 }, { + "amend": 1, "cancel": 1, "create": 1, "delete": 1, @@ -138,20 +143,6 @@ "share": 1, "submit": 1, "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "share": 1, - "submit": 1, - "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json index 5487e1dee85..97c46c829e7 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/hr/doctype/salary_component/salary_component.json @@ -227,7 +227,7 @@ { "default": "0", "depends_on": "eval:doc.type == \"Deduction\" && !doc.variable_based_on_taxable_salary", - "description": "If checked, the full amount will be deducted from taxable income before calculating income tax. Otherwise, it can be exempted via Employee Tax Exemption Declaration.", + "description": "If checked, the full amount will be deducted from taxable income before calculating income tax without any declaration or proof submission.", "fieldname": "exempted_from_income_tax", "fieldtype": "Check", "label": "Exempted from Income Tax" @@ -235,7 +235,7 @@ ], "icon": "fa fa-flag", "links": [], - "modified": "2020-04-24 14:50:28.994054", + "modified": "2020-04-28 15:46:45.252945", "modified_by": "Administrator", "module": "HR", "name": "Salary Component", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index dda15014166..cfd228fc3d3 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -489,15 +489,16 @@ class SalarySlip(TransactionBase): remaining_sub_periods = get_period_factor(self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] # get taxable_earnings, paid_taxes for previous period - previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date) + previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, + self.start_date, tax_slab.allow_tax_exemption) previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) # get taxable_earnings for current period (all days) - current_taxable_earnings = self.get_taxable_earnings() + current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption) future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1) # get taxable_earnings, addition_earnings for current actual payment days - current_taxable_earnings_for_payment_days = self.get_taxable_earnings(based_on_payment_days=1) + current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1) current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax @@ -556,7 +557,7 @@ class SalarySlip(TransactionBase): return income_tax_slab_doc - def get_taxable_earnings_for_prev_period(self, start_date, end_date): + def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_exemption=False): taxable_earnings = frappe.db.sql(""" select sum(sd.amount) from @@ -576,24 +577,26 @@ class SalarySlip(TransactionBase): }) taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 - exempted_amount = frappe.db.sql(""" - select sum(sd.amount) - from - `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name - where - sd.parentfield='deductions' - and sd.exempted_from_income_tax=1 - and is_flexible_benefit=0 - and ss.docstatus=1 - and ss.employee=%(employee)s - and ss.start_date between %(from_date)s and %(to_date)s - and ss.end_date between %(from_date)s and %(to_date)s - """, { - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - }) - exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 + exempted_amount = 0 + if allow_tax_exemption: + exempted_amount = frappe.db.sql(""" + select sum(sd.amount) + from + `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name + where + sd.parentfield='deductions' + and sd.exempted_from_income_tax=1 + and is_flexible_benefit=0 + and ss.docstatus=1 + and ss.employee=%(employee)s + and ss.start_date between %(from_date)s and %(to_date)s + and ss.end_date between %(from_date)s and %(to_date)s + """, { + "employee": self.employee, + "from_date": start_date, + "to_date": end_date + }) + exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 return taxable_earnings - exempted_amount @@ -621,7 +624,7 @@ class SalarySlip(TransactionBase): return total_tax_paid - def get_taxable_earnings(self, based_on_payment_days=0): + def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) @@ -655,12 +658,13 @@ class SalarySlip(TransactionBase): else: taxable_earnings += amount - for ded in self.deductions: - if ded.exempted_from_income_tax: - amount = ded.amount - if based_on_payment_days: - amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] - taxable_earnings -= flt(amount) + if allow_tax_exemption: + for ded in self.deductions: + if ded.exempted_from_income_tax: + amount = ded.amount + if based_on_payment_days: + amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] + taxable_earnings -= flt(amount) return frappe._dict({ "taxable_earnings": taxable_earnings, From 22c4f82fc63a6c87357ae0291cba18dd10a61f51 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 30 Apr 2020 11:03:01 +0530 Subject: [PATCH 086/178] fix: Tax calcualtion based on slab (#21497) --- erpnext/hr/doctype/salary_slip/salary_slip.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index cfd228fc3d3..452aa74281e 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -766,13 +766,13 @@ class SalarySlip(TransactionBase): for slab in tax_slab.slabs: if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): continue - if not slab.to_amount and annual_taxable_earning > slab.from_amount: - tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + if not slab.to_amount and annual_taxable_earning >= slab.from_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 continue - if annual_taxable_earning > slab.from_amount and annual_taxable_earning < slab.to_amount: - tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 - elif annual_taxable_earning > slab.from_amount and annual_taxable_earning > slab.to_amount: - tax_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 + if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 + elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: + tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * .01 # other taxes and charges on income tax for d in tax_slab.other_taxes_and_charges: From 2620ac31f9ef8f7636a2a7a907d4524b949ebe99 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 30 Apr 2020 11:03:57 +0530 Subject: [PATCH 087/178] fix: Desk links for Income Tax Slab and Employee Other Income (#21511) --- erpnext/config/hr.py | 8 ++++++++ erpnext/hr/doctype/income_tax_slab/income_tax_slab.json | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py index 4e5e9037b3a..1be05a346d0 100644 --- a/erpnext/config/hr.py +++ b/erpnext/config/hr.py @@ -170,6 +170,10 @@ def get_data(): "type": "doctype", "name": "Payroll Period", }, + { + "type": "doctype", + "name": "Income Tax Slab", + }, { "type": "doctype", "name": "Salary Component", @@ -209,6 +213,10 @@ def get_data(): "name": "Employee Tax Exemption Proof Submission", "dependencies": ["Employee"] }, + { + "type": "doctype", + "name": "Employee Other Income", + }, { "type": "doctype", "name": "Employee Benefit Application", diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json index bc6bc5ed370..f74315f32e9 100644 --- a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -80,6 +80,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "other_taxes_and_charges", "fieldname": "taxes_and_charges_on_income_tax_section", "fieldtype": "Section Break", "label": "Taxes and Charges on Income Tax" @@ -93,7 +94,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-27 20:10:42.755762", + "modified": "2020-04-29 15:08:21.436120", "modified_by": "Administrator", "module": "HR", "name": "Income Tax Slab", From 6397590f0755cea1881b2d562ecc8757cf011961 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 11:04:51 +0530 Subject: [PATCH 088/178] fix: list index out of range error (#21468) * fix: list index out of range error * fix: condition --- .../gross_and_net_profit_report.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 260f35f270d..8ce745e4d6b 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -35,6 +35,13 @@ def execute(filters=None): }) return columns, data + + # to avoid error eg: gross_income[0] : list index out of range + if not gross_income: + gross_income = [{}] + if not gross_expense: + gross_expense = [{}] + data.append({ "account_name": "'" + _("Included in Gross Profit") + "'", "account": "'" + _("Included in Gross Profit") + "'" From 0ffff47b64c35b0d493ad6a7d7144ee0dfbcad52 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 11:29:03 +0530 Subject: [PATCH 089/178] feat: force cost center renaming from cost center form (#21504) --- .../doctype/cost_center/cost_center.js | 30 ++++++++---- .../doctype/cost_center/cost_center.json | 3 +- erpnext/accounts/utils.py | 47 +++++++------------ 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 96ec57dcb0b..9e2f6eed3b6 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -18,7 +18,7 @@ frappe.ui.form.on('Cost Center', { }, refresh: function(frm) { if (!frm.is_new()) { - frm.add_custom_button(__('Update Cost Center Number'), function () { + frm.add_custom_button(__('Update Cost Center Name / Number'), function () { frm.trigger("update_cost_center_number"); }); } @@ -47,35 +47,45 @@ frappe.ui.form.on('Cost Center', { }, update_cost_center_number: function(frm) { var d = new frappe.ui.Dialog({ - title: __('Update Cost Center Number'), + title: __('Update Cost Center Name / Number'), fields: [ { - "label": 'Cost Center Number', + "label": "Cost Center Name", + "fieldname": "cost_center_name", + "fieldtype": "Data", + "reqd": 1, + "default": frm.doc.cost_center_name + }, + { + "label": "Cost Center Number", "fieldname": "cost_center_number", "fieldtype": "Data", - "reqd": 1 + "reqd": 1, + "default": frm.doc.cost_center_number } ], primary_action: function() { var data = d.get_values(); - if(data.cost_center_number === frm.doc.cost_center_number) { + if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) { d.hide(); return; } + frappe.dom.freeze(); frappe.call({ - method: "erpnext.accounts.utils.update_number_field", + method: "erpnext.accounts.utils.update_cost_center", args: { - doctype_name: frm.doc.doctype, - name: frm.doc.name, - field_name: d.fields[0].fieldname, - number_value: data.cost_center_number, + docname: frm.doc.name, + cost_center_name: data.cost_center_name, + cost_center_number: data.cost_center_number, company: frm.doc.company }, callback: function(r) { + frappe.dom.unfreeze(); if(!r.exc) { if(r.message) { frappe.set_route("Form", "Cost Center", r.message); } else { + me.frm.set_value("cost_center_name", data.cost_center_name); me.frm.set_value("cost_center_number", data.cost_center_number); } d.hide(); diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 126aa543185..5013c92a327 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -2,7 +2,6 @@ "actions": [], "allow_copy": 1, "allow_import": 1, - "allow_rename": 1, "creation": "2013-01-23 19:57:17", "description": "Track separate Income and Expense for product verticals or divisions.", "doctype": "DocType", @@ -126,7 +125,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:26:01.540170", + "modified": "2020-04-29 16:09:30.025214", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5ad8cb50d5f..d1aa4011b63 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -817,48 +817,37 @@ def create_payment_gateway_account(gateway): pass @frappe.whitelist() -def update_number_field(doctype_name, name, field_name, number_value, company): +def update_cost_center(docname, cost_center_name, cost_center_number, company): ''' - doctype_name = Name of the DocType - name = Docname being referred - field_name = Name of the field thats holding the 'number' attribute - number_value = Numeric value entered in field_name - - Stores the number entered in the dialog to the DocType's field. - Renames the document by adding the number as a prefix to the current name and updates all transaction where it was present. ''' - doc_title = frappe.db.get_value(doctype_name, name, frappe.scrub(doctype_name)+"_name") + validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number") - validate_field_number(doctype_name, name, number_value, company, field_name) + if cost_center_number: + frappe.db.set_value("Cost Center", docname, "cost_center_number", cost_center_number.strip()) + else: + frappe.db.set_value("Cost Center", docname, "cost_center_number", "") - frappe.db.set_value(doctype_name, name, field_name, number_value) + frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip()) - if doc_title[0].isdigit(): - separator = " - " if " - " in doc_title else " " - doc_title = doc_title.split(separator, 1)[1] - - frappe.db.set_value(doctype_name, name, frappe.scrub(doctype_name)+"_name", doc_title) - - new_name = get_autoname_with_number(number_value, doc_title, name, company) - - if name != new_name: - frappe.rename_doc(doctype_name, name, new_name) + new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company) + if docname != new_name: + frappe.rename_doc("Cost Center", docname, new_name, force=1) return new_name -def validate_field_number(doctype_name, name, number_value, company, field_name): +def validate_field_number(doctype_name, docname, number_value, company, field_name): ''' Validate if the number entered isn't already assigned to some other document. ''' if number_value: + filters = {field_name: number_value, "name": ["!=", docname]} if company: - doctype_with_same_number = frappe.db.get_value(doctype_name, - {field_name: number_value, "company": company, "name": ["!=", name]}) - else: - doctype_with_same_number = frappe.db.get_value(doctype_name, - {field_name: number_value, "name": ["!=", name]}) + filters["company"] = company + + doctype_with_same_number = frappe.db.get_value(doctype_name, filters) + if doctype_with_same_number: - frappe.throw(_("{0} Number {1} already used in account {2}") - .format(doctype_name, number_value, doctype_with_same_number)) + frappe.throw(_("{0} Number {1} is already used in {2} {3}") + .format(doctype_name, number_value, doctype_name.lower(), doctype_with_same_number)) def get_autoname_with_number(number_value, doc_title, name, company): ''' append title with prefix as number and suffix as company's abbreviation separated by '-' ''' From 393857d7dec2917092348f9b72a404b5b2218bfe Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 30 Apr 2020 13:23:59 +0530 Subject: [PATCH 090/178] chore: handle credit note validation --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 6130a1401fc..56be17d4405 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -273,12 +273,12 @@ class PaymentEntry(AccountsController): continue if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): - outstanding_amount = frappe.get_cached_value(d.reference_doctype, d.reference_name, "outstanding_amount") - if outstanding_amount <= 0: + outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]) + if outstanding_amount <= 0 and not is_return: no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) for k, v in no_oustanding_refs.items(): - frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry. \n \ + frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.

\ If this is undesirable please cancel the corresponding Payment Entry.") .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")), title=_("Warning"), indicator="orange") From 5e817b2aeed8ab2c8e60d267b82f00a95c675f0e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 30 Apr 2020 13:56:49 +0530 Subject: [PATCH 091/178] chore: Added company filter and minor cleanup in Stock Balance Report --- .../report/stock_balance/stock_balance.js | 29 +++++++++++---- .../report/stock_balance/stock_balance.json | 36 ++++++++++--------- .../report/stock_balance/stock_balance.py | 7 ++-- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 537fa7c04b8..d77dd877c4a 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -19,6 +19,14 @@ frappe.query_reports["Stock Balance"] = { "reqd": 1, "default": frappe.datetime.get_today() }, + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "width": "80", + "options": "Company", + "default": frappe.defaults.get_default("company") + }, { "fieldname": "item_group", "label": __("Item Group"), @@ -26,12 +34,6 @@ frappe.query_reports["Stock Balance"] = { "width": "80", "options": "Item Group" }, - { - "fieldname":"brand", - "label": __("Brand"), - "fieldtype": "Link", - "options": "Brand" - }, { "fieldname": "item_code", "label": __("Item"), @@ -84,5 +86,18 @@ frappe.query_reports["Stock Balance"] = { "label": __('Show Stock Ageing Data'), "fieldtype": 'Check' }, - ] + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "out_qty" && data && data.out_qty > 0) { + value = "" + value + ""; + } + else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { + value = "" + value + ""; + } + + return value; + } }; diff --git a/erpnext/stock/report/stock_balance/stock_balance.json b/erpnext/stock/report/stock_balance/stock_balance.json index 2f20b202350..8c45f0c2f47 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.json +++ b/erpnext/stock/report/stock_balance/stock_balance.json @@ -1,24 +1,26 @@ { - "add_total_row": 1, - "creation": "2014-10-10 17:58:11.577901", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2018-08-14 15:24:41.395557", - "modified_by": "Administrator", - "module": "Stock", - "name": "Stock Balance", - "owner": "Administrator", - "prepared_report": 1, - "ref_doctype": "Stock Ledger Entry", - "report_name": "Stock Balance", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2014-10-10 17:58:11.577901", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 2, + "is_standard": "Yes", + "modified": "2020-04-30 13:46:14.680354", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Balance", + "owner": "Administrator", + "prepared_report": 1, + "query": "", + "ref_doctype": "Stock Ledger Entry", + "report_name": "Stock Balance", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Accounts Manager" } diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index ab87ee114d4..74a4f6ef142 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -94,8 +94,6 @@ def get_columns(filters): {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 90}, - {"label": _("Description"), "fieldname": "description", "width": 140}, {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100}, {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, {"label": _("Balance Qty"), "fieldname": "bal_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, @@ -132,6 +130,9 @@ def get_conditions(filters): else: frappe.throw(_("'To Date' is required")) + if filters.get("company"): + conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company")) + if filters.get("warehouse"): warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) @@ -233,8 +234,6 @@ def get_items(filters): if filters.get("item_code"): conditions.append("item.name=%(item_code)s") else: - if filters.get("brand"): - conditions.append("item.brand=%(brand)s") if filters.get("item_group"): conditions.append(get_item_group_condition(filters.get("item_group"))) From c31fc19f08af7c6f5e8b7ebcd1f67aedde7cadb0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 30 Apr 2020 16:28:52 +0530 Subject: [PATCH 092/178] chore: add total row in sales analytics report --- .../sales_analytics/sales_analytics.json | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json index 71932610a6a..cbc59992483 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.json +++ b/erpnext/selling/report/sales_analytics/sales_analytics.json @@ -1,31 +1,31 @@ { - "add_total_row": 0, - "creation": "2018-09-21 12:46:29.451048", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-05-24 05:37:02.866139", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Analytics", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Sales Order", - "report_name": "Sales Analytics", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2018-09-21 12:46:29.451048", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-04-30 16:27:53.112907", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Sales Analytics", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Maintenance User" - }, + }, { "role": "Accounts User" - }, + }, { "role": "Sales Manager" } From a8b87ccce0b055e9a36f5270d40dbbc36beb50cf Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 18:39:16 +0530 Subject: [PATCH 093/178] chore: asset accounts should have company currency (#21525) --- .../doctype/asset_category/asset_category.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 9bf4df423c2..9a33fc14ac0 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -11,7 +11,8 @@ from frappe.model.document import Document class AssetCategory(Document): def validate(self): self.validate_finance_books() - self.validate_accounts() + self.validate_account_types() + self.validate_account_currency() def validate_finance_books(self): for d in self.finance_books: @@ -19,7 +20,26 @@ class AssetCategory(Document): if cint(d.get(frappe.scrub(field)))<1: frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) - def validate_accounts(self): + def validate_account_currency(self): + account_types = [ + 'fixed_asset_account', 'accumulated_depreciation_account', 'depreciation_expense_account', 'capital_work_in_progress_account' + ] + invalid_accounts = [] + for d in self.accounts: + company_currency = frappe.get_value('Company', d.get('company_name'), 'default_currency') + for type_of_account in account_types: + if d.get(type_of_account): + account_currency = frappe.get_value("Account", d.get(type_of_account), "account_currency") + if account_currency != company_currency: + invalid_accounts.append(frappe._dict({ 'type': type_of_account, 'idx': d.idx, 'account': d.get(type_of_account) })) + + for d in invalid_accounts: + frappe.throw(_("Row #{}: Currency of {} - {} doesn't matches company currency.") + .format(d.idx, frappe.bold(frappe.unscrub(d.type)), frappe.bold(d.account)), + title=_("Invalid Account")) + + + def validate_account_types(self): account_type_map = { 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, From 426f0bc168222ff9bc87d35fe4b077207d02034f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2020 19:53:59 +0530 Subject: [PATCH 094/178] fix: only check for payment_account on bank entry (#21445) (#21518) * fix: only check for payment_account on bank entry Since all the fields (company, start and end date are mandatory before form submission, there is no need to check for them again after submission. * fix: cur_frm to frm Co-authored-by: Nabin Hait (cherry picked from commit 95b186268cbe8595baf7df36294cb35e8092a4bc) Co-authored-by: Michelle Alva <50285544+michellealva@users.noreply.github.com> --- erpnext/hr/doctype/payroll_entry/payroll_entry.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.js b/erpnext/hr/doctype/payroll_entry/payroll_entry.js index d25eb6d7810..da25d7574e0 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.js @@ -249,7 +249,7 @@ const submit_salary_slip = function (frm) { let make_bank_entry = function (frm) { var doc = frm.doc; - if (doc.company && doc.start_date && doc.end_date && doc.payment_account) { + if (doc.payment_account) { return frappe.call({ doc: cur_frm.doc, method: "make_payment_entry", @@ -262,7 +262,8 @@ let make_bank_entry = function (frm) { freeze_message: __("Creating Payment Entries......") }); } else { - frappe.msgprint(__("Company, Payment Account, From Date and To Date is mandatory")); + frappe.msgprint(__("Payment Account is mandatory")); + frm.scroll_to_field('payment_account'); } }; From 4451b7eda93a9e8ed8362587c07ecaa57f1d72b9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 30 Apr 2020 20:06:30 +0530 Subject: [PATCH 095/178] chore: calculate total row month-wise in sales analytics --- .../report/sales_analytics/sales_analytics.json | 4 ++-- .../report/sales_analytics/sales_analytics.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json index cbc59992483..bf9edd6cd49 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.json +++ b/erpnext/selling/report/sales_analytics/sales_analytics.json @@ -1,5 +1,5 @@ { - "add_total_row": 1, + "add_total_row": 0, "creation": "2018-09-21 12:46:29.451048", "disable_prepared_report": 0, "disabled": 0, @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2020-04-30 16:27:53.112907", + "modified": "2020-04-30 19:49:02.303320", "modified_by": "Administrator", "module": "Selling", "name": "Sales Analytics", diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index f1726ab8bf7..3fc4633ae3d 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -194,6 +194,9 @@ class Analytics(object): def get_rows(self): self.data = [] self.get_periodic_data() + total_row = { + "entity": "Total", + } for entity, period_data in iteritems(self.entity_periodic_data): row = { @@ -207,6 +210,9 @@ class Analytics(object): row[scrub(period)] = amount total += amount + if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 + total_row[scrub(period)] += amount + row["total"] = total if self.filters.tree_type == "Item": @@ -214,9 +220,14 @@ class Analytics(object): self.data.append(row) + self.data.append(total_row) + def get_rows_by_group(self): self.get_periodic_data() out = [] + total_row = { + "entity": "Total", + } for d in reversed(self.group_entries): row = { @@ -232,8 +243,14 @@ class Analytics(object): self.entity_periodic_data.setdefault(d.parent, frappe._dict()).setdefault(period, 0.0) self.entity_periodic_data[d.parent][period] += amount total += amount + + if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 + total_row[scrub(period)] += amount + row["total"] = total out = [row] + out + + out.append(total_row) self.data = out def get_periodic_data(self): From f77a73546908d531d191e56621bccb30e207dd1e Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 1 May 2020 00:01:37 +0530 Subject: [PATCH 096/178] fix: Make Company the first filter --- .../stock/report/stock_balance/stock_balance.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index d77dd877c4a..7d22823eb80 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -3,6 +3,14 @@ frappe.query_reports["Stock Balance"] = { "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "width": "80", + "options": "Company", + "default": frappe.defaults.get_default("company") + }, { "fieldname":"from_date", "label": __("From Date"), @@ -19,14 +27,6 @@ frappe.query_reports["Stock Balance"] = { "reqd": 1, "default": frappe.datetime.get_today() }, - { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "width": "80", - "options": "Company", - "default": frappe.defaults.get_default("company") - }, { "fieldname": "item_group", "label": __("Item Group"), From 438e0f5d4986660bc50663298c0bd8acbfd2b7cb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 1 May 2020 10:50:02 +0530 Subject: [PATCH 097/178] fix: 'NoneType' object is not iterable (#21538) --- erpnext/stock/get_item_details.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 51f27fd3e78..f43592bef63 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -340,8 +340,14 @@ def get_basic_details(args, item, overwrite_warehouse=True): out["manufacturer_part_no"] = None out["manufacturer"] = None else: - out["manufacturer"], out["manufacturer_part_no"] = frappe.get_value("Item", item.name, - ["default_item_manufacturer", "default_manufacturer_part_no"] ) + data = frappe.get_value("Item", item.name, + ["default_item_manufacturer", "default_manufacturer_part_no"] , as_dict=1) + + if data: + out.update({ + "manufacturer": data.default_item_manufacturer, + "manufacturer_part_no": data.default_manufacturer_part_no + }) child_doctype = args.doctype + ' Item' meta = frappe.get_meta(child_doctype) From db6953dc78b8b31ce5217a241772f92684861115 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 1 May 2020 15:01:04 +0530 Subject: [PATCH 098/178] fix: Party Type filter in payment entry list view (#21542) --- .../doctype/payment_entry/payment_entry_list.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 erpnext/accounts/doctype/payment_entry/payment_entry_list.js diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js new file mode 100644 index 00000000000..7ea60bb48ed --- /dev/null +++ b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings['Payment Entry'] = { + + onload: function(listview) { + listview.page.fields_dict.party_type.get_query = function() { + return { + "filters": { + "name": ["in", Object.keys(frappe.boot.party_account_types)], + } + }; + }; + } +}; \ No newline at end of file From bf1fc47564af4cc5e0d90a933755c9df9ae6a123 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 1 May 2020 18:15:20 +0530 Subject: [PATCH 099/178] fix: accounts payable shows advance amount of other company (#21549) --- erpnext/accounts/party.py | 4 +++- .../accounts_receivable_summary.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 3d9ff273ce2..c8b5240812c 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -603,10 +603,12 @@ def get_party_shipping_address(doctype, name): else: return '' -def get_partywise_advanced_payment_amount(party_type, posting_date = None): +def get_partywise_advanced_payment_amount(party_type, posting_date = None, company=None): cond = "1=1" if posting_date: cond = "posting_date <= '{0}'".format(posting_date) + if company: + cond += "and company = '{0}'".format(company) data = frappe.db.sql(""" SELECT party, sum({0}) as amount FROM `tabGL Entry` diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index b607c0f7028..aa6b42e89d0 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -33,7 +33,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.get_party_total(args) party_advance_amount = get_partywise_advanced_payment_amount(self.party_type, - self.filters.report_date) or {} + self.filters.report_date, self.filters.company) or {} for party, party_dict in iteritems(self.party_total): if party_dict.outstanding == 0: From ed709b36b44c8b1e186c15714422bd974fd62325 Mon Sep 17 00:00:00 2001 From: Marica Date: Sat, 2 May 2020 17:55:47 +0530 Subject: [PATCH 100/178] fix: variable referenced before assignment (#21561) --- erpnext/controllers/buying_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index ff07b59bd9b..70f9033f431 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -100,7 +100,7 @@ class BuyingController(StockController): for d in tax_for_valuation: d.category = 'Total' msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) - + def validate_asset_return(self): if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return: return @@ -663,10 +663,10 @@ class BuyingController(StockController): for qty in range(cint(d.qty)): asset = self.make_asset(d) created_assets.append(asset) - + if len(created_assets) > 5: # dont show asset form links if more than 5 assets are created - messages.append(_('{} Asset{} created for {}').format(len(created_assets), is_plural, frappe.bold(d.item_code))) + messages.append(_('{} Assets created for {}').format(len(created_assets), frappe.bold(d.item_code))) else: assets_link = list(map(lambda d: frappe.utils.get_link_to_form('Asset', d), created_assets)) assets_link = frappe.bold(','.join(assets_link)) From 55b7904e2f3158e6675f21e0f164c2b759c23737 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 2 May 2020 19:39:03 +0530 Subject: [PATCH 101/178] fix: incorrect total in sales analytics for customer/item group --- erpnext/selling/report/sales_analytics/sales_analytics.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 3fc4633ae3d..c0c11339ca8 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -225,9 +225,6 @@ class Analytics(object): def get_rows_by_group(self): self.get_periodic_data() out = [] - total_row = { - "entity": "Total", - } for d in reversed(self.group_entries): row = { @@ -245,12 +242,10 @@ class Analytics(object): total += amount if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 - total_row[scrub(period)] += amount row["total"] = total out = [row] + out - out.append(total_row) self.data = out def get_periodic_data(self): From 272d2bc0b3e9f8e2bf7a9f2f86ac005782b2b25b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 2 May 2020 19:50:45 +0530 Subject: [PATCH 102/178] fix: review fixes --- erpnext/selling/report/sales_analytics/sales_analytics.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index c0c11339ca8..97d9322918d 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -241,8 +241,6 @@ class Analytics(object): self.entity_periodic_data[d.parent][period] += amount total += amount - if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 - row["total"] = total out = [row] + out From 12b5d72e70690c942e22819bc08eeba5270e299c Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 3 May 2020 13:20:30 +0530 Subject: [PATCH 103/178] chore: add validation for gross purchase amount (#21535) * chore: add validation for gross purchase amount * fix: tests --- erpnext/assets/doctype/asset/asset.py | 7 +++++++ erpnext/assets/doctype/asset/test_asset.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 0df6a6c7de0..7c147545372 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -22,6 +22,7 @@ class Asset(AccountsController): self.validate_item() self.set_missing_values() self.prepare_depreciation_data() + self.validate_gross_and_purchase_amount() if self.get("schedules"): self.validate_expected_value_after_useful_life() @@ -123,6 +124,12 @@ class Asset(AccountsController): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) + + def validate_gross_and_purchase_amount(self): + if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount: + frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\ + Please do not book expense of multiple assets against one single Asset.") + .format(frappe.bold("equal"), "
"), title=_("Invalid Gross Purchase Amount")) def cancel_auto_gen_movement(self): movements = frappe.db.sql( diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a56440de3d3..b767393532f 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -82,7 +82,6 @@ class TestAsset(unittest.TestCase): doc.set_missing_values() self.assertEquals(doc.items[0].is_fixed_asset, 1) - def test_schedule_for_straight_line_method(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -599,6 +598,7 @@ def create_asset(**args): "purchase_date": "2015-01-01", "calculate_depreciation": 0, "gross_purchase_amount": 100000, + "purchase_receipt_amount": 100000, "expected_value_after_useful_life": 10000, "warehouse": args.warehouse or "_Test Warehouse - _TC", "available_for_use_date": "2020-06-06", From 0da919c091d1a03943b59bbe9c1fa7e65eee3871 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 3 May 2020 14:53:13 +0530 Subject: [PATCH 104/178] fix: test --- .../report/sales_analytics/test_analytics.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 4d81a1e4dda..0a5198ccc53 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -83,6 +83,22 @@ class TestAnalytics(unittest.TestCase): "feb_2018": 0.0, "mar_2018": 0.0, "total": 3000.0 + }, + { + "entity": "Total", + "entity_name": "", + "apr_2017": 0.0, + "may_2017": 0.0, + "jun_2017": 2000.0, + "jul_2017": 1000.0, + "aug_2017": 0.0, + "sep_2017": 1500.0, + "oct_2017": 1000.0, + "nov_2017": 0.0, + "dec_2017": 0.0, + "jan_2018": 0.0, + "feb_2018": 0.0, + "mar_2018": 0.0 } ] result = sorted(report[1], key=lambda k: k['entity']) From bdbfd2ad0ca5e91829a2705b4673fcbf37b5804e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 May 2020 11:07:10 +0530 Subject: [PATCH 105/178] fix: handle exception if sending Appointment Confirmation message fails (#21568) --- .../patient_appointment/patient_appointment.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 4336fe51606..9bdba032e4a 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -69,12 +69,13 @@ class PatientAppointment(Document): if fee_validity.ref_invoice: frappe.db.set_value("Patient Appointment", appointment.name, "invoiced", True) frappe.msgprint(_("{0} has fee validity till {1}").format(appointment.patient, fee_validity.valid_till)) - confirm_sms(self) if frappe.db.get_value("Healthcare Settings", None, "manage_appointment_invoice_automatically") == '1' and \ frappe.db.get_value("Patient Appointment", self.name, "invoiced") != 1: invoice_appointment(self) + send_confirmation_msg(self) + @frappe.whitelist() def invoice_appointment(appointment_doc): if not appointment_doc.name: @@ -303,10 +304,14 @@ def set_pending_appointments(): "('Scheduled','Open') and appointment_date < %s", today) -def confirm_sms(doc): - if frappe.db.get_value("Healthcare Settings", None, "app_con") == '1': - message = frappe.db.get_value("Healthcare Settings", None, "app_con_msg") - send_message(doc, message) +def send_confirmation_msg(doc): + if frappe.db.get_single_value("Healthcare Settings", "app_con"): + message = frappe.db.get_single_value("Healthcare Settings", "app_con_msg") + try: + send_message(doc, message) + except Exception: + frappe.log_error(frappe.get_traceback(), _("Appointment Confirmation Message Not Sent")) + frappe.msgprint(_("Appointment Confirmation Message Not Sent"), indicator="orange") @frappe.whitelist() def create_encounter(appointment): From 4a37ee8908f13bda033248873617de76e74c0f0f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 May 2020 11:09:19 +0530 Subject: [PATCH 106/178] fix(Healthcare): remove hardcoded UOM during Item creation for templates (#21575) --- .../clinical_procedure_template.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py index 00fad910a4f..69237847986 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py @@ -78,6 +78,8 @@ def create_item_from_template(doc): if doc.is_billable: disabled = 0 + uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom') + #insert item item = frappe.get_doc({ 'doctype': 'Item', @@ -92,7 +94,7 @@ def create_item_from_template(doc): 'show_in_website': 0, 'is_pro_applicable': 0, 'disabled': disabled, - 'stock_uom': 'Unit' + 'stock_uom': uom }).insert(ignore_permissions=True) #insert item price From 3202b0a486d750412563916b40bf8f49c9682ee0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 4 May 2020 11:12:19 +0530 Subject: [PATCH 107/178] fix: Accounting Dimensions in Period Closing Voucher (#21565) --- .../accounting_dimension.py | 9 ++--- .../period_closing_voucher.py | 33 ++++++++++++++----- .../public/js/utils/dimension_tree_filter.js | 2 +- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 14fdffc0a78..7a85bfb26b6 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -206,12 +206,13 @@ def get_dimension_filters(): WHERE disabled = 0 """, as_dict=1) - default_dimensions = frappe.db.sql("""SELECT parent, company, default_dimension - FROM `tabAccounting Dimension Detail`""", as_dict=1) + default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension + FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p + WHERE c.parent = p.name""", as_dict=1) default_dimensions_map = {} for dimension in default_dimensions: - default_dimensions_map.setdefault(dimension['company'], {}) - default_dimensions_map[dimension['company']][dimension['parent']] = dimension['default_dimension'] + default_dimensions_map.setdefault(dimension.company, {}) + default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension return dimension_filters, default_dimensions_map diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index eb95e458dc8..b59a177f43b 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -7,6 +7,8 @@ from frappe.utils import flt from frappe import _ from erpnext.accounts.utils import get_account_currency from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions, + get_dimension_filters) class PeriodClosingVoucher(AccountsController): def validate(self): @@ -49,7 +51,15 @@ class PeriodClosingVoucher(AccountsController): def make_gl_entries(self): gl_entries = [] net_pl_balance = 0 - pl_accounts = self.get_pl_balances() + dimension_fields = ['t1.cost_center'] + + accounting_dimensions = get_accounting_dimensions() + for dimension in accounting_dimensions: + dimension_fields.append('t1.{0}'.format(dimension)) + + dimension_filters, default_dimensions = get_dimension_filters() + + pl_accounts = self.get_pl_balances(dimension_fields) for acc in pl_accounts: if flt(acc.balance_in_company_currency): @@ -65,34 +75,41 @@ class PeriodClosingVoucher(AccountsController): if flt(acc.balance_in_account_currency) > 0 else 0, "credit": abs(flt(acc.balance_in_company_currency)) \ if flt(acc.balance_in_company_currency) > 0 else 0 - })) + }, item=acc)) net_pl_balance += flt(acc.balance_in_company_currency) if net_pl_balance: cost_center = frappe.db.get_value("Company", self.company, "cost_center") - gl_entries.append(self.get_gl_dict({ + gl_entry = self.get_gl_dict({ "account": self.closing_account_head, "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0, "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0, "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0, "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0, "cost_center": cost_center - })) + }) + + for dimension in accounting_dimensions: + gl_entry.update({ + dimension: default_dimensions.get(self.company, {}).get(dimension) + }) + + gl_entries.append(gl_entry) from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries(gl_entries) - def get_pl_balances(self): + def get_pl_balances(self, dimension_fields): """Get balance for pl accounts""" return frappe.db.sql(""" select - t1.account, t1.cost_center, t2.account_currency, + t1.account, t2.account_currency, {dimension_fields}, sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency, sum(t1.debit) - sum(t1.credit) as balance_in_company_currency from `tabGL Entry` t1, `tabAccount` t2 where t1.account = t2.name and t2.report_type = 'Profit and Loss' and t2.docstatus < 2 and t2.company = %s and t1.posting_date between %s and %s - group by t1.account, t1.cost_center - """, (self.company, self.get("year_start_date"), self.posting_date), as_dict=1) + group by t1.account, {dimension_fields} + """.format(dimension_fields = ', '.join(dimension_fields)), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index b223fc557be..b6720c05cb2 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -46,7 +46,7 @@ doctypes_with_dimensions.forEach((doctype) => { if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0 && default_dimensions[frm.doc.company]) { - let default_dimension = default_dimensions[frm.doc.company][dimension['document_type']]; + let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']]; if(default_dimension) { if (frappe.meta.has_field(doctype, dimension['fieldname'])) { From c253f0621dd93ccb56cd3330f17db57b8eb4b38c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 May 2020 12:40:39 +0530 Subject: [PATCH 108/178] fix: fieldname update for 'Credit' and 'Debit' (#21405) (#21577) * fix: fieldname update for 'Credit' and 'Debit' 'credit' updated to 'credit_in_account_currency' 'debit' updated to 'debit_in_account_currency' * Update journal_entry.py Co-authored-by: Nabin Hait (cherry picked from commit f7a0b8b5b6c92826cef6108a6a25c81d0ac4ae34) Co-authored-by: Andy Zhu --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 1a530c75395..f367f952b8a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -561,20 +561,20 @@ class JournalEntry(AccountsController): if self.write_off_based_on == 'Accounts Receivable': jd1.party_type = "Customer" - jd1.credit = flt(d.outstanding_amount, self.precision("credit", "accounts")) + jd1.credit_in_account_currency = flt(d.outstanding_amount, self.precision("credit", "accounts")) jd1.reference_type = "Sales Invoice" jd1.reference_name = cstr(d.name) elif self.write_off_based_on == 'Accounts Payable': jd1.party_type = "Supplier" - jd1.debit = flt(d.outstanding_amount, self.precision("debit", "accounts")) + jd1.debit_in_account_currency = flt(d.outstanding_amount, self.precision("debit", "accounts")) jd1.reference_type = "Purchase Invoice" jd1.reference_name = cstr(d.name) jd2 = self.append('accounts', {}) if self.write_off_based_on == 'Accounts Receivable': - jd2.debit = total + jd2.debit_in_account_currency = total elif self.write_off_based_on == 'Accounts Payable': - jd2.credit = total + jd2.credit_in_account_currency = total self.validate_total_debit_and_credit() From c89d750e5cc94aa6c0a13b77ddccd202dc79c9a4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 4 May 2020 18:53:26 +0530 Subject: [PATCH 109/178] fix: heatmap not working for customer and supplier (#21579) --- erpnext/accounts/party.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c8b5240812c..b5dfbde5a04 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -163,7 +163,7 @@ def get_default_price_list(party): def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list price_list = get_permitted_documents('Price List') - + # if there is only one permitted document based on user permissions, set it if price_list and len(price_list) == 1: price_list = price_list[0] @@ -468,23 +468,25 @@ def get_timeline_data(doctype, name): from frappe.desk.form.load import get_communication_data out = {} - fields = 'date(creation), count(name)' + fields = 'creation, count(*)' after = add_years(None, -1).strftime('%Y-%m-%d') - group_by='group by date(creation)' + group_by='group by Date(creation)' - data = get_communication_data(doctype, name, after=after, group_by='group by date(creation)', - fields='date(C.creation) as creation, count(C.name)',as_dict=False) + data = get_communication_data(doctype, name, after=after, group_by='group by creation', + fields='C.creation as creation, count(C.name)',as_dict=False) # fetch and append data from Activity Log data += frappe.db.sql("""select {fields} from `tabActivity Log` - where (reference_doctype="{doctype}" and reference_name="{name}") - or (timeline_doctype in ("{doctype}") and timeline_name="{name}") - or (reference_doctype in ("Quotation", "Opportunity") and timeline_name="{name}") + where (reference_doctype=%(doctype)s and reference_name=%(name)s) + or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) + or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) and status!='Success' and creation > {after} {group_by} order by creation desc - """.format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields, - group_by=group_by, after=after), as_dict=False) + """.format(fields=fields, group_by=group_by, after=after), { + "doctype": doctype, + "name": name + }, as_dict=False) timeline_items = dict(data) From 67ed21d443fedab89d31e1f9d8774e53462b6c64 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Mon, 4 May 2020 19:23:26 +0530 Subject: [PATCH 110/178] fix(patch): Reload GSTR 3B report --- erpnext/patches/v11_0/add_permissions_in_gst_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index d7936110edb..83b2a4cc09e 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -7,4 +7,5 @@ def execute(): return frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") - add_permissions() \ No newline at end of file + frappe.reload_doc("regional", "doctype", "gstr_3b_report") + add_permissions() From c3a9513978fc0b5b5d9d152b708c951a6bb11f70 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 4 May 2020 20:24:00 +0530 Subject: [PATCH 111/178] fix: test --- .../report/sales_analytics/test_analytics.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 0a5198ccc53..7e8501dcc15 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -33,6 +33,21 @@ class TestAnalytics(unittest.TestCase): report = execute(filters) expected_data = [ + { + 'entity': 'Total', + 'apr_2017': 0.0, + 'may_2017': 0.0, + 'jun_2017': 2000.0, + 'jul_2017': 1000.0, + 'aug_2017': 0.0, + 'sep_2017': 1500.0, + 'oct_2017': 1000.0, + 'nov_2017': 0.0, + 'dec_2017': 0.0, + 'jan_2018': 0.0, + 'feb_2018': 2000.0, + 'mar_2018': 0.0 + }, { "entity": "_Test Customer 1", "entity_name": "_Test Customer 1", @@ -83,22 +98,6 @@ class TestAnalytics(unittest.TestCase): "feb_2018": 0.0, "mar_2018": 0.0, "total": 3000.0 - }, - { - "entity": "Total", - "entity_name": "", - "apr_2017": 0.0, - "may_2017": 0.0, - "jun_2017": 2000.0, - "jul_2017": 1000.0, - "aug_2017": 0.0, - "sep_2017": 1500.0, - "oct_2017": 1000.0, - "nov_2017": 0.0, - "dec_2017": 0.0, - "jan_2018": 0.0, - "feb_2018": 0.0, - "mar_2018": 0.0 } ] result = sorted(report[1], key=lambda k: k['entity']) @@ -150,6 +149,21 @@ class TestAnalytics(unittest.TestCase): report = execute(filters) expected_data = [ + { + 'entity': 'Total', + 'apr_2017': 0.0, + 'may_2017': 0.0, + 'jun_2017': 20.0, + 'jul_2017': 10.0, + 'aug_2017': 0.0, + 'sep_2017': 15.0, + 'oct_2017': 10.0, + 'nov_2017': 0.0, + 'dec_2017': 0.0, + 'jan_2018': 0.0, + 'feb_2018': 20.0, + 'mar_2018': 0.0 + }, { "entity": "_Test Customer 1", "entity_name": "_Test Customer 1", From 794bb6ebdd5bc24864eeef370fe9ec425a3025f3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 May 2020 16:12:10 +0530 Subject: [PATCH 112/178] fix: handle make_gl_entry in case of cwip enable after puchasing (#21530) * fix: handle make_gl_entry in case of cwip enable after puchasing * fix: invalid variable assignment * fix: make gl entries if cwip has been booked even if cwip is disabled * add tests * fix: conditions Co-authored-by: Nabin Hait --- erpnext/assets/doctype/asset/asset.py | 53 +++++++++++-- erpnext/assets/doctype/asset/test_asset.py | 75 +++++++++++++++++++ .../doctype/asset_movement/asset_movement.py | 1 + 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7c147545372..b8daf1fead9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -32,7 +32,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() - if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): + if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() def before_cancel(self): @@ -454,18 +454,55 @@ class Asset(AccountsController): for d in self.get('finance_books'): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 + + def validate_make_gl_entry(self): + purchase_document = self.get_purchase_document() + asset_bought_with_invoice = purchase_document == self.purchase_invoice + fixed_asset_account, cwip_account = self.get_asset_accounts() + cwip_enabled = is_cwip_accounting_enabled(self.asset_category) + # check if expense already has been booked in case of cwip was enabled after purchasing asset + expense_booked = False + cwip_booked = False + + if asset_bought_with_invoice: + expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", + (purchase_document, fixed_asset_account), as_dict=1) + else: + cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", + (purchase_document, cwip_account), as_dict=1) + + if cwip_enabled and (expense_booked or not cwip_booked): + # if expense has already booked from invoice or cwip is booked from receipt + return False + elif not cwip_enabled and (not expense_booked or cwip_booked): + # if cwip is disabled but expense hasn't been booked yet + return True + elif cwip_enabled: + # default condition + return True + + def get_purchase_document(self): + asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') + purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt + + return purchase_document + + def get_asset_accounts(self): + fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, + asset_category = self.asset_category, company = self.company) + + cwip_account = get_asset_account("capital_work_in_progress_account", + self.name, self.asset_category, self.company) + + return fixed_asset_account, cwip_account def make_gl_entries(self): gl_entries = [] - if ((self.purchase_receipt \ - or (self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock'))) - and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): - fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, - asset_category = self.asset_category, company = self.company) + purchase_document = self.get_purchase_document() + fixed_asset_account, cwip_account = self.get_asset_accounts() - cwip_account = get_asset_account("capital_work_in_progress_account", - self.name, self.asset_category, self.company) + if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): gl_entries.append(self.get_gl_dict({ "account": cwip_account, diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index b767393532f..e8def53c070 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -563,6 +563,81 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) + def test_gle_with_cwip_toggling(self): + # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=5000, do_not_submit=True, location="Test Location") + pr.set('taxes', [{ + 'category': 'Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Service Tax - _TC', + 'description': '_Test Account Service Tax', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }, { + 'category': 'Valuation and Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Shipping Charges - _TC', + 'description': '_Test Account Shipping Charges', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }]) + pr.submit() + expected_gle = ( + ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("CWIP Account - _TC", 5250.0, 0.0) + ) + pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Receipt' and voucher_no = %s + order by account""", pr.name) + self.assertEqual(pr_gle, expected_gle) + + pi = make_invoice(pr.name) + pi.submit() + expected_gle = ( + ("_Test Account Service Tax - _TC", 250.0, 0.0), + ("_Test Account Shipping Charges - _TC", 250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Creditors - _TC", 0.0, 5500.0), + ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), + ) + pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no = %s + order by account""", pi.name) + self.assertEqual(pi_gle, expected_gle) + + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + month_end_date = get_last_day(nowdate()) + asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) + asset_doc.append("finance_books", { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date + }) + + # disable cwip and try submitting + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset_doc.submit() + # asset should have gl entries even if cwip is disabled + expected_gle = ( + ("_Test Fixed Asset - _TC", 5250.0, 0.0), + ("CWIP Account - _TC", 0.0, 5250.0) + ) + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Asset' and voucher_no = %s + order by account""", asset_doc.name) + self.assertEqual(gle, expected_gle) + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 3a08baa714e..3da355e2b99 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -110,6 +110,7 @@ class AssetMovement(Document): ORDER BY asm.transaction_date asc """, (d.asset, self.company, 'Receipt'), as_dict=1) + if auto_gen_movement_entry and auto_gen_movement_entry[0].get('name') == self.name: frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \ auto generated for Asset {1}').format(self.name, d.asset)) From 10ea82001ff1ed23c26da7c0ed4a12c962f06106 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 May 2020 16:14:12 +0530 Subject: [PATCH 113/178] chore: fix error message (#21594) * chore: fix error message * chore: add row idx --- erpnext/controllers/selling_controller.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index de771a55f2c..c25ad060674 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -165,9 +165,9 @@ class SellingController(StockController): d.stock_qty = flt(d.qty) * flt(d.conversion_factor) def validate_selling_price(self): - def throw_message(item_name, rate, ref_rate_field): - frappe.throw(_("""Selling rate for item {0} is lower than its {1}. Selling rate should be atleast {2}""") - .format(item_name, ref_rate_field, rate)) + def throw_message(idx, item_name, rate, ref_rate_field): + frappe.throw(_("""Row #{}: Selling rate for item {} is lower than its {}. Selling rate should be atleast {}""") + .format(idx, item_name, ref_rate_field, rate)) if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): return @@ -182,7 +182,7 @@ class SellingController(StockController): last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1) if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom): - throw_message(it.item_name, last_purchase_rate_in_sales_uom, "last purchase rate") + throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") last_valuation_rate = frappe.db.sql(""" SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s @@ -192,7 +192,7 @@ class SellingController(StockController): if last_valuation_rate: last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1) if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom): - throw_message(it.name, last_valuation_rate_in_sales_uom, "valuation rate") + throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") def get_item_list(self): From d476eb79b4427f9e981953f3011c6ba8eacf1e1c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 5 May 2020 16:17:03 +0530 Subject: [PATCH 114/178] fix: against voucher no not all records showing in case of Group By Voucher (consolidated) (#21591) --- erpnext/accounts/report/general_ledger/general_ledger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index d3f3882299b..ae407231caa 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -293,6 +293,9 @@ def get_accountwise_gle(filters, gl_entries, gle_map): data[key].debit_in_account_currency += flt(gle.debit_in_account_currency) data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) + if data[key].against_voucher: + data[key].against_voucher += ', ' + gle.against_voucher + from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) for gle in gl_entries: if (gle.posting_date < from_date or From c6e0dbb1c4eb4251c65ec955fa3d7dc31d97c247 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 5 May 2020 16:26:05 +0530 Subject: [PATCH 115/178] Appending Email and Phone in Child Table --- erpnext/selling/doctype/customer/customer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 493d8730602..3da0eb86d1d 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -164,6 +164,8 @@ class Customer(TransactionBase): contact.phone = lead.phone contact.mobile_no = lead.mobile_no contact.is_primary_contact = 1 + contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) + contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.append('links', dict(link_doctype='Customer', link_name=self.name)) contact.flags.ignore_permissions = self.flags.ignore_permissions contact.autoname() From b4311a41c51efe23b64afd548fda7d68d3ac9acf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 May 2020 16:15:07 +0530 Subject: [PATCH 116/178] fix: work order operation completed qty --- erpnext/manufacturing/doctype/job_card/job_card.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 4c1ab7479fb..8fce3161375 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -107,30 +107,31 @@ class JobCard(Document): for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] - + field = "operation_id" if self.operation_id else "operation" data = frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], filters = {"docstatus": 1, "work_order": self.work_order, - "workstation": self.workstation, "operation": self.operation}) + "workstation": self.workstation, field: self.get(field)}) if data and len(data) > 0: for_quantity = data[0].completed_qty time_in_mins = data[0].time_in_mins - if for_quantity: + if self.get(field): time_data = frappe.db.sql(""" SELECT min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.workstation = %s and jc.operation = %s and jc.docstatus = 1 - """, (self.work_order, self.workstation, self.operation), as_dict=1) + and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1 + """.format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1) wo = frappe.get_doc('Work Order', self.work_order) + work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.workstation == self.workstation and data.operation == self.operation: + if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation: data.completed_qty = for_quantity data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None From 178ad9b4d614239d9880bf0b16291afa82aa5d58 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Tue, 5 May 2020 20:07:48 +0530 Subject: [PATCH 117/178] fix(patch): reload Expense Claim doctype --- .../patches/v12_0/set_correct_status_for_expense_claim.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py b/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py index 1fefc90680e..c3a87fc5ca7 100644 --- a/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py +++ b/erpnext/patches/v12_0/set_correct_status_for_expense_claim.py @@ -4,8 +4,11 @@ import frappe def execute(): + + frappe.reload_doc("hr", "doctype", "expense_claim") + frappe.db.sql(""" update `tabExpense Claim` set status = 'Paid' where total_advance_amount + total_amount_reimbursed = total_sanctioned_amount + total_taxes_and_charges - """) \ No newline at end of file + """) From ec3a462acdafca453c26d8f90d5c1839a863ba62 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 7 May 2020 12:06:05 +0530 Subject: [PATCH 118/178] fix: list index out of range (#21614) --- erpnext/stock/doctype/batch/batch.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 9b7249e66b9..a091ac7fae9 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.utils import flt, cint +from frappe.utils import flt, cint, get_link_to_form from frappe.utils.jinja import render_template from frappe.utils.data import add_days from six import string_types @@ -124,7 +124,7 @@ class Batch(Document): if has_expiry_date and not self.expiry_date: frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \ .format(frappe.bold("Shelf Life in Days"), - frappe.utils.get_link_to_form("Item", self.item), + get_link_to_form("Item", self.item), frappe.bold("Batch Expiry Date")), title=_("Expiry Date Mandatory")) @@ -264,16 +264,20 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos cond = '' - if serial_no: + if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'): + serial_nos = get_serial_nos(serial_no) batch = frappe.get_all("Serial No", fields = ["distinct batch_no"], filters= { "item_code": item_code, "warehouse": warehouse, - "name": ("in", get_serial_nos(serial_no)) + "name": ("in", serial_nos) } ) + if not batch: + validate_serial_no_with_batch(serial_nos, item_code) + if batch and len(batch) > 1: return [] @@ -288,4 +292,15 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format(cond), (item_code, warehouse), as_dict=True) \ No newline at end of file + """.format(cond), (item_code, warehouse), as_dict=True) + +def validate_serial_no_with_batch(serial_nos, item_code): + if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code: + frappe.throw(_("The serial no {0} does not belong to item {1}") + .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) + + serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos]) + + message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" + frappe.throw(_("There is no batch found against the {0}: {1}") + .format(message, serial_no_link)) \ No newline at end of file From 2aa045865fc85352d912d28c091ec46fb79454cd Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Thu, 7 May 2020 12:11:23 +0530 Subject: [PATCH 119/178] fix(item): patch to rename duplicate item_code values to name (#21619) --- erpnext/patches.txt | 1 + .../patches/v11_0/rename_duplicate_item_code_values.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 erpnext/patches/v11_0/rename_duplicate_item_code_values.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index dd3bc240917..580126d401e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -496,6 +496,7 @@ erpnext.patches.v10_0.rename_offer_letter_to_job_offer execute:frappe.delete_doc('DocType', 'Production Planning Tool', ignore_missing=True) erpnext.patches.v10_0.migrate_daily_work_summary_settings_to_daily_work_summary_group # 24-12-2018 erpnext.patches.v10_0.add_default_cash_flow_mappers +erpnext.patches.v11_0.rename_duplicate_item_code_values erpnext.patches.v11_0.make_quality_inspection_template erpnext.patches.v10_0.update_status_for_multiple_source_in_po erpnext.patches.v10_0.set_auto_created_serial_no_in_stock_entry diff --git a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py new file mode 100644 index 00000000000..00ab562c353 --- /dev/null +++ b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py @@ -0,0 +1,8 @@ +import frappe + +def execute(): + items = [] + items = frappe.db.sql("""select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True) + if items: + for item in items: + frappe.db.sql("""update `tabItem` set item_code=name where item_code = %s""", (item.item_code)) From 9bd6d119c405f2ace6c93d53647af140c6543542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BCrker=20Tunal=C4=B1?= Date: Thu, 7 May 2020 12:38:37 +0300 Subject: [PATCH 120/178] fix: Job Card submitted qty Update Operation Status function in work order was throwing exception without checking the "Overproduction Percentage For Work Order" setting. To submit Job Card qty for more than the Work Order's "To Manufacture Qty" we need to apply this fix. --- erpnext/manufacturing/doctype/work_order/work_order.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8a39f7420e9..a7e1acc524c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -382,6 +382,9 @@ class WorkOrder(Document): return holidays[holiday_list] def update_operation_status(self): + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) + max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage/100 * flt(self.qty)) + for d in self.get("operations"): if not d.completed_qty: d.status = "Pending" @@ -389,6 +392,8 @@ class WorkOrder(Document): d.status = "Work in Progress" elif flt(d.completed_qty) == flt(self.qty): d.status = "Completed" + elif flt(d.completed_qty) <= max_allowed_qty_for_wo: + d.status = "Completed" else: frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) @@ -809,4 +814,4 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): doc.set_item_locations() - return doc \ No newline at end of file + return doc From cd0fcfd84cc5015fad41c6c96412317cc8ffeef0 Mon Sep 17 00:00:00 2001 From: Myuddin khatri Date: Thu, 7 May 2020 15:11:39 +0530 Subject: [PATCH 121/178] solved merge conflicts --- erpnext/crm/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 38bf79e5fcb..95b19ec21ec 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -19,6 +19,5 @@ def update_lead_phone_numbers(contact, method): mobile_no = primary_mobile_nos[0] lead = frappe.get_doc("Lead", contact_lead) - lead.phone = phone - lead.mobile_no = mobile_no - lead.save() + lead.db_set("phone", phone) + lead.db_set("mobile_no", mobile_no) From 0b6ef55e78dfaaa18b3016b6eefce3c0165c1a53 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 7 May 2020 19:51:46 +0530 Subject: [PATCH 122/178] renaming LMS to Learning Management System --- .../education_settings/education_settings.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/education/doctype/education_settings/education_settings.json b/erpnext/education/doctype/education_settings/education_settings.json index 967a030fd2a..8ba189e93a1 100644 --- a/erpnext/education/doctype/education_settings/education_settings.json +++ b/erpnext/education/doctype/education_settings/education_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2017-04-05 13:33:04.519313", "doctype": "DocType", "editable_grid": 1, @@ -42,12 +43,14 @@ "fieldtype": "Column Break" }, { + "default": "0", "description": "For Batch based Student Group, the Student Batch will be validated for every Student from the Program Enrollment.", "fieldname": "validate_batch", "fieldtype": "Check", "label": "Validate Batch for Students in Student Group" }, { + "default": "0", "description": "For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment.", "fieldname": "validate_course", "fieldtype": "Check", @@ -74,13 +77,13 @@ { "fieldname": "web_academy_settings_section", "fieldtype": "Section Break", - "label": "LMS Settings" + "label": "Learning Management System Settings" }, { "depends_on": "eval: doc.enable_lms", "fieldname": "portal_title", "fieldtype": "Data", - "label": "LMS Title" + "label": "Learning Management System Title" }, { "depends_on": "eval: doc.enable_lms", @@ -89,9 +92,10 @@ "label": "Description" }, { + "default": "0", "fieldname": "enable_lms", "fieldtype": "Check", - "label": "Enable LMS" + "label": "Enable Learning Management System" }, { "default": "0", @@ -102,7 +106,8 @@ } ], "issingle": 1, - "modified": "2019-05-13 18:36:13.127563", + "links": [], + "modified": "2020-05-07 19:50:22.430576", "modified_by": "Administrator", "module": "Education", "name": "Education Settings", @@ -141,4 +146,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file From 6844dd75dabe9ef6917401a4ee2373b0b644efef Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 May 2020 23:40:17 +0530 Subject: [PATCH 123/178] fix: delete old appointment analytics tree grid report --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 580126d401e..69291af9cd2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -662,3 +662,4 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 +execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") \ No newline at end of file From 56a3b9abc4caedc4bde3a8b176d4d6b89f82f263 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 8 May 2020 02:25:09 +0530 Subject: [PATCH 124/178] Payment Order not allowing to create Payment Entry --- erpnext/accounts/doctype/payment_order/payment_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index 3f3174a69b6..7ecdc41d034 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -80,7 +80,7 @@ def make_journal_entry(doc, supplier, mode_of_payment=None): paid_amt += d.amount je.append('accounts', { - 'account': doc.references[0].account, + 'account': doc.account, 'credit_in_account_currency': paid_amt }) From 231f65ff59e214516003f654978afa37ba227a85 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 May 2020 16:30:46 +0530 Subject: [PATCH 125/178] fix: Item Barcode stays the same after updation. --- erpnext/stock/doctype/item/item.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3e37a1d4899..d7edd3ab51f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -572,6 +572,13 @@ class Item(WebsiteGenerator): frappe.throw(_("Barcode {0} is not a valid {1} code").format( item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode) + if item_barcode.barcode != item_barcode.name: + # if barcode is getting updated , the row name has to reset. + # Delete previous old row doc and re-enter row as if new to reset name in db. + item_barcode.set("__islocal", True) + item_barcode.name = None + frappe.delete_doc("Item Barcode", item_barcode.name) + def validate_warehouse_for_reorder(self): '''Validate Reorder level table for duplicate and conditional mandatory''' warehouse = [] From c739c2d355bf9175798dd8db12bd3fc71702abd7 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 8 May 2020 17:38:14 +0530 Subject: [PATCH 126/178] Appending Email and Phone in Child Table --- erpnext/selling/doctype/customer/customer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 3da0eb86d1d..ad5bfa835db 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -164,9 +164,11 @@ class Customer(TransactionBase): contact.phone = lead.phone contact.mobile_no = lead.mobile_no contact.is_primary_contact = 1 - contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) - contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.append('links', dict(link_doctype='Customer', link_name=self.name)) + if lead.email_id: + contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) + if lead.mobile_no: + contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.flags.ignore_permissions = self.flags.ignore_permissions contact.autoname() if not frappe.db.exists("Contact", contact.name): From 27b2988938c628e803a69897cde5cc90ed69151c Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 9 May 2020 16:44:36 +0530 Subject: [PATCH 127/178] Assessment Plan not getting created --- .../assessment_plan/assessment_plan.json | 702 ++---------------- 1 file changed, 54 insertions(+), 648 deletions(-) diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan.json b/erpnext/education/doctype/assessment_plan/assessment_plan.json index bc3946440c3..c7df672e71b 100644 --- a/erpnext/education/doctype/assessment_plan/assessment_plan.json +++ b/erpnext/education/doctype/assessment_plan/assessment_plan.json @@ -1,790 +1,207 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, "autoname": "EDU-ASP-.YYYY.-.#####", - "beta": 0, "creation": "2015-11-12 16:34:34.658092", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "student_group", + "assessment_name", + "assessment_group", + "grading_scale", + "column_break_2", + "course", + "program", + "academic_year", + "academic_term", + "section_break_5", + "schedule_date", + "room", + "examiner", + "examiner_name", + "column_break_4", + "from_time", + "to_time", + "supervisor", + "supervisor_name", + "section_break_20", + "maximum_assessment_score", + "assessment_criteria", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "student_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Student Group", - "length": 0, - "no_copy": 0, "options": "Student Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Assessment Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Assessment Name" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Assessment Group", - "length": 0, - "no_copy": 0, "options": "Assessment Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "course.default_grading_scale", + "fetch_if_empty": 1, "fieldname": "grading_scale", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Grading Scale", - "length": 0, - "no_copy": 0, "options": "Grading Scale", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.course", + "fetch_if_empty": 1, "fieldname": "course", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Course", - "length": 0, - "no_copy": 0, "options": "Course", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.program", "fieldname": "program", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Program" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.academic_year", "fieldname": "academic_year", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Academic Year", - "length": 0, - "no_copy": 0, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Academic Year" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student_group.academic_term", "fieldname": "academic_term", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Academic Term", - "length": 0, - "no_copy": 0, - "options": "Academic Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Academic Term" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "depends_on": "", "fieldname": "section_break_5", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Schedule", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Schedule" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", "fieldname": "schedule_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Schedule Date", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "room", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Room", - "length": 0, - "no_copy": 0, - "options": "Room", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Room" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "examiner", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Examiner", - "length": 0, - "no_copy": 0, - "options": "Instructor", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Instructor" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "examiner.instructor_name", "fieldname": "examiner_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Examiner Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "From Time", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "To Time", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "supervisor", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Supervisor", - "length": 0, - "no_copy": 0, - "options": "Instructor", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Instructor" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "supervisor.instructor_name", "fieldname": "supervisor_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Supervisor Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_20", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Evaluate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Evaluate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "maximum_assessment_score", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Maximum Assessment Score", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_criteria", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Assessment Criteria", - "length": 0, - "no_copy": 0, "options": "Assessment Plan Criteria", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amended_from", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Amended From", - "length": 0, "no_copy": 1, "options": "Assessment Plan", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-08-30 00:48:03.475522", + "links": [], + "modified": "2020-05-09 16:44:04.313175", "modified_by": "Administrator", "module": "Education", "name": "Assessment Plan", - "name_case": "", "owner": "Administrator", "permissions": [ { @@ -794,28 +211,17 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Academics User", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "restrict_to_domain": "Education", - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "assessment_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "assessment_name" } \ No newline at end of file From d837b6cedce7016560b38166ef3bcc6ef3b039fc Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Thu, 7 May 2020 17:49:32 +0800 Subject: [PATCH 128/178] style: Improve formatting This commit improves indentations and makes sql queries more readable. --- .../budget_variance_report.py | 418 ++++++++++++------ 1 file changed, 284 insertions(+), 134 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 39e218bfad2..f286a457576 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -2,187 +2,337 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +from six import iteritems + import frappe from frappe import _ from frappe.utils import flt from frappe.utils import formatdate + from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges -from six import iteritems -from pprint import pprint + def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} - columns = get_columns(filters) - if filters.get("budget_against_filter"): - dimensions = filters.get("budget_against_filter") - else: - dimensions = get_cost_centers(filters) + columns = get_columns(filters) - period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) - cam_map = get_dimension_account_month_map(filters) + if filters.get("budget_against_filter"): + dimensions = filters.get("budget_against_filter") + else: + dimensions = get_cost_centers(filters) - data = [] - for dimension in dimensions: - dimension_items = cam_map.get(dimension) - if dimension_items: - for account, monthwise_data in iteritems(dimension_items): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate(["target", "actual", "variance"]): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value + period_month_ranges = get_period_month_ranges( + filters["period"], filters["from_fiscal_year"] + ) - period_data[0] += last_total + cam_map = get_dimension_account_month_map(filters) - if(filters.get("show_cumulative")): - last_total = period_data[0] - period_data[1] + data = [] + for dimension in dimensions: + dimension_items = cam_map.get(dimension) + if dimension_items: + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] + totals = [0, 0, 0] + for year in get_fiscal_years(filters): + last_total = 0 + for relevant_months in period_month_ranges: + period_data = [0, 0, 0] + for month in relevant_months: + if monthwise_data.get(year[0]): + month_data = monthwise_data.get(year[0]).get(month, {}) + for i, fieldname in enumerate( + ["target", "actual", "variance"] + ): + value = flt(month_data.get(fieldname)) + period_data[i] += value + totals[i] += value - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly" : - row += totals - data.append(row) + period_data[0] += last_total + + if filters.get("show_cumulative"): + last_total = period_data[0] - period_data[1] + + period_data[2] = period_data[0] - period_data[1] + row += period_data + totals[2] = totals[0] - totals[1] + if filters["period"] != "Yearly": + row += totals + data.append(row) + + return columns, data - return columns, data def get_columns(filters): - columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"] + columns = [ + _(filters.get("budget_against")) + + ":Link/%s:150" % (filters.get("budget_against")), + _("Account") + ":Link/Account:150", + ] - group_months = False if filters["period"] == "Monthly" else True + group_months = False if filters["period"] == "Monthly" else True - fiscal_year = get_fiscal_years(filters) + fiscal_year = get_fiscal_years(filters) - for year in fiscal_year: - for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters["period"] == "Yearly": - labels = [_("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), _("Variance ") + " " + str(year[0])] - for label in labels: - columns.append(label+":Float:150") - else: - for label in [_("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), _("Variance") + " (%s)" + " " + str(year[0])]: - if group_months: - label = label % (formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM")) - else: - label = label % formatdate(from_date, format_string="MMM") + for year in fiscal_year: + for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): + if filters["period"] == "Yearly": + labels = [ + _("Budget") + " " + str(year[0]), + _("Actual ") + " " + str(year[0]), + _("Variance ") + " " + str(year[0]), + ] + for label in labels: + columns.append(label + ":Float:150") + else: + for label in [ + _("Budget") + " (%s)" + " " + str(year[0]), + _("Actual") + " (%s)" + " " + str(year[0]), + _("Variance") + " (%s)" + " " + str(year[0]), + ]: + if group_months: + label = label % ( + formatdate(from_date, format_string="MMM") + + "-" + + formatdate(to_date, format_string="MMM") + ) + else: + label = label % formatdate(from_date, format_string="MMM") - columns.append(label+":Float:150") + columns.append(label + ":Float:150") + + if filters["period"] != "Yearly": + return columns + [ + _("Total Budget") + ":Float:150", + _("Total Actual") + ":Float:150", + _("Total Variance") + ":Float:150", + ] + else: + return columns - if filters["period"] != "Yearly" : - return columns + [_("Total Budget") + ":Float:150", _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150"] - else: - return columns def get_cost_centers(filters): - cond = "and 1=1" - if filters.get("budget_against") == "Cost Center": - cond = "order by lft" + order_by = "" + if filters.get("budget_against") == "Cost Center": + order_by = "order by lft" - if filters.get("budget_against") in ["Cost Center", "Project"]: - return frappe.db.sql_list("""select name from `tab{tab}` where company=%s - {cond}""".format(tab=filters.get("budget_against"), cond=cond), filters.get("company")) - else: - return frappe.db.sql_list("""select name from `tab{tab}`""".format(tab=filters.get("budget_against"))) #nosec + if filters.get("budget_against") in ["Cost Center", "Project"]: + return frappe.db.sql_list( + """ + select + name + from + `tab{tab}` + where + company = %s + {order_by}; + """.format( + tab=filters.get("budget_against"), order_by=order_by + ), + filters.get("company"), + ) + else: + return frappe.db.sql_list( + """ + select + name + from + `tab{tab}` + """.format( + tab=filters.get("budget_against") + ) + ) # nosec -#Get dimension & target details + +# Get dimension & target details def get_dimension_target_details(filters): - cond = "" - if filters.get("budget_against_filter"): - cond += " and b.{budget_against} in (%s)".format(budget_against = \ - frappe.scrub(filters.get('budget_against'))) % ', '.join(['%s']* len(filters.get('budget_against_filter'))) + budget_against = frappe.scrub(filters.get("budget_against")) - return frappe.db.sql(""" - select b.{budget_against} as budget_against, b.monthly_distribution, ba.account, ba.budget_amount,b.fiscal_year - from `tabBudget` b, `tabBudget Account` ba - where b.name=ba.parent and b.docstatus = 1 and b.fiscal_year between %s and %s - and b.budget_against = %s and b.company=%s {cond} order by b.fiscal_year - """.format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond), - tuple([filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company] + filters.get('budget_against_filter')), - as_dict=True) + cond = "" + if filters.get("budget_against_filter"): + cond += """ + and + b.{budget_against} in ( + %s + ) + """.format( + budget_against=budget_against + ) % ", ".join( + ["%s"] * len(filters.get("budget_against_filter")) + ) + + return frappe.db.sql( + """ + select + b.{budget_against} as name + from + `tabBudget` b + where + b.docstatus = 1 + and b.fiscal_year between %s and %s + and b.budget_against = %s + and b.company = %s + {cond} + order by + b.fiscal_year + """.format( + budget_against=budget_against, cond=cond + ), + tuple( + [ + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.budget_against, + filters.company, + ] + + filters.get("budget_against_filter") + ), + as_dict=True, + ) -#Get target distribution details of accounts of cost center +# Get target distribution details of accounts of cost center def get_target_distribution_details(filters): - target_details = {} - for d in frappe.db.sql("""select md.name, mdp.month, mdp.percentage_allocation - from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md - where mdp.parent=md.name and md.fiscal_year between %s and %s order by md.fiscal_year""",(filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): - target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation)) + target_details = {} + for d in frappe.db.sql( + """ + select + md.name, + mdp.month, + mdp.percentage_allocation + from + `tabMonthly Distribution Percentage` mdp, + `tabMonthly Distribution` md + where + mdp.parent = md.name + and md.fiscal_year between %s + and %s + order by md.fiscal_year + """, + (filters.from_fiscal_year, filters.to_fiscal_year), + as_dict=1, + ): + target_details.setdefault(d.name, {}).setdefault( + d.month, flt(d.percentage_allocation) + ) - return target_details + return target_details -#Get actual details from gl entry + +# Get actual details from gl entry def get_actual_details(name, filters): - cond = "1=1" - budget_against=filters.get("budget_against").replace(" ", "_").lower() + budget_against = frappe.scrub(filters.get("budget_against")) - if filters.get("budget_against") == "Cost Center": - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = "lft>='{lft}' and rgt<='{rgt}'".format(lft = cc_lft, rgt=cc_rgt) + cond = "" + if filters.get("budget_against") == "Cost Center": + cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) + cond = """ + and lft >= '{lft}' + and rgt <= '{rgt}' + """.format( + lft=cc_lft, rgt=cc_rgt + ) - ac_details = frappe.db.sql("""select gl.account, gl.debit, gl.credit,gl.fiscal_year, - MONTHNAME(gl.posting_date) as month_name, b.{budget_against} as budget_against - from `tabGL Entry` gl, `tabBudget Account` ba, `tabBudget` b - where - b.name = ba.parent - and b.docstatus = 1 - and ba.account=gl.account - and b.{budget_against} = gl.{budget_against} - and gl.fiscal_year between %s and %s - and b.{budget_against}=%s - and exists(select name from `tab{tab}` where name=gl.{budget_against} and {cond}) group by gl.name order by gl.fiscal_year - """.format(tab = filters.budget_against, budget_against = budget_against, cond = cond,from_year=filters.from_fiscal_year,to_year=filters.to_fiscal_year), - (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1) + ac_details = frappe.db.sql( + """ + select + gl.account, + gl.debit, + gl.credit, + gl.fiscal_year, + MONTHNAME(gl.posting_date) as month_name, + b.{budget_against} as budget_against + from + `tabGL Entry` gl, + `tabBudget Account` ba, + `tabBudget` b + where + b.name = ba.parent + and b.docstatus = 1 + and ba.account=gl.account + and b.{budget_against} = gl.{budget_against} + and gl.fiscal_year between %s and %s + and b.{budget_against} = %s + and exists( + select + name + from + `tab{tab}` + where + name = gl.{budget_against} + {cond} + ) + group by + gl.name + order by gl.fiscal_year + """.format( + tab=filters.budget_against, budget_against=budget_against, cond=cond + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + as_dict=1, + ) - cc_actual_details = {} - for d in ac_details: - cc_actual_details.setdefault(d.account, []).append(d) + cc_actual_details = {} + for d in ac_details: + cc_actual_details.setdefault(d.account, []).append(d) + + return cc_actual_details - return cc_actual_details def get_dimension_account_month_map(filters): - import datetime - dimension_target_details = get_dimension_target_details(filters) - tdd = get_target_distribution_details(filters) + import datetime - cam_map = {} + dimension_target_details = get_dimension_target_details(filters) + tdd = get_target_distribution_details(filters) - for ccd in dimension_target_details: - actual_details = get_actual_details(ccd.budget_against, filters) + cam_map = {} - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime('%B') - cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault(ccd.fiscal_year,{})\ - .setdefault(month, frappe._dict({ - "target": 0.0, "actual": 0.0 - })) + for ccd in dimension_target_details: + actual_details = get_actual_details(ccd.budget_against, filters) - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - month_percentage = tdd.get(ccd.monthly_distribution, {}).get(month, 0) \ - if ccd.monthly_distribution else 100.0/12 + for month_id in range(1, 13): + month = datetime.date(2013, month_id, 1).strftime("%B") + cam_map.setdefault(ccd.budget_against, {}).setdefault( + ccd.account, {} + ).setdefault(ccd.fiscal_year, {}).setdefault( + month, frappe._dict({"target": 0.0, "actual": 0.0}) + ) - tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 + tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] + month_percentage = ( + tdd.get(ccd.monthly_distribution, {}).get(month, 0) + if ccd.monthly_distribution + else 100.0 / 12 + ) - for ad in actual_details.get(ccd.account, []): - if ad.month_name == month: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) + tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 + + for ad in actual_details.get(ccd.account, []): + if ad.month_name == month: + tav_dict.actual += flt(ad.debit) - flt(ad.credit) + + return cam_map - return cam_map def get_fiscal_years(filters): - fiscal_year = frappe.db.sql("""select name from `tabFiscal Year` where - name between %(from_fiscal_year)s and %(to_fiscal_year)s""", - {'from_fiscal_year': filters["from_fiscal_year"], 'to_fiscal_year': filters["to_fiscal_year"]}) + fiscal_year = frappe.db.sql( + """ + select + name + from + `tabFiscal Year` + where + name between %(from_fiscal_year)s and %(to_fiscal_year)s + order by + year + """, + { + "from_fiscal_year": filters["from_fiscal_year"], + "to_fiscal_year": filters["to_fiscal_year"], + }, + ) - return fiscal_year + return fiscal_year From 55640be627bbeff3e97ea4c9364d7d848ef61464 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Thu, 7 May 2020 18:39:47 +0800 Subject: [PATCH 129/178] fix: Fix Budget Variance Report This commit fixes a bug in Budget Variance Report where it combines the actual expense amounts across different fiscal years. This was fixed by updating the function and queries for computing the actual expense amounts. --- .../budget_variance_report.py | 172 +++++++++++++----- 1 file changed, 127 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index f286a457576..1b110b0aacd 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -2,12 +2,12 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +import datetime from six import iteritems import frappe from frappe import _ -from frappe.utils import flt -from frappe.utils import formatdate +from frappe.utils import flt, formatdate from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges @@ -126,8 +126,8 @@ def get_cost_centers(filters): from `tab{tab}` where - company = %s - {order_by}; + company=%s + {order_by} """.format( tab=filters.get("budget_against"), order_by=order_by ), @@ -153,11 +153,11 @@ def get_dimension_target_details(filters): cond = "" if filters.get("budget_against_filter"): cond += """ - and - b.{budget_against} in ( - %s - ) - """.format( + and + b.{budget_against} in ( + %s + ) + """.format( budget_against=budget_against ) % ", ".join( ["%s"] * len(filters.get("budget_against_filter")) @@ -165,7 +165,7 @@ def get_dimension_target_details(filters): return frappe.db.sql( """ - select + select distinct b.{budget_against} as name from `tabBudget` b @@ -198,19 +198,19 @@ def get_target_distribution_details(filters): target_details = {} for d in frappe.db.sql( """ - select - md.name, - mdp.month, - mdp.percentage_allocation - from - `tabMonthly Distribution Percentage` mdp, - `tabMonthly Distribution` md - where - mdp.parent = md.name - and md.fiscal_year between %s - and %s - order by md.fiscal_year - """, + select + md.name, + mdp.month, + mdp.percentage_allocation + from + `tabMonthly Distribution Percentage` mdp, + `tabMonthly Distribution` md + where + mdp.parent = md.name + and md.fiscal_year between %s + and %s + order by md.fiscal_year + """, (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1, ): @@ -229,8 +229,8 @@ def get_actual_details(name, filters): if filters.get("budget_against") == "Cost Center": cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) cond = """ - and lft >= '{lft}' - and rgt <= '{rgt}' + and lft >= \'{lft}\' + and rgt <= \'{rgt}\' """.format( lft=cc_lft, rgt=cc_rgt ) @@ -282,37 +282,44 @@ def get_actual_details(name, filters): def get_dimension_account_month_map(filters): - import datetime - dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) cam_map = {} for ccd in dimension_target_details: - actual_details = get_actual_details(ccd.budget_against, filters) + accounts = get_accounts(ccd.name, filters) + actual_details = get_actual_details(ccd.name, filters) - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map.setdefault(ccd.budget_against, {}).setdefault( - ccd.account, {} - ).setdefault(ccd.fiscal_year, {}).setdefault( - month, frappe._dict({"target": 0.0, "actual": 0.0}) - ) + for year in get_fiscal_years(filters): + year = year[0] + monthly_distribution = get_monthly_distribution(ccd.name, year, filters) + for month_id in range(1, 13): + month = datetime.date(2013, month_id, 1).strftime( + "%B" + ) # Get month string - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 - ) + for account in accounts: + account = account[0] + cam_map.setdefault(ccd.name, {}).setdefault(account, {}).setdefault( + year, {} + ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) - tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 + tav_dict = cam_map[ccd.name][account][year][month] - for ad in actual_details.get(ccd.account, []): - if ad.month_name == month: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) + month_percentage = ( + tdd.get(monthly_distribution, {}).get(month, 0) + if monthly_distribution + else 100.0 / 12 + ) + budget_amount = get_budget_amount(ccd.name, year, account, filters) + + tav_dict.target = flt(budget_amount) * month_percentage / 100 + + for ad in actual_details.get(account, []): + if ad.month_name == month and ad.fiscal_year == year: + tav_dict.actual += flt(ad.debit) - flt(ad.credit) return cam_map @@ -336,3 +343,78 @@ def get_fiscal_years(filters): ) return fiscal_year + + +def get_accounts(name, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + + accounts = frappe.db.sql( + """ + select + distinct(ba.account) + from + `tabBudget Account` ba + join + `tabBudget` b + on b.name = ba.parent + where + b.docstatus = 1 + and b.fiscal_year between %s and %s + and b.{budget_against} = %s + order by + ba.account + """.format( + budget_against=budget_against + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + ) + + return accounts + + +def get_monthly_distribution(name, year, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + + monthly_distribution = frappe.db.sql( + """ + select + monthly_distribution + from + `tabBudget` + where + docstatus = 1 + and {budget_against} = %s + and fiscal_year = %s + """.format( + budget_against=budget_against + ), + (name, year), + ) + + return monthly_distribution[0][0] if monthly_distribution else None + + +def get_budget_amount(name, year, account, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + + budget_amount = frappe.db.sql( + """ + select + ba.budget_amount + from + `tabBudget Account` ba + join + `tabBudget` b + on b.name = ba.parent + where + b.docstatus = 1 + and b.{budget_against} = %s + and b.fiscal_year = %s + and ba.account = %s + """.format( + budget_against=budget_against + ), + (name, year, account), + ) + + return budget_amount[0][0] if budget_amount else 0 From 861f2726a27d8397c889d68cd56d8721d05bfedf Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Sat, 9 May 2020 16:02:11 +0800 Subject: [PATCH 130/178] fix: Simplify get_dimension_account_month_map This commit updates get_dimension_account_month_map to no longer show the actual expense when there is no budget. This also removes the other functions and queries related to it. Spaces are also converted to tabs. --- .../budget_variance_report.py | 500 ++++++++---------- 1 file changed, 209 insertions(+), 291 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 1b110b0aacd..4d5892a9132 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -13,164 +13,165 @@ from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ def execute(filters=None): - if not filters: - filters = {} + if not filters: + filters = {} - columns = get_columns(filters) + columns = get_columns(filters) + if filters.get("budget_against_filter"): + dimensions = filters.get("budget_against_filter") + else: + dimensions = get_cost_centers(filters) - if filters.get("budget_against_filter"): - dimensions = filters.get("budget_against_filter") - else: - dimensions = get_cost_centers(filters) + period_month_ranges = get_period_month_ranges( + filters["period"], filters["from_fiscal_year"] + ) + cam_map = get_dimension_account_month_map(filters) - period_month_ranges = get_period_month_ranges( - filters["period"], filters["from_fiscal_year"] - ) + data = [] + for dimension in dimensions: + dimension_items = cam_map.get(dimension) + if dimension_items: + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] + totals = [0, 0, 0] + for year in get_fiscal_years(filters): + last_total = 0 + for relevant_months in period_month_ranges: + period_data = [0, 0, 0] + for month in relevant_months: + if monthwise_data.get(year[0]): + month_data = monthwise_data.get(year[0]).get(month, {}) + for i, fieldname in enumerate( + ["target", "actual", "variance"] + ): + value = flt(month_data.get(fieldname)) + period_data[i] += value + totals[i] += value - cam_map = get_dimension_account_month_map(filters) + period_data[0] += last_total - data = [] - for dimension in dimensions: - dimension_items = cam_map.get(dimension) - if dimension_items: - for account, monthwise_data in iteritems(dimension_items): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate( - ["target", "actual", "variance"] - ): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value + if filters.get("show_cumulative"): + last_total = period_data[0] - period_data[1] - period_data[0] += last_total + period_data[2] = period_data[0] - period_data[1] + row += period_data + totals[2] = totals[0] - totals[1] + if filters["period"] != "Yearly": + row += totals + data.append(row) - if filters.get("show_cumulative"): - last_total = period_data[0] - period_data[1] - - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly": - row += totals - data.append(row) - - return columns, data + return columns, data def get_columns(filters): - columns = [ - _(filters.get("budget_against")) - + ":Link/%s:150" % (filters.get("budget_against")), - _("Account") + ":Link/Account:150", - ] + columns = [ + _(filters.get("budget_against")) + + ":Link/%s:150" % (filters.get("budget_against")), + _("Account") + ":Link/Account:150", + ] - group_months = False if filters["period"] == "Monthly" else True + group_months = False if filters["period"] == "Monthly" else True - fiscal_year = get_fiscal_years(filters) + fiscal_year = get_fiscal_years(filters) - for year in fiscal_year: - for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters["period"] == "Yearly": - labels = [ - _("Budget") + " " + str(year[0]), - _("Actual ") + " " + str(year[0]), - _("Variance ") + " " + str(year[0]), - ] - for label in labels: - columns.append(label + ":Float:150") - else: - for label in [ - _("Budget") + " (%s)" + " " + str(year[0]), - _("Actual") + " (%s)" + " " + str(year[0]), - _("Variance") + " (%s)" + " " + str(year[0]), - ]: - if group_months: - label = label % ( - formatdate(from_date, format_string="MMM") - + "-" - + formatdate(to_date, format_string="MMM") - ) - else: - label = label % formatdate(from_date, format_string="MMM") + for year in fiscal_year: + for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): + if filters["period"] == "Yearly": + labels = [ + _("Budget") + " " + str(year[0]), + _("Actual ") + " " + str(year[0]), + _("Variance ") + " " + str(year[0]), + ] + for label in labels: + columns.append(label + ":Float:150") + else: + for label in [ + _("Budget") + " (%s)" + " " + str(year[0]), + _("Actual") + " (%s)" + " " + str(year[0]), + _("Variance") + " (%s)" + " " + str(year[0]), + ]: + if group_months: + label = label % ( + formatdate(from_date, format_string="MMM") + + "-" + + formatdate(to_date, format_string="MMM") + ) + else: + label = label % formatdate(from_date, format_string="MMM") - columns.append(label + ":Float:150") + columns.append(label + ":Float:150") - if filters["period"] != "Yearly": - return columns + [ - _("Total Budget") + ":Float:150", - _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150", - ] - else: - return columns + if filters["period"] != "Yearly": + return columns + [ + _("Total Budget") + ":Float:150", + _("Total Actual") + ":Float:150", + _("Total Variance") + ":Float:150", + ] + else: + return columns def get_cost_centers(filters): - order_by = "" - if filters.get("budget_against") == "Cost Center": - order_by = "order by lft" + order_by = "" + if filters.get("budget_against") == "Cost Center": + order_by = "order by lft" - if filters.get("budget_against") in ["Cost Center", "Project"]: - return frappe.db.sql_list( - """ + if filters.get("budget_against") in ["Cost Center", "Project"]: + return frappe.db.sql_list( + """ select name from `tab{tab}` where - company=%s + company = %s {order_by} """.format( - tab=filters.get("budget_against"), order_by=order_by - ), - filters.get("company"), - ) - else: - return frappe.db.sql_list( - """ + tab=filters.get("budget_against"), order_by=order_by + ), + filters.get("company"), + ) + else: + return frappe.db.sql_list( + """ select name from `tab{tab}` """.format( - tab=filters.get("budget_against") - ) - ) # nosec + tab=filters.get("budget_against") + ) + ) # nosec # Get dimension & target details def get_dimension_target_details(filters): - budget_against = frappe.scrub(filters.get("budget_against")) + budget_against = frappe.scrub(filters.get("budget_against")) + cond = "" + if filters.get("budget_against_filter"): + cond += """ + and + b.{budget_against} in ( + %s + ) + """.format( + budget_against=budget_against + ) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) - cond = "" - if filters.get("budget_against_filter"): - cond += """ - and - b.{budget_against} in ( - %s - ) - """.format( - budget_against=budget_against - ) % ", ".join( - ["%s"] * len(filters.get("budget_against_filter")) - ) - - return frappe.db.sql( - """ - select distinct - b.{budget_against} as name + return frappe.db.sql( + """ + select + b.{budget_against} as budget_against, + b.monthly_distribution, + ba.account, + ba.budget_amount, + b.fiscal_year from - `tabBudget` b + `tabBudget` b, + `tabBudget Account` ba where - b.docstatus = 1 + b.name = ba.parent + and b.docstatus = 1 and b.fiscal_year between %s and %s and b.budget_against = %s and b.company = %s @@ -178,65 +179,66 @@ def get_dimension_target_details(filters): order by b.fiscal_year """.format( - budget_against=budget_against, cond=cond - ), - tuple( - [ - filters.from_fiscal_year, - filters.to_fiscal_year, - filters.budget_against, - filters.company, - ] - + filters.get("budget_against_filter") - ), - as_dict=True, - ) + budget_against=budget_against, + cond=cond, + ), + tuple( + [ + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.budget_against, + filters.company, + ] + + filters.get("budget_against_filter") + ), + as_dict=True, + ) # Get target distribution details of accounts of cost center def get_target_distribution_details(filters): - target_details = {} - for d in frappe.db.sql( - """ - select - md.name, - mdp.month, - mdp.percentage_allocation - from - `tabMonthly Distribution Percentage` mdp, - `tabMonthly Distribution` md - where - mdp.parent = md.name - and md.fiscal_year between %s - and %s - order by md.fiscal_year - """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=1, - ): - target_details.setdefault(d.name, {}).setdefault( - d.month, flt(d.percentage_allocation) - ) + target_details = {} + for d in frappe.db.sql( + """ + select + md.name, + mdp.month, + mdp.percentage_allocation + from + `tabMonthly Distribution Percentage` mdp, + `tabMonthly Distribution` md + where + mdp.parent = md.name + and md.fiscal_year between %s and %s + order by + md.fiscal_year + """, + (filters.from_fiscal_year, filters.to_fiscal_year), + as_dict=1, + ): + target_details.setdefault(d.name, {}).setdefault( + d.month, flt(d.percentage_allocation) + ) - return target_details + return target_details # Get actual details from gl entry def get_actual_details(name, filters): - budget_against = frappe.scrub(filters.get("budget_against")) + budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" - if filters.get("budget_against") == "Cost Center": - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = """ - and lft >= \'{lft}\' - and rgt <= \'{rgt}\' + cond = "" + if filters.get("budget_against") == "Cost Center": + cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) + cond = """ + and lft >= "{lft}" + and rgt <= "{rgt}" """.format( - lft=cc_lft, rgt=cc_rgt - ) + lft=cc_lft, rgt=cc_rgt + ) - ac_details = frappe.db.sql( - """ + ac_details = frappe.db.sql( + """ select gl.account, gl.debit, @@ -268,65 +270,56 @@ def get_actual_details(name, filters): gl.name order by gl.fiscal_year """.format( - tab=filters.budget_against, budget_against=budget_against, cond=cond - ), - (filters.from_fiscal_year, filters.to_fiscal_year, name), - as_dict=1, - ) + tab=filters.budget_against, budget_against=budget_against, cond=cond + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + as_dict=1, + ) - cc_actual_details = {} - for d in ac_details: - cc_actual_details.setdefault(d.account, []).append(d) + cc_actual_details = {} + for d in ac_details: + cc_actual_details.setdefault(d.account, []).append(d) - return cc_actual_details + return cc_actual_details def get_dimension_account_month_map(filters): - dimension_target_details = get_dimension_target_details(filters) - tdd = get_target_distribution_details(filters) + dimension_target_details = get_dimension_target_details(filters) + tdd = get_target_distribution_details(filters) - cam_map = {} + cam_map = {} - for ccd in dimension_target_details: - accounts = get_accounts(ccd.name, filters) - actual_details = get_actual_details(ccd.name, filters) + for ccd in dimension_target_details: + actual_details = get_actual_details(ccd.budget_against, filters) - for year in get_fiscal_years(filters): - year = year[0] - monthly_distribution = get_monthly_distribution(ccd.name, year, filters) - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime( - "%B" - ) # Get month string + for month_id in range(1, 13): + month = datetime.date(2013, month_id, 1).strftime("%B") + cam_map.setdefault(ccd.budget_against, {}).setdefault( + ccd.account, {} + ).setdefault(ccd.fiscal_year, {}).setdefault( + month, frappe._dict({"target": 0.0, "actual": 0.0}) + ) - for account in accounts: - account = account[0] - cam_map.setdefault(ccd.name, {}).setdefault(account, {}).setdefault( - year, {} - ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) + tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] + month_percentage = ( + tdd.get(ccd.monthly_distribution, {}).get(month, 0) + if ccd.monthly_distribution + else 100.0 / 12 + ) - tav_dict = cam_map[ccd.name][account][year][month] + tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 - month_percentage = ( - tdd.get(monthly_distribution, {}).get(month, 0) - if monthly_distribution - else 100.0 / 12 - ) + for ad in actual_details.get(ccd.account, []): + if ad.month_name == month and ad.fiscal_year == ccd.fiscal_year: + tav_dict.actual += flt(ad.debit) - flt(ad.credit) - budget_amount = get_budget_amount(ccd.name, year, account, filters) - - tav_dict.target = flt(budget_amount) * month_percentage / 100 - - for ad in actual_details.get(account, []): - if ad.month_name == month and ad.fiscal_year == year: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) - return cam_map + return cam_map def get_fiscal_years(filters): - fiscal_year = frappe.db.sql( - """ + fiscal_year = frappe.db.sql( + """ select name from @@ -336,85 +329,10 @@ def get_fiscal_years(filters): order by year """, - { - "from_fiscal_year": filters["from_fiscal_year"], - "to_fiscal_year": filters["to_fiscal_year"], - }, - ) + { + "from_fiscal_year": filters["from_fiscal_year"], + "to_fiscal_year": filters["to_fiscal_year"], + }, + ) - return fiscal_year - - -def get_accounts(name, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - - accounts = frappe.db.sql( - """ - select - distinct(ba.account) - from - `tabBudget Account` ba - join - `tabBudget` b - on b.name = ba.parent - where - b.docstatus = 1 - and b.fiscal_year between %s and %s - and b.{budget_against} = %s - order by - ba.account - """.format( - budget_against=budget_against - ), - (filters.from_fiscal_year, filters.to_fiscal_year, name), - ) - - return accounts - - -def get_monthly_distribution(name, year, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - - monthly_distribution = frappe.db.sql( - """ - select - monthly_distribution - from - `tabBudget` - where - docstatus = 1 - and {budget_against} = %s - and fiscal_year = %s - """.format( - budget_against=budget_against - ), - (name, year), - ) - - return monthly_distribution[0][0] if monthly_distribution else None - - -def get_budget_amount(name, year, account, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - - budget_amount = frappe.db.sql( - """ - select - ba.budget_amount - from - `tabBudget Account` ba - join - `tabBudget` b - on b.name = ba.parent - where - b.docstatus = 1 - and b.{budget_against} = %s - and b.fiscal_year = %s - and ba.account = %s - """.format( - budget_against=budget_against - ), - (name, year, account), - ) - - return budget_amount[0][0] if budget_amount else 0 + return fiscal_year From 0f455060abf219b85dca62fea1379f69aeedb0a8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 10 May 2020 17:24:11 +0530 Subject: [PATCH 131/178] fix: Formatting fixes --- .../budget_variance_report.py | 69 +++++-------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 4d5892a9132..49c1d0f2ccd 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -22,9 +22,7 @@ def execute(filters=None): else: dimensions = get_cost_centers(filters) - period_month_ranges = get_period_month_ranges( - filters["period"], filters["from_fiscal_year"] - ) + period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) cam_map = get_dimension_account_month_map(filters) data = [] @@ -41,9 +39,7 @@ def execute(filters=None): for month in relevant_months: if monthwise_data.get(year[0]): month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate( - ["target", "actual", "variance"] - ): + for i, fieldname in enumerate(["target", "actual", "variance"]): value = flt(month_data.get(fieldname)) period_data[i] += value totals[i] += value @@ -67,7 +63,7 @@ def get_columns(filters): columns = [ _(filters.get("budget_against")) + ":Link/%s:150" % (filters.get("budget_against")), - _("Account") + ":Link/Account:150", + _("Account") + ":Link/Account:150" ] group_months = False if filters["period"] == "Monthly" else True @@ -80,7 +76,7 @@ def get_columns(filters): labels = [ _("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), - _("Variance ") + " " + str(year[0]), + _("Variance ") + " " + str(year[0]) ] for label in labels: columns.append(label + ":Float:150") @@ -88,7 +84,7 @@ def get_columns(filters): for label in [ _("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), - _("Variance") + " (%s)" + " " + str(year[0]), + _("Variance") + " (%s)" + " " + str(year[0]) ]: if group_months: label = label % ( @@ -105,7 +101,7 @@ def get_columns(filters): return columns + [ _("Total Budget") + ":Float:150", _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150", + _("Total Variance") + ":Float:150" ] else: return columns @@ -126,11 +122,8 @@ def get_cost_centers(filters): where company = %s {order_by} - """.format( - tab=filters.get("budget_against"), order_by=order_by - ), - filters.get("company"), - ) + """.format(tab=filters.get("budget_against"), order_by=order_by), + filters.get("company")) else: return frappe.db.sql_list( """ @@ -138,10 +131,7 @@ def get_cost_centers(filters): name from `tab{tab}` - """.format( - tab=filters.get("budget_against") - ) - ) # nosec + """.format(tab=filters.get("budget_against"))) # nosec # Get dimension & target details @@ -149,14 +139,8 @@ def get_dimension_target_details(filters): budget_against = frappe.scrub(filters.get("budget_against")) cond = "" if filters.get("budget_against_filter"): - cond += """ - and - b.{budget_against} in ( - %s - ) - """.format( - budget_against=budget_against - ) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) + cond += """ and b.{budget_against} in (%s)""".format( + budget_against=budget_against) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) return frappe.db.sql( """ @@ -190,9 +174,7 @@ def get_dimension_target_details(filters): filters.company, ] + filters.get("budget_against_filter") - ), - as_dict=True, - ) + ), as_dict=True) # Get target distribution details of accounts of cost center @@ -213,29 +195,24 @@ def get_target_distribution_details(filters): order by md.fiscal_year """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=1, - ): + (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): target_details.setdefault(d.name, {}).setdefault( d.month, flt(d.percentage_allocation) ) return target_details - # Get actual details from gl entry def get_actual_details(name, filters): budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" + if filters.get("budget_against") == "Cost Center": cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) cond = """ and lft >= "{lft}" and rgt <= "{rgt}" - """.format( - lft=cc_lft, rgt=cc_rgt - ) + """.format(lft=cc_lft, rgt=cc_rgt) ac_details = frappe.db.sql( """ @@ -269,12 +246,8 @@ def get_actual_details(name, filters): group by gl.name order by gl.fiscal_year - """.format( - tab=filters.budget_against, budget_against=budget_against, cond=cond - ), - (filters.from_fiscal_year, filters.to_fiscal_year, name), - as_dict=1, - ) + """.format(tab=filters.budget_against, budget_against=budget_against, cond=cond), + (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1) cc_actual_details = {} for d in ac_details: @@ -282,7 +255,6 @@ def get_actual_details(name, filters): return cc_actual_details - def get_dimension_account_month_map(filters): dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) @@ -326,13 +298,10 @@ def get_fiscal_years(filters): `tabFiscal Year` where name between %(from_fiscal_year)s and %(to_fiscal_year)s - order by - year """, { "from_fiscal_year": filters["from_fiscal_year"], - "to_fiscal_year": filters["to_fiscal_year"], - }, - ) + "to_fiscal_year": filters["to_fiscal_year"] + }) return fiscal_year From 00bbc7660531e6338f0334939177db05648d5d5d Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Mon, 11 May 2020 19:28:49 +0530 Subject: [PATCH 132/178] fix(patch): use translated string while setting notification template (#21678) --- erpnext/patches/v11_0/set_default_email_template_in_hr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v11_0/set_default_email_template_in_hr.py b/erpnext/patches/v11_0/set_default_email_template_in_hr.py index 14954fbeb31..46223761095 100644 --- a/erpnext/patches/v11_0/set_default_email_template_in_hr.py +++ b/erpnext/patches/v11_0/set_default_email_template_in_hr.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals +from frappe import _ import frappe def execute(): hr_settings = frappe.get_single("HR Settings") - hr_settings.leave_approval_notification_template = "Leave Approval Notification" - hr_settings.leave_status_notification_template = "Leave Status Notification" - hr_settings.save() \ No newline at end of file + hr_settings.leave_approval_notification_template = _("Leave Approval Notification") + hr_settings.leave_status_notification_template = _("Leave Status Notification") + hr_settings.save() From c8ed6b1e79a929708a6c6aea92975e78e2baabd1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 May 2020 19:29:18 +0530 Subject: [PATCH 133/178] fix: Run income-tax-slab patch only if slab already exists in payroll period (#21684) (#21687) (cherry picked from commit 85a89812a42a8d8e5f80dbec89c77e5fe5e82988) Co-authored-by: Nabin Hait --- ...ve_tax_slabs_from_payroll_period_to_income_tax_slab.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index 179be2cfde5..ec94cd01d1e 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -7,7 +7,7 @@ import frappe from frappe.model.utils.rename_field import rename_field def execute(): - if not frappe.db.table_exists("Payroll Period"): + if not (frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab")): return for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): @@ -60,6 +60,9 @@ def execute(): """, (income_tax_slab.name, company.name, period.start_date)) # move other incomes to separate document + if not frappe.db.table_exists("Employee Tax Exemption Proof Submission"): + return + migrated = [] proofs = frappe.get_all("Employee Tax Exemption Proof Submission", filters = {'docstatus': 1}, @@ -79,6 +82,9 @@ def execute(): except: pass + if not frappe.db.table_exists("Employee Tax Exemption Declaration"): + return + declerations = frappe.get_all("Employee Tax Exemption Declaration", filters = {'docstatus': 1}, fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] From 855373a2539d4084bbf55f312091bcfe3d6400c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 May 2020 21:26:18 +0530 Subject: [PATCH 134/178] fix: Message for missing valuation rate (#21686) (#21688) (cherry picked from commit 97715f2877320486353a17662ea5f57f8eaf4f8c) Co-authored-by: Marica --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- erpnext/stock/stock_ledger.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f3737ac5f7d..eb3e3e9af4f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -467,7 +467,7 @@ class StockEntry(StockController): "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), "serial_no": item.serial_no, "voucher_type": self.doctype, - "voucher_no": item.name, + "voucher_no": self.name, "company": self.company, "allow_zero_valuation": item.allow_zero_valuation_rate, }) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7567a1ae758..ba885c09487 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -527,7 +527,16 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ and cint(erpnext.is_perpetual_inventory_enabled(company)): frappe.local.message_log = [] - frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.") - .format(item_code, voucher_type, voucher_no)) + form_link = frappe.utils.get_link_to_form("Item", item_code) + + message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no) + message += "

" + _(" Here are the options to proceed:") + solutions = "
  • " + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
  • " + solutions += "
  • " + _("If not, you can Cancel / Submit this entry ") + _("{0}").format(frappe.bold("after")) + _(" performing either one below:") + "
  • " + sub_solutions = "
    • " + _("Create an incoming stock transaction for the Item.") + "
    • " + sub_solutions += "
    • " + _("Mention Valuation Rate in the Item master.") + "
    " + msg = message + solutions + sub_solutions + "" + + frappe.throw(msg=msg, title=_("Valuation Rate Missing")) return valuation_rate From b47f5830f42446f3f910b1ee43680a24e8b3a054 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 May 2020 14:11:33 +0530 Subject: [PATCH 135/178] fix: purchase inv shows overdue for fraction of outstanding --- .../purchase_invoice/purchase_invoice.py | 34 +++++++++++++++++++ erpnext/controllers/status_updater.py | 11 ------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3654f6ef032..2feeea0bd6e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -981,6 +981,40 @@ class PurchaseInvoice(BuyingController): # calculate totals again after applying TDS self.calculate_taxes_and_totals() + + def set_status(self, update=False, status=None, update_modified=True): + if self.is_new(): + if self.get('amended_from'): + self.status = 'Draft' + return + + precision = self.precision("outstanding_amount") + outstanding_amount = flt(self.outstanding_amount, precision) + due_date = getdate(self.due_date) + nowdate = getdate() + + if not status: + if self.docstatus == 2: + status = "Cancelled" + elif self.docstatus == 1: + elif outstanding_amount > 0 and due_date < nowdate: + self.status = "Overdue" + elif outstanding_amount > 0 and due_date >= nowdate: + self.status = "Unpaid" + #Check if outstanding amount is 0 due to debit note issued against invoice + elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + self.status = "Debit Note Issued" + elif self.is_return == 1: + self.status = "Return" + elif outstanding_amount<=0: + self.status = "Paid" + else: + self.status = "Submitted" + else: + self.status = "Draft" + + if update: + self.db_set('status', self.status, update_modified = update_modified) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index de76e45cd11..b465a106f0e 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -69,17 +69,6 @@ status_map = { ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], ], - "Purchase Invoice": [ - ["Draft", None], - ["Submitted", "eval:self.docstatus==1"], - ["Paid", "eval:self.outstanding_amount==0 and self.docstatus==1"], - ["Return", "eval:self.is_return==1 and self.docstatus==1"], - ["Debit Note Issued", - "eval:self.outstanding_amount <= 0 and self.docstatus==1 and self.is_return==0 and get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1})"], - ["Unpaid", "eval:self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.docstatus==1"], - ["Overdue", "eval:self.outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.docstatus==1"], - ["Cancelled", "eval:self.docstatus==2"], - ], "Material Request": [ ["Draft", None], ["Stopped", "eval:self.status == 'Stopped'"], From cb1cc96210c08e0e278a00781ad1afd2bc561d17 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 13 May 2020 16:30:10 +0530 Subject: [PATCH 136/178] fix: item tax template fetching in offline pos --- erpnext/accounts/doctype/sales_invoice/pos.py | 3 +- erpnext/accounts/page/pos/pos.js | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index 16e918cc728..c2f79841725 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -180,10 +180,11 @@ def get_items_list(pos_profile, company): i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image, id.expense_account, id.selling_cost_center, id.default_warehouse, - i.sales_uom, c.conversion_factor + i.sales_uom, c.conversion_factor, it.item_tax_template, it.valid_from from `tabItem` i left join `tabItem Default` id on id.parent = i.name and id.company = %s + left join `tabItem Tax` it on it.parent = i.name left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom where i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 24fcb41a5db..06575ac3a69 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1458,7 +1458,36 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ this.child.batch_no = this.item_batch_no[this.child.item_code]; this.child.serial_no = (this.item_serial_no[this.child.item_code] ? this.item_serial_no[this.child.item_code][0] : ''); - this.child.item_tax_rate = JSON.stringify(this.tax_data[this.child.item_code]); + + const tax_template_is_valid = frappe.datetime.get_diff(frappe.datetime.now_date(), this.items[0].valid_from) > 0; + + this.child.item_tax_template = tax_template_is_valid ? this.items[0].item_tax_template : ''; + this.child.item_tax_rate = JSON.stringify(this.tax_data[this.child.item_tax_template]); + + if (this.child.item_tax_rate) { + this.add_taxes_from_item_tax_template(this.child.item_tax_rate); + } + }, + + add_taxes_from_item_tax_template: function(item_tax_map) { + let me = this; + + if(item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) { + if(typeof (item_tax_map) == "string") { + item_tax_map = JSON.parse(item_tax_map); + } + + $.each(item_tax_map, function(tax, rate) { + let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax); + if(!found) { + let child = frappe.model.add_child(me.frm.doc, "taxes"); + child.charge_type = "On Net Total"; + child.account_head = tax; + child.description = String(tax); + child.rate = rate; + } + }); + } }, update_paid_amount_status: function (update_paid_amount) { From 6a317a8e78efd40f7ee8d3a66a94156a3725f4da Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 13 May 2020 18:58:03 +0530 Subject: [PATCH 137/178] fix: Add total rows in Report Purchase Order Items to be Received or Billed (#21711) --- .../purchase_order_items_to_be_received_or_billed.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json index 48c0f423fd9..6714b2e02c9 100644 --- a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json +++ b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json @@ -1,5 +1,5 @@ { - "add_total_row": 0, + "add_total_row": 1, "creation": "2019-09-16 14:10:33.102865", "disable_prepared_report": 0, "disabled": 0, @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2019-09-21 15:19:55.710578", + "modified": "2020-05-13 15:27:45.228418", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Order Items To Be Received or Billed", From 12739ec464b0079ccf5b185c3b5fc6b42cb0a99d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 13 May 2020 22:02:56 +0530 Subject: [PATCH 138/178] fix: invalid conditional statement --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2feeea0bd6e..978898a54b2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -997,7 +997,7 @@ class PurchaseInvoice(BuyingController): if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - elif outstanding_amount > 0 and due_date < nowdate: + if outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" From b432f3358e54d46ff236c46091009c6c3bd3bc4a Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 15 May 2020 04:25:10 +0530 Subject: [PATCH 139/178] fix: item price not fetching when customer is unset in item price (#21489) * fix: item price not fetching when customer is unset in item price * fix: item price of selling type has hidden supplier value * fix: remove test variable * fix: test * Update erpnext/stock/get_item_details.py Co-authored-by: Marica * patch: invalid customer/supplier based on item price type * fix: remove patches from develop branch * fix: patch Co-authored-by: Marica --- erpnext/patches.txt | 3 ++- ...stomer_supplier_based_on_type_of_item_price.py | 15 +++++++++++++++ erpnext/stock/doctype/item_price/item_price.py | 7 +++++++ erpnext/stock/get_item_details.py | 2 +- 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 69291af9cd2..5666700a935 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -662,4 +662,5 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 -execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") \ No newline at end of file +execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") +erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price diff --git a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py new file mode 100644 index 00000000000..60aec05466a --- /dev/null +++ b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + invalid_selling_item_price = frappe.db.sql( + """SELECT name FROM `tabItem Price` WHERE selling = 1 and buying = 0 and (supplier IS NOT NULL or supplier = '')""" + ) + invalid_buying_item_price = frappe.db.sql( + """SELECT name FROM `tabItem Price` WHERE selling = 0 and buying = 1 and (customer IS NOT NULL or customer = '')""" + ) + docs_to_modify = invalid_buying_item_price + invalid_selling_item_price + for d in docs_to_modify: + # saving the doc will auto reset invalid customer/supplier field + doc = frappe.get_doc("Item Price", d[0]) + doc.save() \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 957c41546b3..8e39eb5037d 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -69,3 +69,10 @@ class ItemPrice(Document): self.reference = self.customer if self.buying: self.reference = self.supplier + + if self.selling and not self.buying: + # if only selling then remove supplier + self.supplier = None + if self.buying and not self.selling: + # if only buying then remove customer + self.customer = None diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f43592bef63..50c8c4e53f4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -624,7 +624,7 @@ def get_item_price(args, item_code, ignore_party=False): elif args.get("supplier"): conditions += " and supplier=%(supplier)s" else: - conditions += " and (customer is null or customer = '') and (supplier is null or supplier = '')" + conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" if args.get('transaction_date'): conditions += """ and %(transaction_date)s between From 0af125f6fe721716773f8b6484f60f8f949d86ce Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 15 May 2020 05:26:41 +0530 Subject: [PATCH 140/178] fix: test --- erpnext/accounts/doctype/pos_profile/test_pos_profile.py | 6 +++--- erpnext/accounts/doctype/sales_invoice/pos.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 58f12162d14..f8d52a78336 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -15,12 +15,12 @@ class TestPOSProfile(unittest.TestCase): pos_profile = get_pos_profile("_Test Company") or {} if pos_profile: doc = frappe.get_doc("POS Profile", pos_profile.get("name")) - doc.append('item_groups', {'item_group': '_Test Item Group'}) - doc.append('customer_groups', {'customer_group': '_Test Customer Group'}) + doc.set('item_groups', [{'item_group': '_Test Item Group'}]) + doc.set('customer_groups', [{'customer_group': '_Test Customer Group'}]) doc.save() items = get_items_list(doc, doc.company) customers = get_customers_list(doc) - + products_count = frappe.db.sql(""" select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1) customers_count = frappe.db.sql(""" select count(name) from tabCustomer where customer_group = '_Test Customer Group'""") diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index c2f79841725..f371bd425c5 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -189,6 +189,7 @@ def get_items_list(pos_profile, company): where i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 {cond} + group by i.item_code """.format(cond=cond), tuple([company] + args_list), as_dict=1) From 71d9a52a079fc7c0018a14ca61c27422f013f0b4 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 15 May 2020 11:55:36 +0530 Subject: [PATCH 141/178] fix: duplicate leave expiry creation (#21506) * fix: validate existing ledger entries to avoid duplicates * patch: remove duplicate ledger entries created * fix: consider only submitted ledger entries * fix: delete duplicate leaves from the ledger * fix: check if duplicate ledger entry exists * chore: formatting changes Co-authored-by: Nabin Hait --- .../leave_application/leave_application.py | 6 +-- .../leave_ledger_entry/leave_ledger_entry.py | 50 +++++++++++-------- erpnext/patches.txt | 1 + .../remove_duplicate_leave_ledger_entries.py | 44 ++++++++++++++++ 4 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index c870cc5d5d0..6ef4a303d87 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -541,7 +541,7 @@ def get_remaining_leaves(allocation, leaves_taken, date, expiry): return _get_remaining_leaves(total_leaves, allocation.to_date) -def get_leaves_for_period(employee, leave_type, from_date, to_date): +def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 @@ -551,8 +551,8 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': leave_days += leave_entry.leaves - elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ - and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date): + elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ + and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)): leave_days += leave_entry.leaves elif leave_entry.transaction_type == 'Leave Application': diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 9ed58c9e59b..63559c4f5ae 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -88,32 +88,40 @@ def get_previous_expiry_ledger_entry(ledger): }, fieldname=['name']) def process_expired_allocation(): - ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry ''' + ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry + Case 1: carry forwarded expiry period is set for the leave type, + create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves + Case 2: leave type has no specific expiry period for carry forwarded leaves + and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves. + ''' # fetch leave type records that has carry forwarded leaves expiry leave_type_records = frappe.db.get_values("Leave Type", filters={ 'expire_carry_forwarded_leaves_after_days': (">", 0) }, fieldname=['name']) - leave_type = [record[0] for record in leave_type_records] + leave_type = [record[0] for record in leave_type_records] or [''] - expired_allocation = frappe.db.sql_list("""SELECT name - FROM `tabLeave Ledger Entry` - WHERE - `transaction_type`='Leave Allocation' - AND `is_expired`=1""") - - expire_allocation = frappe.get_all("Leave Ledger Entry", - fields=['leaves', 'to_date', 'employee', 'leave_type', 'is_carry_forward', 'transaction_name as name', 'transaction_type'], - filters={ - 'to_date': ("<", today()), - 'transaction_type': 'Leave Allocation', - 'transaction_name': ('not in', expired_allocation) - }, - or_filters={ - 'is_carry_forward': 0, - 'leave_type': ('in', leave_type) - }) + # fetch non expired leave ledger entry of transaction_type allocation + expire_allocation = frappe.db.sql(""" + SELECT + leaves, to_date, employee, leave_type, + is_carry_forward, transaction_name as name, transaction_type + FROM `tabLeave Ledger Entry` l + WHERE (NOT EXISTS + (SELECT name + FROM `tabLeave Ledger Entry` + WHERE + transaction_name = l.transaction_name + AND transaction_type = 'Leave Allocation' + AND name<>l.name + AND docstatus = 1 + AND ( + is_carry_forward=l.is_carry_forward + OR (is_carry_forward = 0 AND leave_type not in %s) + ))) + AND transaction_type = 'Leave Allocation' + AND to_date < %s""", (leave_type, today()), as_dict=1) if expire_allocation: create_expiry_ledger_entry(expire_allocation) @@ -133,6 +141,7 @@ def get_remaining_leaves(allocation): 'employee': allocation.employee, 'leave_type': allocation.leave_type, 'to_date': ('<=', allocation.to_date), + 'docstatus': 1 }, fieldname=['SUM(leaves)']) @frappe.whitelist() @@ -159,7 +168,8 @@ def expire_allocation(allocation, expiry_date=None): def expire_carried_forward_allocation(allocation): ''' Expires remaining leaves in the on carried forward allocation ''' from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period - leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date) + leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, + allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5666700a935..30ec5be4902 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -662,5 +662,6 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 +erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price diff --git a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py new file mode 100644 index 00000000000..98a2fcf27ea --- /dev/null +++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py @@ -0,0 +1,44 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + """Delete duplicate leave ledger entries of type allocation created.""" + if not frappe.db.a_row_exists("Leave Ledger Entry"): + return + + duplicate_records_list = get_duplicate_records() + delete_duplicate_ledger_entries(duplicate_records_list) + +def get_duplicate_records(): + """Fetch all but one duplicate records from the list of expired leave allocation.""" + return frappe.db.sql_list(""" + WITH duplicate_records AS + (SELECT + name, transaction_name, is_carry_forward, + ROW_NUMBER() over(partition by transaction_name order by creation)as row + FROM `tabLeave Ledger Entry` l + WHERE (EXISTS + (SELECT name + FROM `tabLeave Ledger Entry` + WHERE + transaction_name = l.transaction_name + AND transaction_type = 'Leave Allocation' + AND name <> l.name + AND employee = l.employee + AND docstatus = 1 + AND leave_type = l.leave_type + AND is_carry_forward=l.is_carry_forward + AND to_date = l.to_date + AND from_date = l.from_date + AND is_expired = 1 + ))) + SELECT name FROM duplicate_records WHERE row > 1 + """) + +def delete_duplicate_ledger_entries(duplicate_records_list): + """Delete duplicate leave ledger entries.""" + if duplicate_records_list: + frappe.db.sql(''' DELETE FROM `tabLeave Ledger Entry` WHERE name in {0}'''.format(tuple(duplicate_records_list))) #nosec \ No newline at end of file From f34436906880e35929aec8600bd38a61b4638028 Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 15 May 2020 12:09:16 +0530 Subject: [PATCH 142/178] format: better error messages for invalid coupon codes (v12) (#21598) * format: better error messages for invalid coupon codes * fix: remove unnecessary docstatus check --- .../accounts/doctype/pricing_rule/utils.py | 30 +++++++------ erpnext/shopping_cart/cart.py | 44 ++++++++++--------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b358f56671f..cb05481df5a 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -4,13 +4,19 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, copy, json -from frappe import throw, _ + +import copy +import json + from six import string_types -from frappe.utils import flt, cint, get_datetime, get_link_to_form, today + +import frappe from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor +from frappe import _, throw +from frappe.utils import cint, flt, get_datetime, get_link_to_form, getdate, today + class MultiplePricingRuleConflict(frappe.ValidationError): pass @@ -502,18 +508,16 @@ def get_pricing_rule_items(pr_doc): return list(set(apply_on_data)) def validate_coupon_code(coupon_name): - from frappe.utils import today,getdate - coupon=frappe.get_doc("Coupon Code",coupon_name) + coupon = frappe.get_doc("Coupon Code", coupon_name) + if coupon.valid_from: - if coupon.valid_from > getdate(today()) : - frappe.throw(_("Sorry,coupon code validity has not started")) + if coupon.valid_from > getdate(today()): + frappe.throw(_("Sorry, this coupon code's validity has not started")) elif coupon.valid_upto: - if coupon.valid_upto < getdate(today()) : - frappe.throw(_("Sorry,coupon code validity has expired")) - elif coupon.used>=coupon.maximum_use: - frappe.throw(_("Sorry,coupon code are exhausted")) - else: - return + if coupon.valid_upto < getdate(today()): + frappe.throw(_("Sorry, this coupon code's validity has expired")) + elif coupon.used >= coupon.maximum_use: + frappe.throw(_("Sorry, this coupon code is no longer valid")) def update_coupon_code_count(coupon_name,transaction_type): coupon=frappe.get_doc("Coupon Code",coupon_name) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 4c3ac100d5b..8e644f4144d 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -541,27 +541,31 @@ def show_terms(doc): return doc.tc_name @frappe.whitelist(allow_guest=True) -def apply_coupon_code(applied_code,applied_referral_sales_partner): +def apply_coupon_code(applied_code, applied_referral_sales_partner): quotation = True - if applied_code: - coupon_list=frappe.get_all('Coupon Code', filters={"docstatus": ("<", "2"), 'coupon_code':applied_code }, fields=['name']) - if coupon_list: - coupon_name=coupon_list[0].name - from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code - validate_coupon_code(coupon_name) - quotation = _get_cart_quotation() - quotation.coupon_code=coupon_name + + if not applied_code: + frappe.throw(_("Please enter a coupon code")) + + coupon_list = frappe.get_all('Coupon Code', filters={'coupon_code': applied_code}) + if not coupon_list: + frappe.throw(_("Please enter a valid coupon code")) + + coupon_name = coupon_list[0].name + + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(coupon_name) + quotation = _get_cart_quotation() + quotation.coupon_code = coupon_name + quotation.flags.ignore_permissions = True + quotation.save() + + if applied_referral_sales_partner: + sales_partner_list = frappe.get_all('Sales Partner', filters={'referral_code': applied_referral_sales_partner}) + if sales_partner_list: + sales_partner_name = sales_partner_list[0].name + quotation.referral_sales_partner = sales_partner_name quotation.flags.ignore_permissions = True quotation.save() - if applied_referral_sales_partner: - sales_partner_list=frappe.get_all('Sales Partner', filters={'docstatus': 0, 'referral_code':applied_referral_sales_partner }, fields=['name']) - if sales_partner_list: - sales_partner_name=sales_partner_list[0].name - quotation.referral_sales_partner=sales_partner_name - quotation.flags.ignore_permissions = True - quotation.save() - else: - frappe.throw(_("Please enter valid coupon code !!")) - else: - frappe.throw(_("Please enter coupon code !!")) + return quotation From 8ed066ba1f01aa209e7bf793ae3fc086569debf4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 15 May 2020 12:58:19 +0530 Subject: [PATCH 143/178] fix: Add misssing dimensions in GL entries (#21691) * fix: Add misssing dimensions in GL entries * fix: expnese_taxes_and_charges.json * fix: Add project filter in trial balance report * fix: Use current dimensions instead of dimensions from asset --- erpnext/accounts/deferred_revenue.py | 12 +++--- .../accounting_dimension.py | 6 +-- .../invoice_discounting.py | 14 +++++-- .../doctype/payment_entry/payment_entry.py | 8 ++-- .../purchase_invoice/purchase_invoice.py | 13 +++---- .../doctype/sales_invoice/sales_invoice.py | 25 ++++++------- .../report/trial_balance/trial_balance.js | 12 ++++-- .../report/trial_balance/trial_balance.py | 8 ++++ erpnext/assets/doctype/asset/asset.py | 6 +-- erpnext/education/doctype/fees/fees.py | 6 ++- .../hr/doctype/expense_claim/expense_claim.py | 15 ++++---- .../expense_claim_detail.json | 37 +++++++++++++++++-- .../expense_taxes_and_charges.json | 18 +++++++-- erpnext/patches.txt | 2 +- ...counting_dimensions_in_missing_doctypes.py | 3 +- 15 files changed, 125 insertions(+), 60 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 62a8f05c659..cbb151f95a3 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -159,7 +159,7 @@ def book_deferred_income_or_expense(doc, posting_date=None): total_days, total_booking_days, account_currency) make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, end_date, project, account_currency, item.cost_center, item.name) + amount, base_amount, end_date, project, account_currency, item.cost_center, item) if getdate(end_date) < getdate(posting_date) and not last_gl_entry: _book_deferred_revenue_or_expense(item) @@ -170,7 +170,7 @@ def book_deferred_income_or_expense(doc, posting_date=None): _book_deferred_revenue_or_expense(item) def make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, posting_date, project, account_currency, cost_center, voucher_detail_no): + amount, base_amount, posting_date, project, account_currency, cost_center, item): # GL Entry for crediting the amount in the deferred expense from erpnext.accounts.general_ledger import make_gl_entries @@ -184,10 +184,10 @@ def make_gl_entries(doc, credit_account, debit_account, against, "credit": base_amount, "credit_in_account_currency": amount, "cost_center": cost_center, - "voucher_detail_no": voucher_detail_no, + "voucher_detail_no": item.name, 'posting_date': posting_date, 'project': project - }, account_currency) + }, account_currency, item=item) ) # GL Entry to debit the amount from the expense gl_entries.append( @@ -197,10 +197,10 @@ def make_gl_entries(doc, credit_account, debit_account, against, "debit": base_amount, "debit_in_account_currency": amount, "cost_center": cost_center, - "voucher_detail_no": voucher_detail_no, + "voucher_detail_no": item.name, 'posting_date': posting_date, 'project': project - }, account_currency) + }, account_currency, item=item) ) if gl_entries: diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 7a85bfb26b6..894ec5bdec5 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -162,9 +162,9 @@ def toggle_disabling(doc): def get_doctypes_with_dimensions(): doclist = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Sales Invoice Item", "Purchase Invoice Item", - "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", - "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", + "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", + "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", + "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", "Subscription Plan"] diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 39fc203d53f..594b4d4a223 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import flt, getdate, nowdate, add_days from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class InvoiceDiscounting(AccountsController): def validate(self): @@ -81,10 +82,15 @@ class InvoiceDiscounting(AccountsController): def make_gl_entries(self): company_currency = frappe.get_cached_value('Company', self.company, "default_currency") + gl_entries = [] + invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"] + accounting_dimensions = get_accounting_dimensions() + + invoice_fields.extend(accounting_dimensions) + for d in self.invoices: - inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, - ["debit_to", "party_account_currency", "conversion_rate", "cost_center"], as_dict=1) + inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1) if d.outstanding_amount: outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate, @@ -102,7 +108,7 @@ class InvoiceDiscounting(AccountsController): "cost_center": inv.cost_center, "against_voucher": d.sales_invoice, "against_voucher_type": "Sales Invoice" - }, inv.party_account_currency)) + }, inv.party_account_currency, item=inv)) gl_entries.append(self.get_gl_dict({ "account": self.accounts_receivable_credit, @@ -115,7 +121,7 @@ class InvoiceDiscounting(AccountsController): "cost_center": inv.cost_center, "against_voucher": d.sales_invoice, "against_voucher_type": "Sales Invoice" - }, ar_credit_account_currency)) + }, ar_credit_account_currency, item=inv)) make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3b23aebd00f..da71acdb9da 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -85,7 +85,7 @@ class PaymentEntry(AccountsController): self.update_payment_schedule(cancel=1) self.set_payment_req_status() self.set_status() - + def set_payment_req_status(self): from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status update_payment_req_status(self, None) @@ -503,7 +503,7 @@ class PaymentEntry(AccountsController): "against": against_account, "account_currency": self.party_account_currency, "cost_center": self.cost_center - }) + }, item=self) dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit" @@ -547,7 +547,7 @@ class PaymentEntry(AccountsController): "credit_in_account_currency": self.paid_amount, "credit": self.base_paid_amount, "cost_center": self.cost_center - }) + }, item=self) ) if self.payment_type in ("Receive", "Internal Transfer"): gl_entries.append( @@ -558,7 +558,7 @@ class PaymentEntry(AccountsController): "debit_in_account_currency": self.received_amount, "debit": self.base_received_amount, "cost_center": self.cost_center - }) + }, item=self) ) def add_deductions_gl_entries(self, gl_entries): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3654f6ef032..e3a70991f89 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -452,7 +452,7 @@ class PurchaseInvoice(BuyingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) def make_item_gl_entries(self, gl_entries): @@ -802,7 +802,7 @@ class PurchaseInvoice(BuyingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( @@ -813,7 +813,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": self.base_paid_amount \ if bank_account_currency==self.company_currency else self.paid_amount, "cost_center": self.cost_center - }, bank_account_currency) + }, bank_account_currency, item=self) ) def make_write_off_gl_entry(self, gl_entries): @@ -834,7 +834,7 @@ class PurchaseInvoice(BuyingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( self.get_gl_dict({ @@ -844,7 +844,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": self.base_write_off_amount \ if write_off_account_currency==self.company_currency else self.write_off_amount, "cost_center": self.cost_center or self.write_off_cost_center - }) + }, item=self) ) def make_gle_for_rounding_adjustment(self, gl_entries): @@ -863,8 +863,7 @@ class PurchaseInvoice(BuyingController): "debit_in_account_currency": self.rounding_adjustment, "debit": self.base_rounding_adjustment, "cost_center": self.cost_center or round_off_cost_center, - } - )) + }, item=self)) def on_cancel(self): super(PurchaseInvoice, self).on_cancel() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5218b740edd..fa175fc816e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -785,7 +785,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) def make_tax_gl_entries(self, gl_entries): @@ -802,7 +802,7 @@ class SalesInvoice(SellingController): tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))), "cost_center": tax.cost_center - }, account_currency) + }, account_currency, item=tax) ) def make_item_gl_entries(self, gl_entries): @@ -822,7 +822,7 @@ class SalesInvoice(SellingController): for gle in fixed_asset_gl_entries: gle["against"] = self.customer - gl_entries.append(self.get_gl_dict(gle)) + gl_entries.append(self.get_gl_dict(gle, item=item)) asset.db_set("disposal_date", self.posting_date) asset.set_status("Sold" if self.docstatus==1 else None) @@ -860,7 +860,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }) + }, item=self) ) gl_entries.append( self.get_gl_dict({ @@ -869,7 +869,7 @@ class SalesInvoice(SellingController): "against": self.customer, "debit": self.loyalty_amount, "remark": "Loyalty Points redeemed by the customer" - }) + }, item=self) ) def make_pos_gl_entries(self, gl_entries): @@ -890,7 +890,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) payment_mode_account_currency = get_account_currency(payment_mode.account) @@ -903,7 +903,7 @@ class SalesInvoice(SellingController): if payment_mode_account_currency==self.company_currency \ else payment_mode.amount, "cost_center": self.cost_center - }, payment_mode_account_currency) + }, payment_mode_account_currency, item=self) ) def make_gle_for_change_amount(self, gl_entries): @@ -921,7 +921,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( @@ -930,7 +930,7 @@ class SalesInvoice(SellingController): "against": self.customer, "credit": self.base_change_amount, "cost_center": self.cost_center - }) + }, item=self) ) else: frappe.throw(_("Select change amount account"), title="Mandatory Field") @@ -954,7 +954,7 @@ class SalesInvoice(SellingController): "against_voucher": self.return_against if cint(self.is_return) else self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center - }, self.party_account_currency) + }, self.party_account_currency, item=self) ) gl_entries.append( self.get_gl_dict({ @@ -965,7 +965,7 @@ class SalesInvoice(SellingController): self.precision("base_write_off_amount")) if write_off_account_currency==self.company_currency else flt(self.write_off_amount, self.precision("write_off_amount"))), "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center - }, write_off_account_currency) + }, write_off_account_currency, item=self) ) def make_gle_for_rounding_adjustment(self, gl_entries): @@ -982,8 +982,7 @@ class SalesInvoice(SellingController): "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")), "cost_center": self.cost_center or round_off_cost_center, - } - )) + }, item=self)) def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 622bab6946f..07752e1e626 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -46,7 +46,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "default": frappe.defaults.get_user_default("year_end_date"), }, { - "fieldname":"cost_center", + "fieldname": "cost_center", "label": __("Cost Center"), "fieldtype": "Link", "options": "Cost Center", @@ -61,7 +61,13 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } }, { - "fieldname":"finance_book", + "fieldname": "project", + "label": __("Project"), + "fieldtype": "Link", + "options": "Project" + }, + { + "fieldname": "finance_book", "label": __("Finance Book"), "fieldtype": "Link", "options": "Finance Book", @@ -97,7 +103,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Trial Balance"].filters.splice(5, 0 ,{ + frappe.query_reports["Trial Balance"].filters.splice(6, 0 ,{ "fieldname": dimension["fieldname"], "label": __(dimension["label"]), "fieldtype": "Link", diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d78324157a9..8bd4399e608 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -69,6 +69,10 @@ def get_data(filters): gl_entries_by_account = {} opening_balances = get_opening_balances(filters) + + #add filter inside list so that the query in financial_statements.py doesn't break + filters.project = [filters.project] + set_gl_entries_by_account(filters.company, filters.from_date, filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) @@ -102,6 +106,9 @@ def get_rootwise_opening_balances(filters, report_type): additional_conditions += """ and cost_center in (select name from `tabCost Center` where lft >= %s and rgt <= %s)""" % (lft, rgt) + if filters.project: + additional_conditions += " and project = %(project)s" + if filters.finance_book: fb_conditions = " AND finance_book = %(finance_book)s" if filters.include_default_book_entries: @@ -116,6 +123,7 @@ def get_rootwise_opening_balances(filters, report_type): "from_date": filters.from_date, "report_type": report_type, "year_start_date": filters.year_start_date, + "project": filters.project, "finance_book": filters.finance_book, "company_fb": frappe.db.get_value("Company", filters.company, 'default_finance_book') } diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b8daf1fead9..d56549f7d90 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -512,7 +512,7 @@ class Asset(AccountsController): "credit": self.purchase_receipt_amount, "credit_in_account_currency": self.purchase_receipt_amount, "cost_center": self.cost_center - })) + }, item=self)) gl_entries.append(self.get_gl_dict({ "account": fixed_asset_account, @@ -522,7 +522,7 @@ class Asset(AccountsController): "debit": self.purchase_receipt_amount, "debit_in_account_currency": self.purchase_receipt_amount, "cost_center": self.cost_center - })) + }, item=self)) if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries @@ -655,7 +655,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non if asset: account = get_asset_category_account(account_name, asset=asset, asset_category = asset_category, company = company) - + if not asset and not account: account = get_asset_category_account(account_name, asset_category = asset_category, company = company) diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index f31003bf326..deb3968ecc7 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -97,14 +97,16 @@ class Fees(AccountsController): "debit_in_account_currency": self.grand_total, "against_voucher": self.name, "against_voucher_type": self.doctype - }) + }, item=self) + fee_gl_entry = self.get_gl_dict({ "account": self.income_account, "against": self.student, "credit": self.grand_total, "credit_in_account_currency": self.grand_total, "cost_center": self.cost_center - }) + }, item=self) + from erpnext.accounts.general_ledger import make_gl_entries make_gl_entries([student_gl_entries, fee_gl_entry], cancel=(self.docstatus == 2), update_outstanding="Yes", merge_entries=False) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index fefec01130a..4d97b0d0c72 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -115,8 +115,9 @@ class ExpenseClaim(AccountsController): "party_type": "Employee", "party": self.employee, "against_voucher_type": self.doctype, - "against_voucher": self.name - }) + "against_voucher": self.name, + "cost_center": self.cost_center + }, item=self) ) # expense entries @@ -127,8 +128,8 @@ class ExpenseClaim(AccountsController): "debit": data.sanctioned_amount, "debit_in_account_currency": data.sanctioned_amount, "against": self.employee, - "cost_center": self.cost_center - }) + "cost_center": data.cost_center + }, item=data) ) for data in self.advances: @@ -156,7 +157,7 @@ class ExpenseClaim(AccountsController): "credit": self.grand_total, "credit_in_account_currency": self.grand_total, "against": self.employee - }) + }, item=self) ) gl_entry.append( @@ -169,7 +170,7 @@ class ExpenseClaim(AccountsController): "debit_in_account_currency": self.grand_total, "against_voucher": self.name, "against_voucher_type": self.doctype, - }) + }, item=self) ) return gl_entry @@ -186,7 +187,7 @@ class ExpenseClaim(AccountsController): "cost_center": self.cost_center, "against_voucher_type": self.doctype, "against_voucher": self.name - }) + }, item=tax) ) def validate_account_details(self): diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index b23fb6af0fe..518e0aeefac 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -10,6 +10,22 @@ "docstatus": 0, "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "expense_date", + "column_break_2", + "expense_type", + "default_account", + "section_break_4", + "description", + "section_break_6", + "amount", + "column_break_8", + "sanctioned_amount", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" + ], "fields": [ { "allow_bulk_edit": 0, @@ -348,6 +364,21 @@ "translatable": 0, "unique": 0, "width": "150px" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "has_web_view": 0, @@ -359,12 +390,12 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-06-10 08:41:36.122565", + "links": [], + "modified": "2020-05-11 18:54:35.601592", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json index 1b1889b205f..f6a1e1bfe25 100644 --- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -7,14 +7,16 @@ "engine": "InnoDB", "field_order": [ "account_head", - "cost_center", "col_break1", "rate", "description", "section_break_6", "tax_amount", "column_break_8", - "total" + "total", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -90,10 +92,20 @@ { "fieldname": "column_break_8", "fieldtype": "Column Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "istable": 1, - "modified": "2019-06-20 12:01:33.919555", + "links": [], + "modified": "2020-05-11 19:01:26.611758", "modified_by": "Administrator", "module": "HR", "name": "Expense Taxes and Charges", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 30ec5be4902..5517cbc9a80 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -625,7 +625,7 @@ erpnext.patches.v11_1.update_default_supplier_in_item_defaults erpnext.patches.v12_0.update_due_date_in_gle erpnext.patches.v12_0.add_default_buying_selling_terms_in_company erpnext.patches.v12_0.update_ewaybill_field_position -erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes +erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes #2020-05-11 erpnext.patches.v11_1.set_status_for_material_request_type_manufacture erpnext.patches.v12_0.move_plaid_settings_to_doctype execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_link') diff --git a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py index b71ea665943..657decfed23 100644 --- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py +++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py @@ -20,7 +20,8 @@ def execute(): else: insert_after_field = 'accounting_dimensions_section' - for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item"]: + for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", + "Expense Claim Detail", "Expense Taxes and Charges"]: field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) From 2fa841821a028b65f62d21e774f9e7be24a4ac9a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 15 May 2020 14:31:19 +0530 Subject: [PATCH 144/178] fix: add tests for set_status --- .../doctype/purchase_invoice/purchase_invoice.py | 2 +- .../purchase_invoice/test_purchase_invoice.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 978898a54b2..b75c1701922 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1002,7 +1002,7 @@ class PurchaseInvoice(BuyingController): elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" #Check if outstanding amount is 0 due to debit note issued against invoice - elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): self.status = "Debit Note Issued" elif self.is_return == 1: self.status = "Return" diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e41ad428469..61700050614 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -86,6 +86,8 @@ class TestPurchaseInvoice(unittest.TestCase): pe.submit() pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name) + pi_doc.load_from_db() + self.assertTrue(pi_doc.status, "Paid") self.assertRaises(frappe.LinkExistsError, pi_doc.cancel) unlink_payment_on_cancel_of_invoice() @@ -203,7 +205,9 @@ class TestPurchaseInvoice(unittest.TestCase): pi.insert() pi.submit() + pi.load_from_db() + self.assertTrue(pi.status, "Unpaid") self.check_gle_for_pi(pi.name) def check_gle_for_pi(self, pi): @@ -234,6 +238,9 @@ class TestPurchaseInvoice(unittest.TestCase): pi = frappe.copy_doc(test_records[0]) pi.insert() + pi.load_from_db() + + self.assertTrue(pi.status, "Draft") pi.naming_series = 'TEST-' self.assertRaises(frappe.CannotChangeConstantError, pi.save) @@ -248,6 +255,8 @@ class TestPurchaseInvoice(unittest.TestCase): pi.get("taxes").pop(1) pi.insert() pi.submit() + pi.load_from_db() + self.assertTrue(pi.status, "Unpaid") gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s @@ -599,6 +608,11 @@ class TestPurchaseInvoice(unittest.TestCase): # return entry pi1 = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, rate=50, update_stock=1) + pi.load_from_db() + self.assertTrue(pi.status, "Debit Note Issued") + pi1.load_from_db() + self.assertTrue(pi1.status, "Return") + actual_qty_2 = get_qty_after_transaction() self.assertEqual(actual_qty_1 - 2, actual_qty_2) @@ -771,6 +785,8 @@ class TestPurchaseInvoice(unittest.TestCase): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) + pi.load_from_db() + self.assertTrue(pi.status, "Return") outstanding_amount = get_outstanding_amount(pi.doctype, pi.name, "Creditors - _TC", pi.supplier, "Supplier") From 725684a0c3857d9b60c75552ef8d0daee6bf4f24 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 15 May 2020 16:39:02 +0530 Subject: [PATCH 145/178] fix: update remark on submitting payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index da71acdb9da..ab7aa15e3dd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -448,8 +448,6 @@ class PaymentEntry(AccountsController): frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) def set_remarks(self): - if self.remarks: return - if self.payment_type=="Internal Transfer": remarks = [_("Amount {0} {1} transferred from {2} to {3}") .format(self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to)] From 52890341d569c1b0c1a6c5bc18d92125672dfd0a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 May 2020 17:15:16 +0530 Subject: [PATCH 146/178] fix: Get basic and hra component from db, not from cache (cherry picked from commit 10df3d5081c54d5ee70bdf69c81c3d70acb35169) --- erpnext/regional/india/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e518c14a8a0..d22aa1eebb8 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -236,8 +236,7 @@ def get_tax_template_for_sez(party_details, master_doctype, company, party_type) def calculate_annual_eligible_hra_exemption(doc): - basic_component = frappe.get_cached_value('Company', doc.company, "basic_component") - hra_component = frappe.get_cached_value('Company', doc.company, "hra_component") + basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"]) if not (basic_component and hra_component): frappe.throw(_("Please mention Basic and HRA component in Company")) annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 From 78f69e448b361d134a76aef663997d8490e702c7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 May 2020 17:18:29 +0530 Subject: [PATCH 147/178] fix: Added submit permission in employee other income (cherry picked from commit 200f80c3d368884000ebd8b5942e1e9ea28d0301) --- .../employee_other_income.json | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json index 2dd6c10988d..8abfe1e93a8 100644 --- a/erpnext/hr/doctype/employee_other_income/employee_other_income.json +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json @@ -76,25 +76,15 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-19 18:06:45.361830", + "modified": "2020-05-14 17:17:38.883126", "modified_by": "Administrator", "module": "HR", "name": "Employee Other Income", "owner": "Administrator", "permissions": [ { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -104,9 +94,12 @@ "report": 1, "role": "HR Manager", "share": 1, + "submit": 1, "write": 1 }, { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -116,9 +109,12 @@ "report": 1, "role": "HR User", "share": 1, + "submit": 1, "write": 1 }, { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -128,6 +124,7 @@ "report": 1, "role": "Employee", "share": 1, + "submit": 1, "write": 1 } ], From 79b691fe182470e58d9282bb5fdd252192ff96e4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 15 May 2020 19:23:16 +0530 Subject: [PATCH 148/178] fix: Better validation message for group accounts (#21726) --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 512e348c8bf..7358a31469b 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -115,8 +115,8 @@ class GLEntry(Document): from tabAccount where name=%s""", self.account, as_dict=1)[0] if ret.is_group==1: - frappe.throw(_("{0} {1}: Account {2} cannot be a Group") - .format(self.voucher_type, self.voucher_no, self.account)) + frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in + transactions''').format(self.voucher_type, self.voucher_no, self.account)) if ret.docstatus==2: frappe.throw(_("{0} {1}: Account {2} is inactive") From a52fe009cc2f63ab513d5432d6f8148add6ea8e1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 15 May 2020 19:35:35 +0530 Subject: [PATCH 149/178] fix: user not able to view product (#21697) --- erpnext/shopping_cart/product_info.py | 8 +++++--- erpnext/stock/doctype/item/item.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py index a7da09cb808..21ee335125b 100644 --- a/erpnext/shopping_cart/product_info.py +++ b/erpnext/shopping_cart/product_info.py @@ -10,14 +10,16 @@ from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status @frappe.whitelist(allow_guest=True) -def get_product_info_for_website(item_code): +def get_product_info_for_website(item_code, skip_quotation_creation=False): """get product price / stock info for website""" cart_settings = get_shopping_cart_settings() if not cart_settings.enabled: return frappe._dict() - cart_quotation = _get_cart_quotation() + cart_quotation = frappe._dict() + if not skip_quotation_creation: + cart_quotation = _get_cart_quotation() price = get_price( item_code, @@ -51,7 +53,7 @@ def get_product_info_for_website(item_code): def set_product_info_for_website(item): """set product price uom for website""" - product_info = get_product_info_for_website(item.item_code) + product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True) if product_info: item.update(product_info) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d7edd3ab51f..f5ffe242a1f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -467,7 +467,7 @@ class Item(WebsiteGenerator): def set_shopping_cart_data(self, context): from erpnext.shopping_cart.product_info import get_product_info_for_website - context.shopping_cart = get_product_info_for_website(self.name) + context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True) def add_default_uom_in_conversion_factor_table(self): uom_conv_list = [d.uom for d in self.get("uoms")] From 88c2ba54abc4357e4cff623447e8fad6d140b5a1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 23 Feb 2020 21:03:47 +0530 Subject: [PATCH 150/178] fix: Excel support and UX fixes for chart of accounts importer --- .../chart_of_accounts_importer.js | 58 ++-- .../chart_of_accounts_importer.json | 267 ++++-------------- .../chart_of_accounts_importer.py | 166 +++++++++-- 3 files changed, 235 insertions(+), 256 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 40a97ae295e..93a4180a30a 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -2,6 +2,12 @@ frappe.ui.form.on('Chart of Accounts Importer', { onload: function (frm) { frm.set_value("company", ""); frm.set_value("import_file", ""); + + frappe.db.get_value('File', {'attached_to_doctype': 'Chart of Accounts Importer'}, 'file_url', (r) => { + if (r) { + frm.set_value('import_file', r.file_url); + } + }); }, refresh: function (frm) { // disable default save @@ -17,17 +23,15 @@ frappe.ui.form.on('Chart of Accounts Importer', { if (frm.page && frm.page.show_import_button) { create_import_button(frm); } + }, - // show download template button when company is properly selected - if(frm.doc.company) { - // download the csv template file - frm.add_custom_button(__("Download template"), function () { - let get_template_url = 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template'; - open_url_post(frappe.request.url, { cmd: get_template_url, doctype: frm.doc.doctype }); - }); - } else { - frm.set_value("import_file", ""); - } + download_template: function(frm) { + open_url_post( + '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', + { + file_type: frm.doc.file_type + } + ) }, import_file: function (frm) { @@ -41,21 +45,23 @@ frappe.ui.form.on('Chart of Accounts Importer', { }, company: function (frm) { - // validate that no Gl Entry record for the company exists. - frappe.call({ - method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company", - args: { - company: frm.doc.company - }, - callback: function(r) { - if(r.message===false) { - frm.set_value("company", ""); - frappe.throw(__("Transactions against the company already exist! ")); - } else { - frm.trigger("refresh"); + if (frm.doc.company) { + // validate that no Gl Entry record for the company exists. + frappe.call({ + method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_company", + args: { + company: frm.doc.company + }, + callback: function(r) { + if(r.message===false) { + frm.set_value("company", ""); + frappe.throw(__("Transactions against the company already exist! ")); + } else { + frm.trigger("refresh"); + } } - } - }); + }); + } } }); @@ -87,6 +93,7 @@ var create_import_button = function(frm) { freeze: true, freeze_message: __("Creating Accounts..."), callback: function(r) { + console.log(r, '###########'); if(!r.exc) { clearInterval(frm.page["interval"]); frm.page.set_indicator(__('Import Successfull'), 'blue'); @@ -118,7 +125,8 @@ var generate_tree_preview = function(frm) { args: { file_name: frm.doc.import_file, parent: parent, - doctype: 'Chart of Accounts Importer' + doctype: 'Chart of Accounts Importer', + file_type: frm.doc.file_type }, onclick: function(node) { parent = node.value; diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json index d544e69231e..555413b31fa 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json @@ -1,226 +1,83 @@ { - "allow_copy": 1, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2019-02-01 12:24:34.761380", - "custom": 0, + "actions": [], + "allow_copy": 1, + "creation": "2019-02-01 12:24:34.761380", "description": "Import Chart of Accounts from a csv file", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 1, - "engine": "InnoDB", + "doctype": "DocType", + "document_type": "Other", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "file_type", + "download_template", + "import_file_section", + "import_file", + "chart_preview", + "chart_tree" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "import_file_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", + "depends_on": "eval:!doc.__islocal", "fieldname": "import_file", "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Attach custom Chart of Accounts file", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "label": "Attach custom Chart of Accounts file" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "fieldname": "chart_preview", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Chart Preview", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldtype": "Section Break", + "label": "Chart Preview" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "chart_tree", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Chart Tree", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Chart Tree" + }, + { + "depends_on": "company", + "fieldname": "download_template", + "fieldtype": "Button", + "label": "Download Template" + }, + { + "depends_on": "company", + "fieldname": "file_type", + "fieldtype": "Select", + "label": "File Type", + "options": "CSV\nExcel" } - ], - "has_web_view": 0, - "hide_heading": 1, - "hide_toolbar": 1, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-02-04 23:10:30.136807", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Chart of Accounts Importer", - "name_case": "", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "in_create": 1, + "issingle": 1, + "links": [], + "modified": "2020-02-22 19:32:33.349075", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Chart of Accounts Importer", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "read_only": 1, + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index f496280317a..18fb75a24cd 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -4,18 +4,28 @@ from __future__ import unicode_literals from functools import reduce -import frappe, csv +import frappe, csv, os from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, cint from frappe.model.document import Document from frappe.utils.csvutils import UnicodeWriter from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts, build_tree_from_json +from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file class ChartofAccountsImporter(Document): pass @frappe.whitelist() def validate_company(company): + parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company', + {'name': company}, ['parent_company', + 'allow_account_creation_against_child_company']) + + if parent_company and (not allow_account_creation_against_child_company): + frappe.throw(_("""{0} is child company. Please import accounts in parent company + or enable {1} in company master""").format(frappe.bold(company), + frappe.bold('Allow Account Creation Against Child Company'))) + if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1): return False @@ -25,42 +35,85 @@ def import_coa(file_name, company): unset_existing_data(company) # create accounts - forest = build_forest(generate_data_from_csv(file_name)) + file_doc, extension = get_file(file_name) + + if extension == 'csv': + data = generate_data_from_csv(file_doc) + else: + data = generate_data_from_excel(file_doc, extension) + + forest = build_forest(data) create_charts(company, custom_chart=forest) # trigger on_update for company to reset default accounts set_default_accounts(company) -def generate_data_from_csv(file_name, as_dict=False): - ''' read csv file and return the generated nested tree ''' - if not file_name.endswith('.csv'): - frappe.throw("Only CSV files can be used to for importing data. Please check the file format you are trying to upload") +def get_file(file_name): + file_doc = frappe.get_doc("File", {"file_url": file_name}) + parts = file_doc.get_extension() + extension = parts[1] + extension = extension.lstrip(".") + + if extension not in ('csv', 'xlsx', 'xls'): + frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload") + + return file_doc, extension + +def generate_data_from_csv(file_doc, as_dict=False): + ''' read csv file and return the generated nested tree ''' - file_doc = frappe.get_doc('File', {"file_url": file_name}) file_path = file_doc.get_full_path() data = [] with open(file_path, 'r') as in_file: csv_reader = list(csv.reader(in_file)) - headers = csv_reader[1][1:] - del csv_reader[0:2] # delete top row and headers row + headers = csv_reader[0] + del csv_reader[0] # delete top row and headers row for row in csv_reader: if as_dict: - data.append({frappe.scrub(header): row[index+1] for index, header in enumerate(headers)}) + data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) else: - if not row[2]: row[2] = row[1] - data.append(row[1:]) + if not row[1]: row[1] = row[0] + data.append(row) # convert csv data return data +def generate_data_from_excel(file_doc, extension, as_dict=False): + content = file_doc.get_content() + + if extension == "xlsx": + rows = read_xlsx_file_from_attached_file(fcontent=content) + elif extension == "xls": + rows = read_xls_file_from_attached_file(content) + + data = [] + headers = rows[0] + del rows[0] + + for row in rows: + if as_dict: + data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) + else: + if not row[1]: row[1] = row[0] + data.append(row) + + return data + @frappe.whitelist() -def get_coa(doctype, parent, is_root=False, file_name=None): +def get_coa(doctype, parent, file_type, is_root=False, file_name=None): ''' called by tree view (to fetch node's children) ''' + file_doc, extension = get_file(file_name) parent = None if parent==_('All Accounts') else parent - forest = build_forest(generate_data_from_csv(file_name)) + + if extension == 'csv': + data = generate_data_from_csv(file_doc) + else: + data = generate_data_from_excel(file_doc, extension) + + forest = build_forest(data) accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form # filter out to show data for the selected node only @@ -114,7 +167,7 @@ def build_forest(data): error_messages.append("Row {0}: Please enter Account Name".format(line_no)) charts_map[account_name] = {} - if is_group == 1: charts_map[account_name]["is_group"] = is_group + if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group if account_type: charts_map[account_name]["account_type"] = account_type if root_type: charts_map[account_name]["root_type"] = root_type if account_number: charts_map[account_name]["account_number"] = account_number @@ -132,24 +185,59 @@ def build_forest(data): return out +def build_response_as_excel(writer): + filename = frappe.generate_hash("", 10) + with open(filename, 'wb') as f: + f.write(cstr(writer.getvalue()).encode('utf-8')) + f = open(filename) + reader = csv.reader(f) + + from frappe.utils.xlsxutils import make_xlsx + xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template") + + f.close() + os.remove(filename) + + # write out response as a xlsx type + frappe.response['filename'] = 'coa_importer_template.xlsx' + frappe.response['filecontent'] = xlsx_file.getvalue() + frappe.response['type'] = 'binary' + @frappe.whitelist() -def download_template(): +def download_template(file_type): data = frappe._dict(frappe.local.form_dict) + fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"] writer = UnicodeWriter() - writer.writerow([_('Chart of Accounts Template')]) - writer.writerow([_("Column Labels : ")] + fields) - writer.writerow([_("Start entering data from here : ")]) + writer.writerow(fields) - # download csv file - frappe.response['result'] = cstr(writer.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = data.get('doctype') + for root_type in get_root_types(): + writer.writerow(['', '', '', 1, '', root_type]) + + for account in get_mandatory_group_accounts(): + writer.writerow(['', '', '', 1, account, 'Asset']) + + for account_type in get_mandatory_account_types(): + writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')]) + + if file_type == 'CSV': + # download csv file + frappe.response['result'] = cstr(writer.getvalue()) + frappe.response['type'] = 'csv' + frappe.response['doctype'] = 'Chart of Accounts Importer' + else: + build_response_as_excel(writer) @frappe.whitelist() def validate_accounts(file_name): - accounts = generate_data_from_csv(file_name, as_dict=True) + + file_doc, extension = get_file(file_name) + + if extension == 'csv': + accounts = generate_data_from_csv(file_doc, as_dict=True) + else: + accounts = generate_data_from_excel(file_doc, extension, as_dict=True) accounts_dict = {} for account in accounts: @@ -174,12 +262,38 @@ def validate_root(accounts): for account in roots: if not account.get("root_type") and account.get("account_name"): error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) - elif account.get("root_type") not in ("Asset", "Liability", "Expense", "Income", "Equity") and account.get("account_name"): + elif account.get("root_type") not in get_root_types() and account.get("account_name"): error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) if error_messages: return "
    ".join(error_messages) +def get_root_types(): + return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') + +def get_report_type(root_type): + if root_type in ('Asset', 'Liability', 'Equity'): + return 'Balance Sheet' + else: + return 'Profit and Loss' + +def get_mandatory_group_accounts(): + return ('Bank', 'Cash', 'Stock') + +def get_mandatory_account_types(): + return [ + {'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'}, + {'account_type': 'Depreciation', 'root_type': 'Expense'}, + {'account_type': 'Fixed Asset', 'root_type': 'Asset'}, + {'account_type': 'Payable', 'root_type': 'Liability'}, + {'account_type': 'Receivable', 'root_type': 'Asset'}, + {'account_type': 'Stock Adjustment', 'root_type': 'Expense'}, + {'account_type': 'Bank', 'root_type': 'Asset'}, + {'account_type': 'Cash', 'root_type': 'Asset'}, + {'account_type': 'Stock', 'root_type': 'Asset'} + ] + + def validate_account_types(accounts): account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"] account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1] From 8ff718249eff94fae5741a49086475be887d817a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 24 Feb 2020 12:06:25 +0530 Subject: [PATCH 151/178] fix: Linting Errors --- .../chart_of_accounts_importer/chart_of_accounts_importer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 93a4180a30a..92358c9af27 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Chart of Accounts Importer', { { file_type: frm.doc.file_type } - ) + ); }, import_file: function (frm) { @@ -93,7 +93,6 @@ var create_import_button = function(frm) { freeze: true, freeze_message: __("Creating Accounts..."), callback: function(r) { - console.log(r, '###########'); if(!r.exc) { clearInterval(frm.page["interval"]); frm.page.set_indicator(__('Import Successfull'), 'blue'); From 838ed777977b43334e5b0de10bf73a29b669cd9d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 24 Feb 2020 17:36:15 +0530 Subject: [PATCH 152/178] fix: Blank chart preview --- .../chart_of_accounts_importer.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 92358c9af27..189b2f9210f 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -2,12 +2,6 @@ frappe.ui.form.on('Chart of Accounts Importer', { onload: function (frm) { frm.set_value("company", ""); frm.set_value("import_file", ""); - - frappe.db.get_value('File', {'attached_to_doctype': 'Chart of Accounts Importer'}, 'file_url', (r) => { - if (r) { - frm.set_value('import_file', r.file_url); - } - }); }, refresh: function (frm) { // disable default save From 101612e5990634148dc890bfa14dba3ebec5e79f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Feb 2020 15:07:05 +0530 Subject: [PATCH 153/178] fix: Added template types for download --- .../chart_of_accounts_importer.json | 11 +++- .../chart_of_accounts_importer.py | 64 +++++++++++++++---- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json index 555413b31fa..b4c4fce3511 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json @@ -10,6 +10,7 @@ "field_order": [ "company", "file_type", + "template_type", "download_template", "import_file_section", "import_file", @@ -57,13 +58,21 @@ "fieldtype": "Select", "label": "File Type", "options": "CSV\nExcel" + }, + { + "depends_on": "company", + "fieldname": "template_type", + "fieldtype": "Select", + "label": "Template Type", + "options": "\nBlank Template\nSample Template", + "reqd": 1 } ], "hide_toolbar": 1, "in_create": 1, "issingle": 1, "links": [], - "modified": "2020-02-22 19:32:33.349075", + "modified": "2020-02-24 19:07:52.670868", "modified_by": "Administrator", "module": "Accounts", "name": "Chart of Accounts Importer", diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 18fb75a24cd..12e147192cb 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -204,22 +204,10 @@ def build_response_as_excel(writer): frappe.response['type'] = 'binary' @frappe.whitelist() -def download_template(file_type): +def download_template(file_type, template_type): data = frappe._dict(frappe.local.form_dict) - fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"] - writer = UnicodeWriter() - - writer.writerow(fields) - - for root_type in get_root_types(): - writer.writerow(['', '', '', 1, '', root_type]) - - for account in get_mandatory_group_accounts(): - writer.writerow(['', '', '', 1, account, 'Asset']) - - for account_type in get_mandatory_account_types(): - writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')]) + writer = get_template(template_type) if file_type == 'CSV': # download csv file @@ -229,6 +217,54 @@ def download_template(file_type): else: build_response_as_excel(writer) +def get_template(template_type): + + fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"] + writer = UnicodeWriter() + writer.writerow(fields) + + if template_type == 'Blank Template': + for root_type in get_root_types(): + writer.writerow(['', '', '', 1, '', root_type]) + + for account in get_mandatory_group_accounts(): + writer.writerow(['', '', '', 1, account, 'Asset']) + + for account_type in get_mandatory_account_types(): + writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')]) + else: + writer = get_sample_template(writer) + + return writer + +def get_sample_template(writer): + template = [ + ['Account Name', 'Parent Account', 'Account Number', 'Is Group', 'Account Type', 'Root Type'], + ['Application Of Funds(Assets)', '', '', 1, '', 'Asset'], + ['Sources Of Funds(Liabilities)', '', '', 1, '', 'Liability'], + ['Equity', '', '', 1, '', 'Equity'], + ['Expenses', '', '', 1, '', 'Expense'], + ['Income', '', '', 1, '', 'Income'], + ['Bank Accounts', 'Application Of Funds(Assets)', '', 1, 'Bank', 'Asset'], + ['Cash In Hand', 'Application Of Funds(Assets)', '', 1, 'Cash', 'Asset'], + ['Stock Assets', 'Application Of Funds(Assets)', '', 1, 'Stock', 'Asset'], + ['Cost Of Goods Sold', 'Expenses', '', 0, 'Cost of Goods Sold', 'Expense'], + ['Asset Depreciation', 'Expenses', '', 0, 'Depreciation', 'Expense'], + ['Fixed Assets', 'Application Of Funds(Assets)', '', 0, 'Fixed Asset', 'Asset'], + ['Accounts Payable', 'Sources Of Funds(Liabilities)', '', 0, 'Payable', 'Liability'], + ['Accounts Receivable', 'Application Of Funds(Assets)', '', 1, 'Receivable', 'Asset'], + ['Stock Expenses', 'Expenses', '', 0, 'Stock Adjustment', 'Expense'], + ['Sample Bank', 'Bank Accounts', '', 0, 'Bank', 'Asset'], + ['Cash', 'Cash In Hand', '', 0, 'Cash', 'Asset'], + ['Stores', 'Stock Asset', '', 0, 'Stock', 'Asset'], + ] + + for row in template: + writer.writerow(row) + + return writer + + @frappe.whitelist() def validate_accounts(file_name): From 317b53a8b68a9c4ea0b52ea5e08e69ac6022d5ac Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Feb 2020 15:07:31 +0530 Subject: [PATCH 154/178] fix: Description on template selection --- .../chart_of_accounts_importer.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 189b2f9210f..049c15d6650 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -20,14 +20,32 @@ frappe.ui.form.on('Chart of Accounts Importer', { }, download_template: function(frm) { + + if (!frm.doc.template_type) { + frappe.throw(__('Please select Template Type to download template')); + } + open_url_post( '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', { - file_type: frm.doc.file_type + file_type: frm.doc.file_type, + template_type: frm.doc.template_type } ); }, + template_type: function(frm) { + if (frm.doc.template_type == 'Sample Template') { + frm.set_df_property('template_type', 'description', + `The Sample Template contains all the required accounts pre filled in the template. + You can add more accounts or change existing accounts in the template as per your choice.`) + } else { + frm.set_df_property('template_type', 'description', + `The Blank Template contains just the account type and root type required to build the Chart + of Accounts. Please enter the account names and add more rows as per your requirement.`) + } + }, + import_file: function (frm) { if (!frm.doc.import_file) { frm.page.set_indicator(""); @@ -49,7 +67,8 @@ frappe.ui.form.on('Chart of Accounts Importer', { callback: function(r) { if(r.message===false) { frm.set_value("company", ""); - frappe.throw(__("Transactions against the company already exist! ")); + frappe.throw(__(`Transactions against the company already exist! + Chart Of accounts can be imported for company with no transactions`)); } else { frm.trigger("refresh"); } From 39790f05e7c1ec752d9c499a041a6a52ff500b95 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Feb 2020 17:11:33 +0530 Subject: [PATCH 155/178] fix: Linting fixes --- .../chart_of_accounts_importer/chart_of_accounts_importer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 049c15d6650..c310e26a656 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -38,11 +38,11 @@ frappe.ui.form.on('Chart of Accounts Importer', { if (frm.doc.template_type == 'Sample Template') { frm.set_df_property('template_type', 'description', `The Sample Template contains all the required accounts pre filled in the template. - You can add more accounts or change existing accounts in the template as per your choice.`) + You can add more accounts or change existing accounts in the template as per your choice.`); } else { frm.set_df_property('template_type', 'description', `The Blank Template contains just the account type and root type required to build the Chart - of Accounts. Please enter the account names and add more rows as per your requirement.`) + of Accounts. Please enter the account names and add more rows as per your requirement.`); } }, From ec19926a97d0967dca0c9ccecc020c52ca5a15bd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 29 Feb 2020 20:23:39 +0530 Subject: [PATCH 156/178] Move logic for download template to dialog --- .../chart_of_accounts_importer.js | 71 +++++++++++++------ .../chart_of_accounts_importer.json | 27 +------ .../chart_of_accounts_importer.py | 52 +++++++------- 3 files changed, 79 insertions(+), 71 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index c310e26a656..0b7cff3d63c 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -20,30 +20,57 @@ frappe.ui.form.on('Chart of Accounts Importer', { }, download_template: function(frm) { + var d = new frappe.ui.Dialog({ + title: __("Download Template"), + fields: [ + { + label : "File Type", + fieldname: "file_type", + fieldtype: "Select", + reqd: 1, + options: ["Excel", "CSV"] + }, + { + label: "Template Type", + fieldname: "template_type", + fieldtype: "Select", + reqd: 1, + options: ["Sample Template", "Blank Template"], + change: () => { + let template_type = d.get_value('template_type'); - if (!frm.doc.template_type) { - frappe.throw(__('Please select Template Type to download template')); - } + if (template_type === "Sample Template") { + d.set_df_property('template_type', 'description', + `The Sample Template contains all the required accounts pre filled in the template. + You can add more accounts or change existing accounts in the template as per your choice.`); + } else { + d.set_df_property('template_type', 'description', + `The Blank Template contains just the account type and root type required to build the Chart + of Accounts. Please enter the account names and add more rows as per your requirement.`); + } + } + } + ], + primary_action: function() { + var data = d.get_values(); - open_url_post( - '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', - { - file_type: frm.doc.file_type, - template_type: frm.doc.template_type - } - ); - }, + if (!data.template_type) { + frappe.throw(__('Please select Template Type to download template')); + } - template_type: function(frm) { - if (frm.doc.template_type == 'Sample Template') { - frm.set_df_property('template_type', 'description', - `The Sample Template contains all the required accounts pre filled in the template. - You can add more accounts or change existing accounts in the template as per your choice.`); - } else { - frm.set_df_property('template_type', 'description', - `The Blank Template contains just the account type and root type required to build the Chart - of Accounts. Please enter the account names and add more rows as per your requirement.`); - } + open_url_post( + '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', + { + file_type: data.file_type, + template_type: data.template_type + } + ); + + d.hide(); + }, + primary_action_label: __('Download') + }); + d.show(); }, import_file: function (frm) { @@ -96,7 +123,7 @@ var validate_csv_data = function(frm) { }; var create_import_button = function(frm) { - frm.page.set_primary_action(__("Start Import"), function () { + frm.page.set_primary_action(__("Import"), function () { frappe.call({ method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa", args: { diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json index b4c4fce3511..ee095ac386a 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.json @@ -9,10 +9,7 @@ "engine": "InnoDB", "field_order": [ "company", - "file_type", - "template_type", "download_template", - "import_file_section", "import_file", "chart_preview", "chart_tree" @@ -26,11 +23,7 @@ "options": "Company" }, { - "fieldname": "import_file_section", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval:!doc.__islocal", + "depends_on": "company", "fieldname": "import_file", "fieldtype": "Attach", "label": "Attach custom Chart of Accounts file" @@ -51,28 +44,13 @@ "fieldname": "download_template", "fieldtype": "Button", "label": "Download Template" - }, - { - "depends_on": "company", - "fieldname": "file_type", - "fieldtype": "Select", - "label": "File Type", - "options": "CSV\nExcel" - }, - { - "depends_on": "company", - "fieldname": "template_type", - "fieldtype": "Select", - "label": "Template Type", - "options": "\nBlank Template\nSample Template", - "reqd": 1 } ], "hide_toolbar": 1, "in_create": 1, "issingle": 1, "links": [], - "modified": "2020-02-24 19:07:52.670868", + "modified": "2020-02-28 08:49:11.422846", "modified_by": "Administrator", "module": "Accounts", "name": "Chart of Accounts Importer", @@ -88,5 +66,6 @@ ], "quick_entry": 1, "read_only": 1, + "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 12e147192cb..4100699fe39 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -22,9 +22,9 @@ def validate_company(company): 'allow_account_creation_against_child_company']) if parent_company and (not allow_account_creation_against_child_company): - frappe.throw(_("""{0} is child company. Please import accounts in parent company + frappe.throw(_("""{0} is a child company. Please import accounts against parent company or enable {1} in company master""").format(frappe.bold(company), - frappe.bold('Allow Account Creation Against Child Company'))) + frappe.bold('Allow Account Creation Against Child Company')), title='Wrong Company') if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1): return False @@ -102,7 +102,7 @@ def generate_data_from_excel(file_doc, extension, as_dict=False): return data @frappe.whitelist() -def get_coa(doctype, parent, file_type, is_root=False, file_name=None): +def get_coa(doctype, parent, is_root=False, file_name=None): ''' called by tree view (to fetch node's children) ''' file_doc, extension = get_file(file_name) @@ -144,15 +144,18 @@ def build_forest(data): # returns the path of any node in list format def return_parent(data, child): + from frappe import _ + for row in data: account_name, parent_account = row[0:2] if parent_account == account_name == child: return [parent_account] elif account_name == child: parent_account_list = return_parent(data, parent_account) - if not parent_account_list and parent_account: - frappe.throw(_("The parent account {0} does not exists") - .format(parent_account)) + if not parent_account_list: + frappe.throw(_("The parent account {0} does not exists in the uploaded template").format( + frappe.bold(parent_account))) + return [child] + parent_account_list charts_map, paths = {}, [] @@ -228,7 +231,7 @@ def get_template(template_type): writer.writerow(['', '', '', 1, '', root_type]) for account in get_mandatory_group_accounts(): - writer.writerow(['', '', '', 1, account, 'Asset']) + writer.writerow(['', '', '', 1, account, "Asset"]) for account_type in get_mandatory_account_types(): writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')]) @@ -239,24 +242,23 @@ def get_template(template_type): def get_sample_template(writer): template = [ - ['Account Name', 'Parent Account', 'Account Number', 'Is Group', 'Account Type', 'Root Type'], - ['Application Of Funds(Assets)', '', '', 1, '', 'Asset'], - ['Sources Of Funds(Liabilities)', '', '', 1, '', 'Liability'], - ['Equity', '', '', 1, '', 'Equity'], - ['Expenses', '', '', 1, '', 'Expense'], - ['Income', '', '', 1, '', 'Income'], - ['Bank Accounts', 'Application Of Funds(Assets)', '', 1, 'Bank', 'Asset'], - ['Cash In Hand', 'Application Of Funds(Assets)', '', 1, 'Cash', 'Asset'], - ['Stock Assets', 'Application Of Funds(Assets)', '', 1, 'Stock', 'Asset'], - ['Cost Of Goods Sold', 'Expenses', '', 0, 'Cost of Goods Sold', 'Expense'], - ['Asset Depreciation', 'Expenses', '', 0, 'Depreciation', 'Expense'], - ['Fixed Assets', 'Application Of Funds(Assets)', '', 0, 'Fixed Asset', 'Asset'], - ['Accounts Payable', 'Sources Of Funds(Liabilities)', '', 0, 'Payable', 'Liability'], - ['Accounts Receivable', 'Application Of Funds(Assets)', '', 1, 'Receivable', 'Asset'], - ['Stock Expenses', 'Expenses', '', 0, 'Stock Adjustment', 'Expense'], - ['Sample Bank', 'Bank Accounts', '', 0, 'Bank', 'Asset'], - ['Cash', 'Cash In Hand', '', 0, 'Cash', 'Asset'], - ['Stores', 'Stock Asset', '', 0, 'Stock', 'Asset'], + ["Application Of Funds(Assets)", "", "", 1, "", "Asset"], + ["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"], + ["Equity", "", "", 1, "", "Equity"], + ["Expenses", "", "", 1, "", "Expense"], + ["Income", "", "", 1, "", "Income"], + ["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"], + ["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"], + ["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"], + ["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"], + ["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"], + ["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"], + ["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"], + ["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"], + ["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"], + ["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"], + ["Cash", "Cash In Hand", "", 0, "Cash", "Asset"], + ["Stores", "Stock Assets", "", 0, "Stock", "Asset"], ] for row in template: From e2f53cdc83ef67f1f77311a3b166788d0dbf5e95 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 13 May 2020 16:11:22 +0530 Subject: [PATCH 157/178] fix: error log title for failing bank transactions (cherry picked from commit 4a9fd9ef6d5e34eb6f04deb0423c93f33f0b3028) --- .../doctype/plaid_settings/plaid_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 970793f7468..c175eaaa251 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -209,7 +209,7 @@ def new_bank_transaction(transaction): result.append(new_transaction.name) except Exception: - frappe.throw(frappe.get_traceback()) + frappe.throw(title=_('Bank transaction creation error')) return result From 3e321d5c24c07d33e47beddbf730c35c98e451c6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 16 May 2020 04:59:43 +0530 Subject: [PATCH 158/178] fix: promotional scheme not able to savce --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 086f42b293f..983f1ef85a0 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -103,7 +103,7 @@ class PricingRule(Document): self.same_item = 1 def validate_max_discount(self): - if self.rate_or_discount == "Discount Percentage" and self.items: + if self.rate_or_discount == "Discount Percentage" and self.get("items"): for d in self.items: max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount") if max_discount and flt(self.discount_percentage) > flt(max_discount): From 1045dc49f58cac1e91262a9d7d3ea8bb0986eec2 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Sun, 17 May 2020 20:17:39 +0530 Subject: [PATCH 159/178] fix: Future date half day validation (#21719) * fix: Future date half day validation * fix: Allow half day attendance only via leave application Co-authored-by: Nabin Hait --- erpnext/hr/doctype/attendance/attendance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index e612c3d7572..f23b4220849 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -38,7 +38,7 @@ class Attendance(Document): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") # leaves can be marked for future dates - if self.status not in ('On Leave', 'Half Day') and getdate(self.attendance_date) > getdate(nowdate()): + if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()): frappe.throw(_("Attendance can not be marked for future dates")) elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) From 7f2aedb67c1a4d01b16490c39096f845df84bbb5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 17 May 2020 20:28:21 +0530 Subject: [PATCH 160/178] fix: incorrect stock valuation for repack entry (#21737) --- .../stock/doctype/stock_entry/stock_entry.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index eb3e3e9af4f..66145c650be 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -360,6 +360,9 @@ class StockEntry(StockController): + self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError) def set_incoming_rate(self): + if self.purpose == "Repack": + self.set_basic_rate_for_finished_goods() + for d in self.items: if d.s_warehouse: args = self.get_args_for_incoming_rate(d) @@ -472,20 +475,31 @@ class StockEntry(StockController): "allow_zero_valuation": item.allow_zero_valuation_rate, }) - def set_basic_rate_for_finished_goods(self, raw_material_cost, scrap_material_cost): + def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0): + total_fg_qty = 0 + if not raw_material_cost and self.get("items"): + raw_material_cost = sum([flt(row.basic_amount) for row in self.items + if row.s_warehouse and not row.t_warehouse]) + + total_fg_qty = sum([flt(row.qty) for row in self.items + if row.t_warehouse and not row.s_warehouse]) + if self.purpose in ["Manufacture", "Repack"]: for d in self.get("items"): if (d.transfer_qty and (d.bom_no or d.t_warehouse) and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)): - if self.work_order \ - and frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if (self.work_order and self.purpose == "Manufacture" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")): bom_items = self.get_bom_raw_materials(d.transfer_qty) raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) - if raw_material_cost: + if raw_material_cost and self.purpose == "Manufacture": d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate")) d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) + elif self.purpose == "Repack" and total_fg_qty: + d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) + d.basic_amount = d.basic_rate * d.qty def distribute_additional_costs(self): if self.purpose == "Material Issue": From 9b082589551b8462cb4f8fd6d2b670b94cea6666 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sun, 17 May 2020 17:27:51 +0200 Subject: [PATCH 161/178] fix: remove guest access (#21692) --- erpnext/crm/doctype/lead/lead.json | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index eb68c679ba5..d0565d19b8d 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -366,7 +366,7 @@ "icon": "fa fa-user", "idx": 5, "image_field": "image", - "modified": "2019-09-19 12:49:02.536647", + "modified": "2020-05-11 20:30:02.536647", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -423,15 +423,6 @@ "read": 1, "report": 1, "role": "Sales User" - }, - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 } ], "search_fields": "lead_name,lead_owner,status", @@ -439,4 +430,4 @@ "sort_field": "modified", "sort_order": "DESC", "title_field": "lead_name" -} \ No newline at end of file +} From 395da09daeca3b5a9ec8d6ea330d8ec49229cfe7 Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 18 May 2020 11:18:56 +0530 Subject: [PATCH 162/178] fix: Patch to set status in old serial no data (#21721) * fix: Patch to set status in old serial no data * fix: Avoid get_doc in patch * fix: fetch all values and check status in one query --- erpnext/patches.txt | 1 + erpnext/patches/v12_0/set_serial_no_status.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 erpnext/patches/v12_0/set_serial_no_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5517cbc9a80..97a45692fd1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -665,3 +665,4 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price +erpnext.patches.v12_0.set_serial_no_status diff --git a/erpnext/patches/v12_0/set_serial_no_status.py b/erpnext/patches/v12_0/set_serial_no_status.py new file mode 100644 index 00000000000..4ec84ef0f9e --- /dev/null +++ b/erpnext/patches/v12_0/set_serial_no_status.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, nowdate + +def execute(): + frappe.reload_doc('stock', 'doctype', 'serial_no') + + for serial_no in frappe.db.sql("""select name, delivery_document_type, warranty_expiry_date from `tabSerial No` + where (status is NULL OR status='')""", as_dict = 1): + if serial_no.get("delivery_document_type"): + status = "Delivered" + elif serial_no.get("warranty_expiry_date") and getdate(serial_no.get("warranty_expiry_date")) <= getdate(nowdate()): + status = "Expired" + else: + status = "Active" + + frappe.db.set_value("Serial No", serial_no.get("name"), "status", status) \ No newline at end of file From ea4c91f51cecb603ded9f3b7981f093be47d2b53 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 18 May 2020 14:22:42 +0530 Subject: [PATCH 163/178] fix: bom incorrect price list rate for raw material if price list currency is different from company currency (#21586) * fix: bom incorrect price list rate for raw material if price list currency is different from company currency * fixed test cases * fixed base_rate calculation and added plc_conversion_rate trigger --- erpnext/manufacturing/doctype/bom/bom.js | 21 ++++++++- erpnext/manufacturing/doctype/bom/bom.json | 44 ++++++++++++++----- erpnext/manufacturing/doctype/bom/bom.py | 20 +++++++-- erpnext/manufacturing/doctype/bom/test_bom.py | 8 ++-- erpnext/patches.txt | 1 + .../update_price_list_currency_in_bom.py | 31 +++++++++++++ 6 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 erpnext/patches/v12_0/update_price_list_currency_in_bom.py diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 4f08bbc3fc7..cfcab7aea68 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -175,6 +175,12 @@ frappe.ui.form.on("BOM", { }); }, + rm_cost_as_per: function(frm) { + if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { + frm.set_value("plc_conversion_rate", 1.0); + } + }, + routing: function(frm) { if (frm.doc.routing) { frappe.call({ @@ -205,7 +211,7 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ item_code: function(doc, cdt, cdn){ var scrap_items = false; var child = locals[cdt][cdn]; - if(child.doctype == 'BOM Scrap Item') { + if (child.doctype == 'BOM Scrap Item') { scrap_items = true; } @@ -215,8 +221,19 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ get_bom_material_detail(doc, cdt, cdn, scrap_items); }, + + buying_price_list: function(doc) { + this.apply_price_list(); + }, + + plc_conversion_rate: function(doc) { + if (!this.in_apply_price_list) { + this.apply_price_list(); + } + }, + conversion_factor: function(doc, cdt, cdn) { - if(frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 63f4f977c59..4ce0ecf3f27 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2013-01-22 15:11:38", "doctype": "DocType", @@ -6,23 +7,25 @@ "engine": "InnoDB", "field_order": [ "item", - "quantity", - "set_rate_of_sub_assembly_item_based_on_bom", + "company", + "item_name", + "uom", "cb0", "is_active", "is_default", "allow_alternative_item", - "image", - "item_name", - "uom", - "currency_detail", - "company", + "set_rate_of_sub_assembly_item_based_on_bom", "project", + "quantity", + "image", + "currency_detail", + "currency", "conversion_rate", "column_break_12", - "currency", "rm_cost_as_per", "buying_price_list", + "price_list_currency", + "plc_conversion_rate", "section_break_21", "with_operations", "column_break_23", @@ -176,7 +179,8 @@ }, { "fieldname": "currency_detail", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Currency and Price List" }, { "fieldname": "company", @@ -324,7 +328,7 @@ }, { "fieldname": "base_scrap_material_cost", - "fieldtype": "Data", + "fieldtype": "Currency", "label": "Scrap Material Cost(Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -477,13 +481,31 @@ { "fieldname": "column_break_52", "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-sitemap", "idx": 1, "image_field": "image", "is_submittable": 1, - "modified": "2019-11-22 14:35:12.142150", + "links": [], + "modified": "2020-05-05 14:29:32.634952", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c898d378c3a..d11077270aa 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -55,11 +55,13 @@ class BOM(WebsiteGenerator): self.validate_main_item() self.validate_currency() self.set_conversion_rate() + self.set_plc_conversion_rate() self.validate_uom_is_interger() self.set_bom_material_details() self.validate_materials() self.validate_operations() self.calculate_cost() + self.update_cost(update_parent=False, from_child_bom=True, save=False) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -155,7 +157,7 @@ class BOM(WebsiteGenerator): 'rate' : rate, 'qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, - 'base_rate' : rate, + 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0 } @@ -216,7 +218,7 @@ class BOM(WebsiteGenerator): frappe.msgprint(_("{0} not found for item {1}") .format(self.rm_cost_as_per, arg["item_code"]), alert=True) - return flt(rate) / (self.conversion_rate or 1) + return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) def update_cost(self, update_parent=True, from_child_bom=False, save=True): if self.docstatus == 2: @@ -233,10 +235,15 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor }) + if rate: d.rate = rate d.amount = flt(d.rate) * flt(d.qty) - d.db_update() + d.base_rate = flt(d.rate) * flt(self.conversion_rate) + d.base_amount = flt(d.amount) * flt(self.conversion_rate) + + if save: + d.db_update() if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True @@ -362,6 +369,13 @@ class BOM(WebsiteGenerator): elif self.conversion_rate == 1 or flt(self.conversion_rate) <= 0: self.conversion_rate = get_exchange_rate(self.currency, self.company_currency(), args="for_buying") + def set_plc_conversion_rate(self): + if self.rm_cost_as_per in ["Valuation Rate", "Last Purchase Rate"]: + self.plc_conversion_rate = 1 + elif not self.plc_conversion_rate and self.price_list_currency: + self.plc_conversion_rate = get_exchange_rate(self.price_list_currency, + self.company_currency(), args="for_buying") + def validate_materials(self): """ Validate raw material entries """ diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 45a7b935d38..3dfd03b1395 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -81,13 +81,13 @@ class TestBOM(unittest.TestCase): # test amounts in selected currency self.assertEqual(bom.operating_cost, 100) - self.assertEqual(bom.raw_material_cost, 8000) - self.assertEqual(bom.total_cost, 8100) + self.assertEqual(bom.raw_material_cost, 351.68) + self.assertEqual(bom.total_cost, 451.68) # test amounts in selected currency self.assertEqual(bom.base_operating_cost, 6000) - self.assertEqual(bom.base_raw_material_cost, 480000) - self.assertEqual(bom.base_total_cost, 486000) + self.assertEqual(bom.base_raw_material_cost, 21100.80) + self.assertEqual(bom.base_total_cost, 27100.80) def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 97a45692fd1..ea12e2f2210 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -666,3 +666,4 @@ erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price erpnext.patches.v12_0.set_serial_no_status +erpnext.patches.v12_0.update_price_list_currency_in_bom diff --git a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py new file mode 100644 index 00000000000..f5e7b947c23 --- /dev/null +++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, flt +from erpnext.setup.utils import get_exchange_rate + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "bom") + frappe.reload_doc("manufacturing", "doctype", "bom_item") + + frappe.db.sql(""" UPDATE `tabBOM`, `tabPrice List` + SET + `tabBOM`.price_list_currency = `tabPrice List`.currency, + `tabBOM`.plc_conversion_rate = 1.0 + WHERE + `tabBOM`.buying_price_list = `tabPrice List`.name AND `tabBOM`.docstatus < 2 + AND `tabBOM`.rm_cost_as_per = 'Price List' + """) + + for d in frappe.db.sql(""" + SELECT + bom.creation, bom.name, bom.price_list_currency as currency, + company.default_currency as company_currency + FROM + `tabBOM` as bom, `tabCompany` as company + WHERE + bom.company = company.name AND bom.rm_cost_as_per = 'Price List' AND + bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", as_dict=1): + plc_conversion_rate = get_exchange_rate(d.currency, + d.company_currency, getdate(d.creation), "for_buying") + + frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) \ No newline at end of file From 6dbb02d29347c44b2f29b6d678b251fa019a34d0 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Mon, 18 May 2020 14:26:43 +0530 Subject: [PATCH 164/178] fix: show searchfields result (#21760) --- erpnext/controllers/queries.py | 66 +++++++++++++++----- erpnext/selling/doctype/customer/customer.py | 25 +++++--- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c58dd7a8e36..73ed4b01686 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,11 +8,14 @@ from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.utils import nowdate, getdate from collections import defaultdict from erpnext.stock.get_item_details import _get_item_tax_template +from frappe.utils import unique # searches for active employees def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] - return frappe.db.sql("""select name, employee_name from `tabEmployee` + fields = get_fields("Employee", ["name", "employee_name"]) + + return frappe.db.sql("""select {fields} from `tabEmployee` where status = 'Active' and docstatus < 2 and ({key} like %(txt)s @@ -24,6 +27,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, employee_name limit %(start)s, %(page_len)s""".format(**{ + 'fields': ", ".join(fields), 'key': searchfield, 'fcond': get_filters_cond(doctype, filters, conditions), 'mcond': get_match_cond(doctype) @@ -34,9 +38,12 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) - # searches for leads which are not converted + +# searches for leads which are not converted def lead_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, lead_name, company_name from `tabLead` + fields = get_fields("Lead", ["name", "lead_name", "company_name"]) + + return frappe.db.sql("""select {fields} from `tabLead` where docstatus < 2 and ifnull(status, '') != 'Converted' and ({key} like %(txt)s @@ -50,6 +57,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, lead_name limit %(start)s, %(page_len)s""".format(**{ + 'fields': ", ".join(fields), 'key': searchfield, 'mcond':get_match_cond(doctype) }), { @@ -59,6 +67,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + # searches for customer def customer_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] @@ -69,13 +78,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): else: fields = ["name", "customer_name", "customer_group", "territory"] - meta = frappe.get_meta("Customer") - searchfields = meta.get_search_fields() - searchfields = searchfields + [f for f in [searchfield or "name", "customer_name"] \ - if not f in searchfields] - fields = fields + [f for f in searchfields if not f in fields] + fields = get_fields("Customer", fields) - fields = ", ".join(fields) + searchfields = frappe.get_meta("Customer").get_search_fields() searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) return frappe.db.sql("""select {fields} from `tabCustomer` @@ -88,7 +93,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, customer_name limit %(start)s, %(page_len)s""".format(**{ - "fields": fields, + "fields": ", ".join(fields), "scond": searchfields, "mcond": get_match_cond(doctype), "fcond": get_filters_cond(doctype, filters, conditions).replace('%', '%%'), @@ -99,6 +104,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + # searches for supplier def supplier_query(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") @@ -106,7 +112,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "supplier_group"] else: fields = ["name", "supplier_name", "supplier_group"] - fields = ", ".join(fields) + + fields = get_fields("Supplier", fields) return frappe.db.sql("""select {field} from `tabSupplier` where docstatus < 2 @@ -119,7 +126,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, supplier_name limit %(start)s, %(page_len)s """.format(**{ - 'field': fields, + 'field': ', '.join(fields), 'key': searchfield, 'mcond':get_match_cond(doctype) }), { @@ -129,6 +136,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + def tax_account_query(doctype, txt, searchfield, start, page_len, filters): company_currency = erpnext.get_company_currency(filters.get('company')) @@ -153,6 +161,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): return tax_accounts + def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] @@ -214,10 +223,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals "page_len": page_len }, as_dict=as_dict) + def bom(doctype, txt, searchfield, start, page_len, filters): conditions = [] + fields = get_fields("BOM", ["name", "item"]) - return frappe.db.sql("""select tabBOM.name, tabBOM.item + return frappe.db.sql("""select {fields} from tabBOM where tabBOM.docstatus=1 and tabBOM.is_active=1 @@ -227,6 +238,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, name limit %(start)s, %(page_len)s """.format( + fields=", ".join(fields), fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), mcond=get_match_cond(doctype).replace('%', '%%'), key=searchfield), @@ -237,13 +249,16 @@ def bom(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len or 20 }) + def get_project_name(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters.get('customer'): cond = """(`tabProject`.customer = %s or ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer"))) - return frappe.db.sql("""select `tabProject`.name from `tabProject` + fields = get_fields("Project", ["name"]) + + return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") and {cond} `tabProject`.name like %(txt)s {match_cond} order by @@ -251,6 +266,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): idx desc, `tabProject`.name asc limit {start}, {page_len}""".format( + fields=", ".join(['`tabProject`.{0}'.format(f) for f in fields]), cond=cond, match_cond=get_match_cond(doctype), start=start, @@ -261,8 +277,10 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): + fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) + return frappe.db.sql(""" - select `tabDelivery Note`.name, `tabDelivery Note`.customer, `tabDelivery Note`.posting_date + select %(fields)s from `tabDelivery Note` where `tabDelivery Note`.`%(key)s` like %(txt)s and `tabDelivery Note`.docstatus = 1 @@ -277,6 +295,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, ) %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s """ % { + "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), "key": searchfield, "fcond": get_filters_cond(doctype, filters, []), "mcond": get_match_cond(doctype), @@ -342,6 +361,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): order by expiry_date, name desc limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args) + def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list = [] @@ -364,6 +384,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) + def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date from `tabBlanket Order` bo, `tabBlanket Order Item` boi @@ -378,6 +399,7 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): company = frappe.db.escape(filters.get("company")) )) + @frappe.whitelist() def get_income_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -483,6 +505,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters): item_filters = [ @@ -500,6 +523,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) ) return item_manufacturers + @frappe.whitelist() def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): query = """ @@ -513,6 +537,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): query = """ @@ -526,6 +551,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def get_tax_template(doctype, txt, searchfield, start, page_len, filters): @@ -549,3 +575,13 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): taxes = _get_item_tax_template(args, taxes, for_validate=True) return [(d,) for d in set(taxes)] + + +def get_fields(doctype, fields=[]): + meta = frappe.get_meta(doctype) + fields.extend(meta.get_search_fields()) + + if meta.title_field and not meta.title_field.strip() in fields: + fields.insert(1, meta.title_field.strip()) + + return unique(fields) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index ad5bfa835db..9a5d2ebd910 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -300,11 +300,15 @@ def get_loyalty_programs(doc): return lp_details def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): + from erpnext.controllers.queries import get_fields + if frappe.db.get_default("cust_master_name") == "Customer Name": fields = ["name", "customer_group", "territory"] else: fields = ["name", "customer_name", "customer_group", "territory"] + fields = get_fields("Customer", fields) + match_conditions = build_match_conditions("Customer") match_conditions = "and {}".format(match_conditions) if match_conditions else "" @@ -312,15 +316,18 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): filter_conditions = get_filters_cond(doctype, filters, []) match_conditions += "{}".format(filter_conditions) - return frappe.db.sql("""select %s from `tabCustomer` where docstatus < 2 - and (%s like %s or customer_name like %s) - {match_conditions} - order by - case when name like %s then 0 else 1 end, - case when customer_name like %s then 0 else 1 end, - name, customer_name limit %s, %s""".format(match_conditions=match_conditions) % - (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), - ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len)) + return frappe.db.sql(""" + select %s + from `tabCustomer` + where docstatus < 2 + and (%s like %s or customer_name like %s) + {match_conditions} + order by + case when name like %s then 0 else 1 end, + case when customer_name like %s then 0 else 1 end, + name, customer_name limit %s, %s + """.format(match_conditions=match_conditions) % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), + ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len)) def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): From 795d318fcc77f237d86caee69454b70b473fe5bd Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 18 May 2020 14:38:57 +0530 Subject: [PATCH 165/178] fix: Validate Filters in Sales Funnel. (#21761) * fix: Validate Filters in Sales Funnel. * fix: Style fixes (cherry picked from commit c734db5d4505fc08c2a55d1c5be53a10c669e8cb) --- .../selling/page/sales_funnel/sales_funnel.js | 4 ++++ .../selling/page/sales_funnel/sales_funnel.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js index 85c0cd8bf0c..e3d0a55c3a0 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.js +++ b/erpnext/selling/page/sales_funnel/sales_funnel.js @@ -90,6 +90,10 @@ erpnext.SalesFunnel = class SalesFunnel { get_data(btn) { var me = this; + if (!this.company) { + frappe.throw(__("Please Select a Company.")); + } + const method_map = { "sales_funnel": "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data", "opp_by_lead_source": "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source", diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index d62e2093c69..dba24ef5b00 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -8,14 +8,23 @@ from frappe import _ from erpnext.accounts.report.utils import convert import pandas as pd +def validate_filters(from_date, to_date, company): + if from_date and to_date and (from_date >= to_date): + frappe.throw(_("To Date must be greater than From Date")) + + if not company: + frappe.throw(_("Please Select a Company")) + @frappe.whitelist() def get_funnel_data(from_date, to_date, company): + validate_filters(from_date, to_date, company) + active_leads = frappe.db.sql("""select count(*) from `tabLead` where (date(`modified`) between %s and %s) and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' + left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` @@ -38,6 +47,8 @@ def get_funnel_data(from_date, to_date, company): @frappe.whitelist() def get_opp_by_lead_source(from_date, to_date, company): + validate_filters(from_date, to_date, company) + opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source']) if opportunities: @@ -68,11 +79,13 @@ def get_opp_by_lead_source(from_date, to_date, company): @frappe.whitelist() def get_pipeline_data(from_date, to_date, company): + validate_filters(from_date, to_date, company) + opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability']) if opportunities: default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') - + cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict() From 632c65cd59b8544a1189bb447418e997da5a1ae0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 18 May 2020 19:54:52 +0530 Subject: [PATCH 166/178] chore: Added change log --- erpnext/change_log/v12/v12_9_0.md | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 erpnext/change_log/v12/v12_9_0.md diff --git a/erpnext/change_log/v12/v12_9_0.md b/erpnext/change_log/v12/v12_9_0.md new file mode 100644 index 00000000000..5d7e0587a34 --- /dev/null +++ b/erpnext/change_log/v12/v12_9_0.md @@ -0,0 +1,43 @@ +## ERPNext v12.9.0 Release Note + +### Enhancements +- Pick List enhancements [#20962](https://github.com/frappe/erpnext/pull/20962) + - The purpose **Delivery Against Sales Order** has been changed to **Delivery** and patch for existing records. + - If the purpose is Delivery then allow rows without Sales Order against them to be mapped to the Delivery Note. + - **Update Current Stock** button to update locations and quantity after Submit. + - Validations if Item Locations table is empty (on creation of Delivery Note, Stock Entry). + - Company-wise fetching of locations and quantity. +- Payment allocation on Payment Entry based on invoice payment terms. [#20945](https://github.com/frappe/erpnext/pull/20945) +- Allow Tax Withholding Category selection at invoice level [#20870](https://github.com/frappe/erpnext/pull/20870) +- Enhanced Employee Leave Balance report, added new fields New Allocation, Expired Leaves. [#21282](https://github.com/frappe/erpnext/pull/21282) +- Provision to set Default Item Manufacturer. [#21197](https://github.com/frappe/erpnext/pull/21197) +- Tax Amount in Credit Note print format should be shown in positive. [#21252](https://github.com/frappe/erpnext/pull/21252) +- On creation of return from employee advance, sets default voucher type as Bank Entry and default debit account as Cash. [#21411](https://github.com/frappe/erpnext/pull/21411) +- On saving a Contact linked with a Lead, update the contact info from Contact into the Lead. [#21469](https://github.com/frappe/erpnext/pull/21469) +- Warning on making payment against paid invoices. [#21501](https://github.com/frappe/erpnext/pull/21501) +- Enhanced Stock Balance report with color-coding. [#21516](https://github.com/frappe/erpnext/pull/21516) +- Added total row in sales analytics report [#21519](https://github.com/frappe/erpnext/pull/21519) +- Asset related accounts must be in company currency. [#21524](https://github.com/frappe/erpnext/pull/21524) +- Accounting Dimensions in Period Closing Voucher. [#21564](https://github.com/frappe/erpnext/pull/21564) +- Renamed LMS to Learning Management System. [#21645](https://github.com/frappe/erpnext/pull/21645) +- Allow half-day attendance only via leave application. [#21719](https://github.com/frappe/erpnext/pull/21719) +- In BOM, allowed Price List in other than company currency. [#21585](https://github.com/frappe/erpnext/pull/21585) + +### Fixes: +- Account Type validation for accounts selected in Asset Category. [#21102](https://github.com/frappe/erpnext/pull/21102) +- Warehouse unset if an item doesn't have a default warehouse. [#21285](https://github.com/frappe/erpnext/pull/21285) +- Bin Requested Qty should be calculated for customer-provided items. [#21300](https://github.com/frappe/erpnext/pull/21300) +- On change of item in sales and purchase transactions, UOM should be reset based on the item's default UOM. [#21254](https://github.com/frappe/erpnext/pull/21254) +- Target warehouse in Delivery Note and Sales Invoice should not be set based on user permission. Patch to fix affected records. [#21359](https://github.com/frappe/erpnext/pull/21359) +- Budget validation against an Accounting Dimension was missing. [#21268](https://github.com/frappe/erpnext/pull/21268) +- On change of qty in Stock Entry, the fields in base currency were not set for non-serialized items. [#21389](https://github.com/frappe/erpnext/pull/21389) +- Payment request not able to make against fees [#21486](https://github.com/frappe/erpnext/pull/21486) +- Cost Center renaming is allowed only from Cost Center form [#21503](https://github.com/frappe/erpnext/pull/21503) +- Accounts Payable report showing the advance amount of other company [#21548](https://github.com/frappe/erpnext/pull/21548) +- Heatmap was not working for customer and supplier [#21578](https://github.com/frappe/erpnext/pull/21578) +- Item tax template set in the Item master is not fetched into the sales invoice and taxes are not shown in the offline pos. [#21714](https://github.com/frappe/erpnext/pull/21714) +- Validate duplicate creation of expiry ledger entry for carry forward allocation and patch to remove duplicate ledgers. [#21505](https://github.com/frappe/erpnext/pull/21505) +- Dimensions were not getting added for some GL Entries and were missing in some recently added transactions. [#21689](https://github.com/frappe/erpnext/pull/21689) +- In Repack entry, set rate for finished goods based on the cost of raw materials. [#21736](https://github.com/frappe/erpnext/pull/21736) +- Removed Guest access from Lead. [#21692](https://github.com/frappe/erpnext/pull/21692) +- Standard and Custom queries did not show search fields for Link fields. [#21685](https://github.com/frappe/erpnext/pull/21685) \ No newline at end of file From a4e92cf577771e309d5000301a1c863b91305c63 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 19 May 2020 15:09:11 +0530 Subject: [PATCH 167/178] refactor: use text editor in issue web form --- erpnext/support/web_form/issues/issues.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json index 0f15e4737fd..1c0c8ff1584 100644 --- a/erpnext/support/web_form/issues/issues.json +++ b/erpnext/support/web_form/issues/issues.json @@ -18,7 +18,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2020-03-06 05:24:05.749664", + "modified": "2020-05-19 15:08:45.361878", "modified_by": "Administrator", "module": "Support", "name": "issues", @@ -76,7 +76,7 @@ { "allow_read_on_all_link_options": 0, "fieldname": "description", - "fieldtype": "Text", + "fieldtype": "Text Editor", "hidden": 0, "label": "Description", "max_length": 0, From 8209507a8f7625cf2af8326c1ed16263d99c3449 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Tue, 19 May 2020 16:02:41 +0530 Subject: [PATCH 168/178] fix: add naming series (#21775) --- .../doctype/quality_meeting/quality_meeting.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json index 0849fd7aeb0..3818f566c17 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json @@ -1,10 +1,11 @@ { - "autoname": "format:MTNG-{date}", + "autoname": "naming_series:", "creation": "2018-10-15 16:25:41.548432", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "naming_series", "date", "cb_00", "status", @@ -53,9 +54,15 @@ "fieldname": "sb_01", "fieldtype": "Section Break", "label": "Minutes" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "MTNG-.YYYY.-.MM.-.DD.-" } ], - "modified": "2019-07-13 19:57:40.500541", + "modified": "2020-05-19 13:18:59.821740", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Meeting", From e85c3a50cded001560f50d8a27718c2355873ecf Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 19 May 2020 20:30:11 +0530 Subject: [PATCH 169/178] refactor: changed the fieldtype from data to small text (#21789) --- .../accounts/doctype/sales_invoice/sales_invoice.json | 4 ++-- .../stock/doctype/delivery_note/delivery_note.json | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ea78c27333e..6e0a30d48e0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -397,7 +397,7 @@ { "allow_on_submit": 1, "fieldname": "po_no", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Customer's Purchase Order", "no_copy": 1, "print_hide": 1 @@ -1570,7 +1570,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-04-29 13:37:09.355300", + "modified": "2020-05-19 17:00:57.208696", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 424a7f3c22f..8a2fa397731 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -24,10 +24,10 @@ "return_against", "customer_po_details", "po_no", - "section_break_18", - "pick_list", "column_break_17", "po_date", + "section_break_18", + "pick_list", "contact_info", "shipping_address_name", "shipping_address", @@ -294,7 +294,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "po_no", "fieldname": "customer_po_details", "fieldtype": "Section Break", "label": "Customer PO Details" @@ -302,7 +301,7 @@ { "allow_on_submit": 1, "fieldname": "po_no", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Customer's Purchase Order No", "no_copy": 1, "oldfieldname": "po_no", @@ -316,7 +315,6 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.po_no", "fieldname": "po_date", "fieldtype": "Date", "label": "Customer's Purchase Order Date", @@ -324,7 +322,6 @@ "oldfieldtype": "Data", "print_hide": 1, "print_width": "100px", - "read_only": 1, "width": "100px" }, { @@ -1240,7 +1237,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-04-17 12:51:41.288600", + "modified": "2020-05-19 17:03:45.880106", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From 96a05f65aaaf7cd22e4373b631bc392e1f495c7d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 19 May 2020 20:59:51 +0530 Subject: [PATCH 170/178] fix: Against voucher in General Ledger --- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ae407231caa..bb9cfcd886f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -293,7 +293,7 @@ def get_accountwise_gle(filters, gl_entries, gle_map): data[key].debit_in_account_currency += flt(gle.debit_in_account_currency) data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) - if data[key].against_voucher: + if data[key].against_voucher and gle.against_voucher: data[key].against_voucher += ', ' + gle.against_voucher from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) From 0550b3537da3e3762c7dad19f4722d3224156c21 Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 20 May 2020 16:13:34 +0530 Subject: [PATCH 171/178] fix: Validate Payment Gateway only if it exists in Payment Request. (#21807) --- erpnext/accounts/doctype/payment_request/payment_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 4e4eac0610f..c863ce8e7f7 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -69,7 +69,7 @@ class PaymentRequest(Document): elif self.payment_request_type == 'Inward': self.db_set('status', 'Requested') - send_mail = self.payment_gateway_validation() + send_mail = self.payment_gateway_validation() if self.payment_gateway else None ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart") \ From 938cde30e37f71cbfff637d5a6e82a2da30102fa Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 20 May 2020 16:14:58 +0530 Subject: [PATCH 172/178] enable Allow Rename in sales stage (#21803) --- .../crm/doctype/sales_stage/sales_stage.json | 120 +++++------------- 1 file changed, 34 insertions(+), 86 deletions(-) diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json index 4374bb58314..fe5fce32f86 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.json +++ b/erpnext/crm/doctype/sales_stage/sales_stage.json @@ -1,96 +1,44 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:stage_name", - "beta": 0, - "creation": "2018-10-01 09:28:16.399518", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:stage_name", + "creation": "2018-10-01 09:28:16.399518", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stage_name" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stage_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Stage Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "stage_name", + "fieldtype": "Data", + "label": "Stage Name", "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-10-01 09:29:43.230378", - "modified_by": "Administrator", - "module": "CRM", - "name": "Sales Stage", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-05-20 14:39:33.300588", + "modified_by": "Administrator", + "module": "CRM", + "name": "Sales Stage", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From d1569b9581eb2168b1081f10bd24d3e21d337935 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 May 2020 22:15:12 +0530 Subject: [PATCH 173/178] fix: Project filter in Trial Baalance Report --- erpnext/accounts/report/trial_balance/trial_balance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 8bd4399e608..5a699b6580a 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -71,7 +71,8 @@ def get_data(filters): opening_balances = get_opening_balances(filters) #add filter inside list so that the query in financial_statements.py doesn't break - filters.project = [filters.project] + if filters.project: + filters.project = [filters.project] set_gl_entries_by_account(filters.company, filters.from_date, filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) From 74dd64501cae27a8b41e3935d8d3f89f97469d52 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 21 May 2020 10:09:42 +0530 Subject: [PATCH 174/178] fix: item tax template not applied if valid from is blank --- erpnext/accounts/page/pos/pos.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 06575ac3a69..28c9149c561 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1459,7 +1459,11 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ this.child.serial_no = (this.item_serial_no[this.child.item_code] ? this.item_serial_no[this.child.item_code][0] : ''); - const tax_template_is_valid = frappe.datetime.get_diff(frappe.datetime.now_date(), this.items[0].valid_from) > 0; + const tax_template_is_valid = true; + if (this.items && this.items[0].valid_from) { + tax_template_is_valid = frappe.datetime.get_diff(frappe.datetime.now_date(), + this.items[0].valid_from) > 0; + } this.child.item_tax_template = tax_template_is_valid ? this.items[0].item_tax_template : ''; this.child.item_tax_rate = JSON.stringify(this.tax_data[this.child.item_tax_template]); From 2894640d56e446625b405ddbebf74766456b0d22 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 21 May 2020 12:08:15 +0530 Subject: [PATCH 175/178] fix: plc conversion rate set infinitely (#21822) --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index cfcab7aea68..74169c80d35 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -228,7 +228,7 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ plc_conversion_rate: function(doc) { if (!this.in_apply_price_list) { - this.apply_price_list(); + this.apply_price_list(null, true); } }, From 3c84ef3b5e8e61badc8c638815b9890ef342d019 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 21 May 2020 13:11:48 +0530 Subject: [PATCH 176/178] fix: Fetch customer into Delivery Note from Pick List --- erpnext/stock/doctype/pick_list/pick_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 917524929e1..c79025c1fb7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -300,6 +300,7 @@ def create_delivery_note(source_name, target_doc=None): set_delivery_note_missing_values(delivery_note) delivery_note.pick_list = pick_list.name + delivery_note.customer = pick_list.customer if pick_list.customer else None return delivery_note From cb74ff870db56f7b717ec6db8671ce1afcbc8485 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 21 May 2020 14:03:29 +0530 Subject: [PATCH 177/178] fix: Supplier Invoice No not fetched in Import Supplier Invoice --- .../import_supplier_invoice/import_supplier_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index 72fe17fb379..203ea8c865a 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -58,7 +58,7 @@ class ImportSupplierInvoice(Document): "naming_series": self.invoice_series, "document_type": line.TipoDocumento.text, "bill_date": get_datetime_str(line.Data.text), - "invoice_no": line.Numero.text, + "bill_no": line.Numero.text, "total_discount": 0, "items": [], "buying_price_list": self.default_buying_price_list @@ -249,7 +249,7 @@ def create_supplier(supplier_group, args): return existing_supplier_name else: - + new_supplier = frappe.new_doc("Supplier") new_supplier.supplier_name = args.supplier new_supplier.supplier_group = supplier_group From ab79a9554ba0830da296581e8d8f2f050c416d1c Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Thu, 21 May 2020 15:20:37 +0550 Subject: [PATCH 178/178] bumped to version 12.9.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 9b972e7561e..54ffe3f6a14 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.8.0' +__version__ = '12.9.0' def get_default_company(user=None): '''Get default company for user'''