From 9578ead7f9d6ac25094108e341a9f7b9c09561d0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 28 Mar 2020 18:44:53 +0530 Subject: [PATCH 001/108] 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/108] 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/108] 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/108] 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 39b8e150bf78c961503b19fe07d7610086a090ce Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 9 Apr 2020 12:41:24 +0530 Subject: [PATCH 005/108] 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 006/108] 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 007/108] 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 008/108] 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 009/108] 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 010/108] 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 011/108] 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 012/108] 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 013/108] 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 014/108] 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 015/108] 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 016/108] 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 017/108] 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 018/108] 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 019/108] 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 020/108] 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 021/108] 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 022/108] 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 023/108] 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 024/108] 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 025/108] 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 026/108] 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 027/108] 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 028/108] 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 029/108] 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 030/108] 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 031/108] 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 032/108] 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 033/108] 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 034/108] 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 035/108] 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 036/108] 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 037/108] 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 038/108] 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 039/108] 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 040/108] 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 041/108] 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 042/108] 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 043/108] 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 044/108] 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 045/108] 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 046/108] 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 047/108] 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 048/108] 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 049/108] 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 050/108] 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 051/108] 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 052/108] 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 053/108] 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 054/108] 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 055/108] 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 056/108] 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 057/108] 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 058/108] 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 059/108] 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 060/108] 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 061/108] 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 062/108] 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 063/108] 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 064/108] 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 065/108] 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 066/108] 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 067/108] 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 068/108] 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 069/108] 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 070/108] 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 071/108] 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 072/108] 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 073/108] 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 074/108] 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 075/108] 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 076/108] 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 077/108] 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 078/108] 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 079/108] 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 080/108] 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 081/108] 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 082/108] 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 083/108] 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 084/108] 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 085/108] 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 086/108] 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 087/108] 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 088/108] 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 089/108] 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 090/108] 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 a8b87ccce0b055e9a36f5270d40dbbc36beb50cf Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 18:39:16 +0530 Subject: [PATCH 091/108] 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 092/108] 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 f77a73546908d531d191e56621bccb30e207dd1e Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 1 May 2020 00:01:37 +0530 Subject: [PATCH 093/108] 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 094/108] 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 095/108] 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 096/108] 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 097/108] 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 12b5d72e70690c942e22819bc08eeba5270e299c Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 3 May 2020 13:20:30 +0530 Subject: [PATCH 098/108] 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 bdbfd2ad0ca5e91829a2705b4673fcbf37b5804e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 May 2020 11:07:10 +0530 Subject: [PATCH 099/108] 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 100/108] 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 101/108] 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 102/108] 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 103/108] 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 104/108] 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 794bb6ebdd5bc24864eeef370fe9ec425a3025f3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 May 2020 16:12:10 +0530 Subject: [PATCH 105/108] 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 106/108] 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 107/108] 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 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 108/108] 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 + """)