From 25d208aa8a1fcb535696f7da450176aacc23d675 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Sep 2021 10:37:41 +0530 Subject: [PATCH 001/247] fix: GL Entries on advance TDS allocation --- erpnext/controllers/accounts_controller.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b90db054b5..515d6fdef5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -832,7 +832,7 @@ class AccountsController(TransactionBase): if pe.reference_type == 'Payment Entry': pe = frappe.get_doc('Payment Entry', pe.reference_name) for tax in pe.get('taxes'): - allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head) + allocated_amount = flt(tax_map.get(tax.account_head)) - flt(allocated_tax_map.get(tax.account_head)) if allocated_amount > tax.tax_amount: allocated_amount = tax.tax_amount @@ -931,15 +931,19 @@ class AccountsController(TransactionBase): if pe.reference_type == "Payment Entry" and \ frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'): pe = frappe.get_doc("Payment Entry", pe.reference_name) + advance_tax_account = pe.advance_tax_account + for tax in pe.get("taxes"): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - else: dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" + advance_tax_account = pe.advance_tax_account if pe.paid_from != pe.advance_tax_account \ + else self.credit_to + else: + dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" + rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount @@ -961,13 +965,15 @@ class AccountsController(TransactionBase): gl_entries.append( self.get_gl_dict({ - "account": pe.advance_tax_account, + "account": advance_tax_account, "against": party, rev_dr_cr: unallocated_amount, rev_dr_cr + "_in_account_currency": unallocated_amount if account_currency==self.company_currency else unallocated_amount, - "cost_center": tax.cost_center + "cost_center": self.get('cost_center') if advance_tax_account == self.get('credit_to') else tax.cost_center, + "party_type": 'Supplier' if advance_tax_account == self.get('credit_to') else '', + "party": self.get('supplier') if advance_tax_account == self.get('credit_to') else '', }, account_currency, item=tax)) frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount", From 1aed8c4b2fca6000b036773539a75f0c697e83a8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:40:38 +0530 Subject: [PATCH 002/247] feat: Add Serial No field --- .../asset_repair_consumed_item.json | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index 528f0ec986..f63add1235 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -5,19 +5,13 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "item", + "item_code", "valuation_rate", "consumed_quantity", - "total_value" + "total_value", + "serial_no" ], "fields": [ - { - "fieldname": "item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item", - "options": "Item" - }, { "fetch_from": "item.valuation_rate", "fieldname": "valuation_rate", @@ -38,12 +32,24 @@ "in_list_view": 1, "label": "Total Value", "read_only": 1 + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-12 03:19:55.006300", + "modified": "2021-11-11 18:23:00.492483", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", From abb535540a0910bb6193ad0985e28116123e38e7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:41:16 +0530 Subject: [PATCH 003/247] fix: Rename item to item_code --- erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 99a7d9bfbf..1131197bd2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -120,7 +120,7 @@ class AssetRepair(AccountsController): for stock_item in self.get('stock_items'): stock_entry.append('items', { "s_warehouse": self.warehouse, - "item_code": stock_item.item, + "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, "basic_rate": stock_item.valuation_rate }) From 4668bb4be0320580f10608cdc74d23fc4a775a88 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:42:25 +0530 Subject: [PATCH 004/247] feat: Add 'Add Serial No' button in the Stock Items table --- erpnext/assets/doctype/asset_repair/asset_repair.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 18a56d33e6..d554d52a71 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -60,6 +60,10 @@ frappe.ui.form.on('Asset Repair', { if (frm.doc.repair_status == "Completed") { frm.set_value('completion_date', frappe.datetime.now_datetime()); } + }, + + stock_items_on_form_rendered() { + erpnext.setup_serial_or_batch_no(); } }); From 1393f97ad52902cc57a87d2a700ce3c169a5707c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 11 Nov 2021 18:48:23 +0530 Subject: [PATCH 005/247] fix: Add serial no to Stock Entry doc to decrease quantity for Stock Items consumed during repair --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 1131197bd2..2c01ed7748 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -122,7 +122,8 @@ class AssetRepair(AccountsController): "s_warehouse": self.warehouse, "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, - "basic_rate": stock_item.valuation_rate + "basic_rate": stock_item.valuation_rate, + "serial_no": stock_item.serial_no }) stock_entry.insert() From 520f33b02fcd77b4fab31da35198f2195800d83f Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sun, 14 Nov 2021 08:10:53 +0530 Subject: [PATCH 006/247] fix(regional): hsn_wise as false returns item_code --- erpnext/regional/india/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 1733220c0a..e09c38fa45 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -572,17 +572,17 @@ def get_item_list(data, doc, hsn_wise=False): } item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) - for hsn_code, taxable_amount in hsn_taxable_amount.items(): + for item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() - if not hsn_code: + if not item_or_hsn: frappe.throw(_('GST HSN Code does not exist for one or more items')) - item_data.hsnCode = int(hsn_code) + item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" for attr in item_data_attrs: item_data[attr] = 0 - for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items(): + for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): account_type = gst_accounts.get(account, '') for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: From aa347802656a92695c3f3b94c292d7f6a6209709 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 04:57:40 +0530 Subject: [PATCH 007/247] fix: Filter Depreciation Expense Account by root type --- erpnext/assets/doctype/asset_category/asset_category.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 51ce157a81..c702687072 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', { var d = locals[cdt][cdn]; return { "filters": { - "root_type": "Expense", + "root_type": ["in", ["Expense", "Income"]], "is_group": 0, "company": d.company_name } From 8fc31e3ceabcda85ba15087a07f7c8ca4c1d57a7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 04:58:13 +0530 Subject: [PATCH 008/247] fix: Make Depreciation Entry posting more flexible --- erpnext/assets/doctype/asset/depreciation.py | 30 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ca10b1db19..6591654f94 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None): je.finance_book = d.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) + credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account) + credit_entry = { - "account": accumulated_depreciation_account, + "account": credit_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None): } debit_entry = { - "account": depreciation_expense_account, + "account": debit_account, "debit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -132,6 +134,30 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account +def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): + if is_income_or_expense_account(depreciation_expense_account) == "Expense": + credit_account = accumulated_depreciation_account + debit_account = depreciation_expense_account + else: + credit_account = depreciation_expense_account + debit_account = accumulated_depreciation_account + + return credit_account, debit_account + +def is_income_or_expense_account(account): + from frappe.utils.nestedset import get_ancestors_of + + ancestors = get_ancestors_of("Account", account) + if ancestors: + root_account = ancestors[-1].split(' - ')[0] + + if root_account == "Expenses": + return "Expense" + elif root_account == "Income": + return "Income" + + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) From 62fbbe8915fcb3b03c823b5e5aae7c2e400c5002 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 04:59:59 +0530 Subject: [PATCH 009/247] fix: Only raise an error if Depreciation Expense Account is neither an Income nor an Expense Account --- .../doctype/asset_category/asset_category.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index e2f3ca318f..bd573bf479 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -42,10 +42,10 @@ class AssetCategory(Document): def validate_account_types(self): account_type_map = { - 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, - 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, - 'depreciation_expense_account': { 'root_type': 'Expense' }, - 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } + 'fixed_asset_account': {'account_type': ['Fixed Asset']}, + 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']}, + 'depreciation_expense_account': {'root_type': ['Expense', 'Income']}, + 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']} } for d in self.accounts: for fieldname in account_type_map.keys(): @@ -53,11 +53,11 @@ class AssetCategory(Document): selected_account = d.get(fieldname) 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] + expected_key_types = account_type_map[fieldname][key_to_match] - if selected_key_type != expected_key_type: + if selected_key_type not in expected_key_types: 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)), + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)), title=_("Invalid Account")) def valide_cwip_account(self): From cb93cc972d18ba7f52482901eb849bd418fb9fc5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 17 Nov 2021 05:22:43 +0530 Subject: [PATCH 010/247] fix: Check all ancestors and not just the root node --- erpnext/assets/doctype/asset/depreciation.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 6591654f94..0c96b519ed 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -147,13 +147,11 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation def is_income_or_expense_account(account): from frappe.utils.nestedset import get_ancestors_of - ancestors = get_ancestors_of("Account", account) + ancestors = [ancestor.split(' - ')[0] for ancestor in get_ancestors_of("Account", account)] if ancestors: - root_account = ancestors[-1].split(' - ')[0] - - if root_account == "Expenses": + if "Expenses" in ancestors: return "Expense" - elif root_account == "Income": + elif "Income" in ancestors: return "Income" frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) From a4043c035d4116e206e486d4eecabe897cf24630 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:27:06 +0530 Subject: [PATCH 011/247] fix: Prevent clearing of Depreciation Schedule on adding more than one Finance Book --- erpnext/assets/doctype/asset/asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2..7031ed15bd 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -185,11 +185,11 @@ class Asset(AccountsController): if not self.available_for_use_date: return + start = self.clear_depreciation_schedule() + for d in self.get('finance_books'): self.validate_asset_finance_books(d) - start = self.clear_depreciation_schedule() - # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - From 059d1f3b74b2bee707387cea518eeb394bfce3d8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:35:41 +0530 Subject: [PATCH 012/247] fix: Rename loop variable --- erpnext/assets/doctype/asset/asset.py | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7031ed15bd..c00d6836a6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -187,23 +187,23 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - for d in self.get('finance_books'): - self.validate_asset_finance_books(d) + for finance_book in self.get('finance_books'): + self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value - if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = (flt(finance_book.value_after_depreciation) - flt(self.opening_accumulated_depreciation)) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) - d.value_after_depreciation = value_after_depreciation + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(d) + has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: number_of_pending_depreciations += 1 @@ -213,56 +213,56 @@ class Asset(AccountsController): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(d.depreciation_start_date, - n * cint(d.frequency_of_depreciation)) + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) # if asset is being sold if date_of_sale: - from_date = self.get_from_date(d.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, from_date, date_of_sale) if depreciation_amount > 0: self.append("schedules", { "schedule_date": date_of_sale, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) break # For first row if has_pro_rata and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - self.available_for_use_date, d.depreciation_start_date) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + self.available_for_use_date, finance_book.depreciation_start_date) # For first depr schedule date will be the start date # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + n * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = self.get_pro_rata_amt(d, + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, schedule_date, self.to_date) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, d.finance_book) + depreciation_amount, finance_book.finance_book) monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) @@ -273,10 +273,10 @@ class Asset(AccountsController): self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != d.expected_value_after_useful_life) - or value_after_depreciation < d.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) skip_row = True if depreciation_amount > 0: @@ -286,7 +286,7 @@ class Asset(AccountsController): # In pro rata case, for first and last depreciation, month range would be different month_range = months \ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else d.frequency_of_depreciation + else finance_book.frequency_of_depreciation for r in range(month_range): if (has_pro_rata and n == 0): @@ -312,17 +312,17 @@ class Asset(AccountsController): self.append("schedules", { "schedule_date": date, "depreciation_amount": amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) else: self.append("schedules", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) # used when depreciation schedule needs to be modified due to increase in asset life From 9d319c2205c475f4f886bc0bd7c32721993ba06a Mon Sep 17 00:00:00 2001 From: Mohammed Redah Date: Fri, 19 Nov 2021 15:13:23 +0300 Subject: [PATCH 013/247] fix:Change QR Code Triggerr event This fixes the bug if the user changes the date after insertion it will show the wrong values --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a277ee035..4f00ef817a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -248,10 +248,10 @@ doc_events = { "validate": "erpnext.regional.india.utils.validate_tax_category" }, "Sales Invoice": { - "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code", "on_submit": [ "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", + "erpnext.regional.saudi_arabia.utils.create_qr_code", "erpnext.erpnext_integrations.taxjar_integration.create_transaction" ], "on_cancel": [ From db9bd5b912e8d7072d6f362b64919a62a6f18f78 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Nov 2021 17:55:48 +0530 Subject: [PATCH 014/247] feat: report to see the consumed materials against the work order --- .../work_order_consumed_materials/__init__.py | 0 .../work_order_consumed_materials.js | 70 +++++++++ .../work_order_consumed_materials.json | 30 ++++ .../work_order_consumed_materials.py | 131 +++++++++++++++++ .../manufacturing/manufacturing.json | 135 ++++++++++++++++-- 5 files changed, 351 insertions(+), 15 deletions(-) create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/__init__.py create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json create mode 100644 erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js new file mode 100644 index 0000000000..b2428e85b7 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js @@ -0,0 +1,70 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Work Order Consumed Materials"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + label: __("Work Order"), + fieldname: "name", + fieldtype: "Link", + options: "Work Order", + get_query: function() { + return { + filters: { + status: ["in", ["In Process", "Completed", "Stopped"]] + } + } + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "Link", + depends_on: "eval: !doc.name", + options: "Item" + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["In Process", "Completed", "Stopped"] + }, + { + label: __("Excess Materials Consumed"), + fieldname: "show_extra_consumed_materials", + fieldtype: "Check" + } + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) { + value = `
${value}
`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json new file mode 100644 index 0000000000..2fc986aa36 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-11-22 17:36:11.886939", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2021-11-22 17:36:14.999091", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Consumed Materials", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Work Order Consumed Materials", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py new file mode 100644 index 0000000000..8d2505ad95 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -0,0 +1,131 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +import json +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_data(report_filters): + fields = get_fields() + filters = get_filter_condition(report_filters) + + wo_items = {} + for d in frappe.get_all("Work Order", filters = filters, fields=fields): + d.extra_consumed_qty = 0.0 + if d.consumed_qty and d.consumed_qty > d.required_qty: + d.extra_consumed_qty = d.consumed_qty - d.required_qty + + if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials: + wo_items.setdefault((d.name, d.production_item), []).append(d) + + data = [] + for key, wo_data in wo_items.items(): + for index, row in enumerate(wo_data): + if index != 0: + #If one work order has multiple raw materials then show parent data in the first row only + for field in ["name", "status", "production_item", "qty", "produced_qty"]: + row[field] = "" + + data.append(row) + + return data + +def get_fields(): + return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code", + "`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`", + "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`", + "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`", + "`tabWork Order`.`produced_qty`"] + +def get_filter_condition(report_filters): + filters = { + "docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]), + "creation": ("between", [report_filters.from_date, report_filters.to_date]) + } + + for field in ["name", "production_item", "company", "status"]: + value = report_filters.get(field) + if value: + key = f"`{field}`" + filters.update({key: value}) + + return filters + +def get_columns(): + return [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 80 + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": 80 + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Qty to Produce"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("Produced Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Raw Material Item"), + "fieldname": "raw_material_item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150 + }, + { + "label": _("Item Name"), + "fieldname": "raw_material_name", + "width": 130 + }, + { + "label": _("Required Qty"), + "fieldname": "required_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Transferred Qty"), + "fieldname": "transferred_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Extra Consumed Qty"), + "fieldname": "extra_consumed_qty", + "fieldtype": "Float", + "width": 100 + } + ] \ No newline at end of file diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index cfa80f8e9f..65b4d02639 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,10 +1,6 @@ { - "charts": [ - { - "chart_name": "Produced Quantity" - } - ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]", + "charts": [], + "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "docstatus": 0, "doctype": "Workspace", @@ -140,14 +136,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Work Order", "hidden": 0, @@ -295,9 +283,126 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 10, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Production Planning Report", + "link_count": 0, + "link_to": "Production Planning Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Work Order Summary", + "link_count": 0, + "link_to": "Work Order Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Quality Inspection", + "hidden": 0, + "is_query_report": 1, + "label": "Quality Inspection Summary", + "link_count": 0, + "link_to": "Quality Inspection Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Downtime Entry", + "hidden": 0, + "is_query_report": 1, + "label": "Downtime Analysis", + "link_count": 0, + "link_to": "Downtime Analysis", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Job Card", + "hidden": 0, + "is_query_report": 1, + "label": "Job Card Summary", + "link_count": 0, + "link_to": "Job Card Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Search", + "link_count": 0, + "link_to": "BOM Search", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Stock Report", + "link_count": 0, + "link_to": "BOM Stock Report", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Production Analytics", + "link_count": 0, + "link_to": "Production Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "BOM", + "hidden": 0, + "is_query_report": 1, + "label": "BOM Operations Time", + "link_count": 0, + "link_to": "BOM Operations Time", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Work Order Consumed Materials", + "link_count": 0, + "link_to": "Work Order Consumed Materials", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2021-08-05 12:16:00.825742", + "modified": "2021-11-22 17:55:03.524496", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", From 475d8394e413d530ec57d9c7f713fecd706b2271 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 22 Nov 2021 21:23:27 +0530 Subject: [PATCH 015/247] fix: Clear Depreciation Schedule entries that aren't linked with Journal Entries before modifying the schedule --- erpnext/assets/doctype/asset/asset.py | 42 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c00d6836a6..f3825ea682 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -209,7 +209,8 @@ class Asset(AccountsController): number_of_pending_depreciations += 1 skip_row = False - for n in range(start, number_of_pending_depreciations): + + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -325,14 +326,39 @@ class Asset(AccountsController): "finance_book_id": finance_book.idx }) - # used when depreciation schedule needs to be modified due to increase in asset life + # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales + # JE: Journal Entry, FB: Finance Book def clear_depreciation_schedule(self): - start = 0 - for n in range(len(self.schedules)): - if not self.schedules[n].journal_entry: - del self.schedules[n:] - start = n - break + start = [] + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in self.get('schedules'): + + # to ensure that start will only be updated once for each FB + if len(start) == (int(schedule.finance_book_id) - 1): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + elif len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when all the schedule rows corresponding to the last FB are linked with JEs + if len(start) == (len(self.finance_books) - 1): + start.append(num_of_depreciations_completed) + + # when the Depreciation Schedule is being created for the first time + if start == []: + start = [0] * len(self.finance_books) + else: + self.schedules = depr_schedule + return start def get_from_date(self, finance_book): From 406278b5c1d51dbe7f09a94beb1092f5cb9f7230 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 04:51:53 +0530 Subject: [PATCH 016/247] fix: Add bundle items to PO only if the Product Bundle was selected from the SO --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 47b8ebd348..1c2482568f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -925,6 +925,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "supplier", "pricing_rules" ], + "condition": lambda doc: doc.parent_item in items_to_map } }, target_doc, set_missing_values) From d406775d1ab9b5b5c3a73da4aa00e656cc239047 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 20:21:24 +0530 Subject: [PATCH 017/247] fix: Check root_type of Depreciation Expense Account --- erpnext/assets/doctype/asset/depreciation.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 0c96b519ed..874fb630f8 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -135,27 +135,19 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): - if is_income_or_expense_account(depreciation_expense_account) == "Expense": + root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") + + if root_type == "Expense": credit_account = accumulated_depreciation_account debit_account = depreciation_expense_account - else: + elif root_type == "Income": credit_account = depreciation_expense_account debit_account = accumulated_depreciation_account + else: + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) return credit_account, debit_account -def is_income_or_expense_account(account): - from frappe.utils.nestedset import get_ancestors_of - - ancestors = [ancestor.split(' - ')[0] for ancestor in get_ancestors_of("Account", account)] - if ancestors: - if "Expenses" in ancestors: - return "Expense" - elif "Income" in ancestors: - return "Income" - - frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) - @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) From a36713cd2d353826d4c0e6330eb993ab972c1313 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 21:03:03 +0530 Subject: [PATCH 018/247] fix: Test Depreciation Entry posting when Depreciation Expense Account is an Expense Account --- erpnext/assets/doctype/asset/test_asset.py | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec7..cc7eab0ffd 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -869,6 +869,34 @@ class TestDepreciationBasics(AssetSetup): self.assertFalse(asset.schedules[1].journal_entry) self.assertFalse(asset.schedules[2].journal_entry) + def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): + """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + else: + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" From 60d82d913c2a09dcafc1f917e79fbe1a6be444bc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:03:48 +0530 Subject: [PATCH 019/247] fix: Test Depreciation Entry posting when Depreciation Expense Account is an Income Account --- erpnext/assets/doctype/asset/test_asset.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index cc7eab0ffd..b0549b1027 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -897,6 +897,42 @@ class TestDepreciationBasics(AssetSetup): self.assertTrue(entry["credit"]) self.assertFalse(entry["debit"]) + def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self): + """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account.""" + + depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") + depr_expense_account.root_type = "Income" + depr_expense_account.parent_account = "Income - _TC" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + else: + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + + # resetting + depr_expense_account.root_type = "Expense" + depr_expense_account.parent_account = "Expenses - _TC" + def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" From f455de2924f7c7554b9eefc757c354992901136b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:26:32 +0530 Subject: [PATCH 020/247] fix: Test if multiple Depreciation Schedules are set up for multiple Finance Books --- erpnext/assets/doctype/asset/test_asset.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec7..fd4d9ff239 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,38 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.save() + + self.assertEqual(len(asset.schedules), 9) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, 1) + else: + self.assertEqual(schedule.finance_book_id, 2) + def test_depreciation_entry_cancellation(self): asset = create_asset( item_code = "Macbook Pro", From 33a0b1db2ced1fe84386dbd6b1e5eacb535e5098 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 02:45:17 +0530 Subject: [PATCH 021/247] fix: Retain depreciation schedule rows that are linked with JEs while clearing the schedule --- erpnext/assets/doctype/asset/asset.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f3825ea682..351556c76c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -335,6 +335,11 @@ class Asset(AccountsController): for schedule in self.get('schedules'): + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + if len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + # to ensure that start will only be updated once for each FB if len(start) == (int(schedule.finance_book_id) - 1): if schedule.journal_entry: @@ -344,11 +349,6 @@ class Asset(AccountsController): start.append(num_of_depreciations_completed) num_of_depreciations_completed = 0 - # to update start when there are JEs linked with all the schedule rows corresponding to an FB - elif len(start) == (int(schedule.finance_book_id) - 2): - start.append(num_of_depreciations_completed) - num_of_depreciations_completed = 0 - # to update start when all the schedule rows corresponding to the last FB are linked with JEs if len(start) == (len(self.finance_books) - 1): start.append(num_of_depreciations_completed) From 6ec5a190631b563804748fe43532b70d0f230ff9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 03:19:36 +0530 Subject: [PATCH 022/247] fix: Test if clear_depreciation_schedule() works for multiple finance books --- erpnext/assets/doctype/asset/test_asset.py | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fd4d9ff239..c1a978572e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,50 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_clear_depreciation_schedule_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2023-12-31" + }) + asset.submit() + + post_depreciation_entries(date="2023-01-01") + asset.load_from_db() + + asset.clear_depreciation_schedule() + + self.assertEqual(len(asset.schedules), 6) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, "1") + else: + self.assertEqual(schedule.finance_book_id, "2") + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset = create_asset( item_code = "Macbook Pro", From 4311936b7dd2bf7e3f5e11fe5ce6eeff03a7d49b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 05:21:31 +0530 Subject: [PATCH 023/247] fix: Edit dates and frequency of depreciation --- erpnext/assets/doctype/asset/test_asset.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c1a978572e..a297a5a501 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -900,28 +900,28 @@ class TestDepreciationBasics(AssetSetup): asset.calculate_depreciation = 1 asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2023-12-31" + "depreciation_start_date": "2020-12-31" }) asset.submit() - post_depreciation_entries(date="2023-01-01") + post_depreciation_entries(date="2020-04-01") asset.load_from_db() asset.clear_depreciation_schedule() From 0803f87660625086f6ea787f85c46f3587463302 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 06:07:58 +0530 Subject: [PATCH 024/247] fix: Fix Product Bundle price calculation when there are multiple Product Bundles --- erpnext/stock/doctype/packed_item/packed_item.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 3f73093d67..095457f04c 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -128,7 +128,8 @@ def update_product_bundle_price(doc, parent_items): else: update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price) - bundle_price = 0 + bundle_item_rate = bundle_item.rate if bundle_item.rate else 0 + bundle_price = bundle_item.qty * bundle_item_rate parent_items_index += 1 # for the last product bundle From 9047e2b9dd8b6d8b1819404d98480d86b598d562 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 13:52:20 +0530 Subject: [PATCH 025/247] refactor: do not set default priority in isssues --- erpnext/support/doctype/issue/issue.json | 4 ++-- erpnext/support/doctype/issue/issue_list.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 14712f89fe..75b6d0f4f9 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -123,7 +123,6 @@ "search_index": 1 }, { - "default": "Medium", "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -410,10 +409,11 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2021-06-10 03:22:27.098898", + "modified": "2021-11-24 13:13:10.276630", "modified_by": "Administrator", "module": "Support", "name": "Issue", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js index e04498e29e..5bfecb019c 100644 --- a/erpnext/support/doctype/issue/issue_list.js +++ b/erpnext/support/doctype/issue/issue_list.js @@ -18,7 +18,6 @@ frappe.listview_settings['Issue'] = { }, get_indicator: function(doc) { if (doc.status === 'Open') { - if (!doc.priority) doc.priority = 'Medium'; const color = { 'Low': 'yellow', 'Medium': 'orange', From 1f060c0b0a2d1925ebfda8b269517462d428ef35 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 13:53:39 +0530 Subject: [PATCH 026/247] feat: find SLA based on customer group's ancestors --- .../service_level_agreement.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 5f8f83d89b..9dfe339c51 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -22,6 +22,7 @@ from frappe.utils import ( time_diff_in_seconds, to_timedelta, ) +from frappe.utils.nestedset import get_ancestors_of from frappe.utils.safe_exec import get_safe_globals from erpnext.support.doctype.issue.issue import get_holidays @@ -248,7 +249,7 @@ def get_active_service_level_agreement_for(doc): customer = doc.get('customer') or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)] ) default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] @@ -275,11 +276,23 @@ def get_context(doc): return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} def get_customer_group(customer): - return frappe.db.get_value("Customer", customer, "customer_group") if customer else None + customer_groups = [] + customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None + if customer_group: + ancestors = get_ancestors_of("Customer Group", customer_group) + customer_groups = [customer_group] + ancestors + + return customer_groups def get_customer_territory(customer): - return frappe.db.get_value("Customer", customer, "territory") if customer else None + customer_territories = [] + customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None + if customer_territory: + ancestors = get_ancestors_of("Territory", customer_territory) + customer_territories = [customer_territory] + ancestors + + return customer_territories @frappe.whitelist() @@ -299,7 +312,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): if customer: # Include SLA with No Entity and Entity Type or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]] + ["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)] ) return { @@ -343,6 +356,8 @@ def apply(doc, method=None): service_level_agreement = get_active_service_level_agreement_for(doc) + print(service_level_agreement) + if not service_level_agreement: return From 5b2ba245637befb7c6a01abcd9fbc5d83318a551 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 24 Nov 2021 19:19:45 +0530 Subject: [PATCH 027/247] fix: company tour --- .../selling/form_tour/customer/customer.json | 29 ++++++++ .../form_tour/quotation/quotation.json | 67 +++++++++++++++++++ erpnext/setup/form_tour/company/company.json | 67 +++++++++++++++++++ .../setup/module_onboarding/home/home.json | 62 +++++++++++++++++ .../company_set_up/company_set_up.json | 21 ++++++ .../create_a_customer/create_a_customer.json | 21 ++++++ .../create_a_quotation.json | 21 ++++++ .../create_a_supplier/create_a_supplier.json | 21 ++++++ .../create_an_item/create_an_item.json | 22 ++++++ .../data_import/data_import.json | 21 ++++++ .../letterhead/letterhead.json | 21 ++++++ .../navigation_help/navigation_help.json | 21 ++++++ erpnext/setup/workspace/home/home.json | 13 +++- erpnext/stock/form_tour/item/item.json | 30 +++++++-- 14 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 erpnext/selling/form_tour/customer/customer.json create mode 100644 erpnext/selling/form_tour/quotation/quotation.json create mode 100644 erpnext/setup/form_tour/company/company.json create mode 100644 erpnext/setup/module_onboarding/home/home.json create mode 100644 erpnext/setup/onboarding_step/company_set_up/company_set_up.json create mode 100644 erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json create mode 100644 erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json create mode 100644 erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json create mode 100644 erpnext/setup/onboarding_step/create_an_item/create_an_item.json create mode 100644 erpnext/setup/onboarding_step/data_import/data_import.json create mode 100644 erpnext/setup/onboarding_step/letterhead/letterhead.json create mode 100644 erpnext/setup/onboarding_step/navigation_help/navigation_help.json diff --git a/erpnext/selling/form_tour/customer/customer.json b/erpnext/selling/form_tour/customer/customer.json new file mode 100644 index 0000000000..1de45b7f5d --- /dev/null +++ b/erpnext/selling/form_tour/customer/customer.json @@ -0,0 +1,29 @@ +{ + "creation": "2021-11-23 10:44:13.185982", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-23 10:54:09.602358", + "modified_by": "Administrator", + "module": "Selling", + "name": "Customer", + "owner": "Administrator", + "reference_doctype": "Customer", + "save_on_complete": 1, + "steps": [ + { + "description": "Enter the Full Name of the Customer", + "field": "", + "fieldname": "customer_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Full Name", + "parent_field": "", + "position": "Left", + "title": "Full Name" + } + ], + "title": "Customer" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json new file mode 100644 index 0000000000..2a2aa5e63e --- /dev/null +++ b/erpnext/selling/form_tour/quotation/quotation.json @@ -0,0 +1,67 @@ +{ + "creation": "2021-11-23 12:00:36.138824", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-23 12:02:48.010298", + "modified_by": "Administrator", + "module": "Selling", + "name": "Quotation", + "owner": "Administrator", + "reference_doctype": "Quotation", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.", + "field": "", + "fieldname": "quotation_to", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Quotation To", + "parent_field": "", + "position": "Right", + "title": "Quotation To" + }, + { + "description": "Select a specific Customer to whom this quotation will be sent.", + "field": "", + "fieldname": "party_name", + "fieldtype": "Dynamic Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Party", + "parent_field": "", + "position": "Right", + "title": "Party" + }, + { + "child_doctype": "Quotation Item", + "description": "Select an item for which you will be quoting a price.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "parent_fieldname": "items", + "position": "Bottom", + "title": "Items" + }, + { + "description": "You can select pre-populated Sales Taxes and Charges from here.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Sales Taxes and Charges" + } + ], + "title": "Quotation" +} \ No newline at end of file diff --git a/erpnext/setup/form_tour/company/company.json b/erpnext/setup/form_tour/company/company.json new file mode 100644 index 0000000000..c66abc0a72 --- /dev/null +++ b/erpnext/setup/form_tour/company/company.json @@ -0,0 +1,67 @@ +{ + "creation": "2021-11-24 10:17:18.534917", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 1, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-11-24 15:38:21.026582", + "modified_by": "Administrator", + "module": "Setup", + "name": "Company", + "owner": "Administrator", + "reference_doctype": "Company", + "save_on_complete": 0, + "steps": [ + { + "description": "This is the default currency for this company.", + "field": "", + "fieldname": "default_currency", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Currency", + "parent_field": "", + "position": "Right", + "title": "Default Currency" + }, + { + "description": "Here, you can add multiple addresses of the company", + "field": "", + "fieldname": "company_info", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Address & Contact", + "parent_field": "", + "position": "Top", + "title": "Address & Contact" + }, + { + "description": "Here, you can set default Accounts, which will ease the creation of accounting entries.", + "field": "", + "fieldname": "default_settings", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts Settings", + "parent_field": "", + "position": "Top", + "title": "Accounts Settings" + }, + { + "description": "This setting is recommended if you wish to track the real-time stock balance in your books of account. This will allow the creation of a General Ledger entry for every stock transaction.", + "field": "", + "fieldname": "enable_perpetual_inventory", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Enable Perpetual Inventory", + "parent_field": "", + "position": "Right", + "title": "Enable Perpetual Inventory" + } + ], + "title": "Company" +} \ No newline at end of file diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json new file mode 100644 index 0000000000..fbff98ff8a --- /dev/null +++ b/erpnext/setup/module_onboarding/home/home.json @@ -0,0 +1,62 @@ +{ + "allow_roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Sales Manager" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + } + ], + "creation": "2021-11-22 12:19:15.888642", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", + "idx": 0, + "is_complete": 0, + "modified": "2021-11-24 16:40:15.154081", + "modified_by": "Administrator", + "module": "Setup", + "name": "Home", + "owner": "Administrator", + "steps": [ + { + "step": "Company Set Up" + }, + { + "step": "Create an Item" + }, + { + "step": "Create a Customer" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Navigation Help" + }, + { + "step": "Data import" + }, + { + "step": "Create a Quotation" + }, + { + "step": "Letterhead" + } + ], + "subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation", + "success_message": "Masters are all set up!", + "title": "Let's Set Up Some Masters" +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json new file mode 100644 index 0000000000..dc85189764 --- /dev/null +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let's create a Company", + "creation": "2021-11-22 11:55:48.931427", + "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:41:01.620963", + "modified_by": "Administrator", + "name": "Company Set Up", + "owner": "Administrator", + "reference_document": "Company", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Set Up a Company", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json new file mode 100644 index 0000000000..c3162f2b87 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Customer", + "creation": "2020-05-14 17:46:41.831517", + "description": "The Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:20.882675", + "modified_by": "Administrator", + "name": "Create a Customer", + "owner": "Administrator", + "reference_document": "Customer", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Manage Customers", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json new file mode 100644 index 0000000000..12d4372470 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Quotation", + "creation": "2020-06-01 13:34:58.958641", + "description": "# Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:55:28.950596", + "modified_by": "Administrator", + "name": "Create a Quotation", + "owner": "Administrator", + "reference_document": "Quotation", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create your first Quotation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 0000000000..cc87d97ae4 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Supplier", + "creation": "2020-05-14 22:09:10.043554", + "description": "# Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:36.656949", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Manage Suppliers", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json new file mode 100644 index 0000000000..0d650de665 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "Create a new Item", + "creation": "2021-05-17 13:47:18.515052", + "description": "Item is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:51:21.446969", + "modified_by": "Administrator", + "name": "Create an Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Manage Items", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json new file mode 100644 index 0000000000..5f4e8db4ab --- /dev/null +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "action_label": "Learn more about data migration", + "creation": "2021-05-19 05:29:16.809610", + "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:54:37.622063", + "modified_by": "Administrator", + "name": "Data import", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Import Data from Spreadsheet", + "validate_action": 1, + "video_url": "https://youtu.be/DQyqeurPI64" +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json new file mode 100644 index 0000000000..b8d430365f --- /dev/null +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s setup your first Letter Head", + "creation": "2021-11-22 12:36:34.583783", + "description": "# Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 16:19:25.379798", + "modified_by": "Administrator", + "name": "Letterhead", + "owner": "Administrator", + "reference_document": "Letter Head", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Setup Your Letterhead", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json new file mode 100644 index 0000000000..1271a86cb9 --- /dev/null +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "action_label": "Learn about Navigation options", + "creation": "2021-11-22 12:09:52.233872", + "description": "Ease of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:40.641949", + "modified_by": "Administrator", + "name": "Navigation Help", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "How to Navigate in ERPNext", + "validate_action": 1, + "video_url": "https://youtu.be/j60xyNFqX_A" +} \ No newline at end of file diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 4e1ccf9b94..f9c585c015 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -1,13 +1,18 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", "creation": "2020-01-23 13:46:38.833076", + "developer_mode_only": 0, + "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends_another_page": 0, "for_user": "", "hide_custom": 0, "icon": "getting-started", "idx": 0, + "is_default": 0, + "is_standard": 0, "label": "Home", "links": [ { @@ -271,12 +276,14 @@ "type": "Link" } ], - "modified": "2021-08-10 15:33:20.704741", + "modified": "2021-11-22 12:50:15.771366", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "parent_page": "", + "pin_to_bottom": 0, + "pin_to_top": 0, "public": 1, "restrict_to_domain": "", "roles": [], @@ -309,4 +316,4 @@ } ], "title": "Home" -} \ No newline at end of file +} diff --git a/erpnext/stock/form_tour/item/item.json b/erpnext/stock/form_tour/item/item.json index 821e91b28d..5369366edb 100644 --- a/erpnext/stock/form_tour/item/item.json +++ b/erpnext/stock/form_tour/item/item.json @@ -2,15 +2,17 @@ "creation": "2021-08-24 17:56:40.754909", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-24 18:04:50.928431", + "modified": "2021-11-24 17:59:44.559001", "modified_by": "Administrator", "module": "Stock", "name": "Item", "owner": "Administrator", "reference_doctype": "Item", - "save_on_complete": 0, + "save_on_complete": 1, "steps": [ { "description": "Enter code for Asset Item", @@ -36,14 +38,27 @@ "position": "Bottom", "title": "Asset Item Name" }, + { + "description": "Select an Item Group", + "field": "", + "fieldname": "item_group", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Group", + "parent_field": "", + "position": "Right", + "title": "Item Group" + }, { "description": "Check this field to make this an Asset Item", "field": "", "fieldname": "is_fixed_asset", "fieldtype": "Check", - "has_next_condition": 0, + "has_next_condition": 1, "is_table_field": 0, "label": "Is Fixed Asset", + "next_step_condition": "eval:doc.is_fixed_asset", "parent_field": "", "position": "Bottom", "title": "Is this a Fixed Asset?" @@ -53,9 +68,10 @@ "field": "", "fieldname": "auto_create_assets", "fieldtype": "Check", - "has_next_condition": 0, + "has_next_condition": 1, "is_table_field": 0, "label": "Auto Create Assets on Purchase", + "next_step_condition": "eval:doc.auto_create_assets", "parent_field": "", "position": "Bottom", "title": "Auto Create Asset on Purchase" @@ -69,7 +85,7 @@ "is_table_field": 0, "label": "Asset Category", "parent_field": "", - "position": "Bottom", + "position": "Left", "title": "Asset Category" }, { @@ -81,9 +97,9 @@ "is_table_field": 0, "label": "Asset Naming Series", "parent_field": "", - "position": "Bottom", + "position": "Left", "title": "Asset Naming Series" } ], "title": "Item" -} +} \ No newline at end of file From c46c8dd6c57a1d12df977b02c14affaf479e1bb3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 19:23:31 +0530 Subject: [PATCH 028/247] feat: do not change variance if response or resolution is set --- .../service_level_agreement.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9dfe339c51..a25608b275 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -515,17 +515,21 @@ def set_service_level_agreement_variance(doctype, doc=None): if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) - frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) + else: + variance = round(time_diff_in_seconds(current_doc.response_by, current_doc.first_responded_on), 2) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) + if variance < 0: + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) - frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) + elif apply_sla_for_resolution and current_doc.get("resolution_date"): + variance = round(time_diff_in_seconds(current_doc.resolution_by, current_doc.get("resolution_date")), 2) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) + if variance < 0: + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) def set_user_resolution_time(doc, meta): @@ -808,10 +812,18 @@ def update_agreement_status_on_custom_status(doc): # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) + if meta.has_field("first_responded_on") and doc.first_responded_on: + # first_responded_on set when first reply is sent to customer + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) + if meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) + if meta.has_field("resolution_date") and doc.resolution_date: + # resolution_date set when issue has been closed + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) + if meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" @@ -857,6 +869,8 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) + elif meta.has_field("response_by_variance") and doc.get('first_responded_on'): + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): From 8370042f82478a85fb29faded0e8b7423874bf21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:05:54 +0530 Subject: [PATCH 029/247] fix: Reset indices in the Packed/Bundle Items table on deleting Product Bundles --- erpnext/stock/doctype/packed_item/packed_item.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 095457f04c..0ce8ee4584 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -106,11 +106,15 @@ def cleanup_packing_list(doc, parent_items): if not delete_list: return doc + index = 1 packed_items = doc.get("packed_items") doc.set("packed_items", []) + for d in packed_items: if d not in delete_list: + d.idx = index doc.append("packed_items", d) + index += 1 def update_product_bundle_price(doc, parent_items): """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" From 325923afc7da6a9f9296a30b56c17f701af29881 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:47:06 +0530 Subject: [PATCH 030/247] fix: Test that indices are reset for Packed/Bundle Items when Product Bundles are removed from the Items table --- .../doctype/quotation/test_quotation.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 769e0661b1..02764b6c07 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -302,6 +302,59 @@ class TestQuotation(unittest.TestCase): enable_calculate_bundle_price(enable=0) + def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Product Bundle 1", {"is_stock_item": 0}) + make_item("_Test Product Bundle 2", {"is_stock_item": 0}) + make_item("_Test Product Bundle 3", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Bundle Item 3", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle 1", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", + ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 3", + ["_Test Bundle Item 3", "_Test Bundle Item 1"]) + + item_list = [ + { + "item_code": "_Test Product Bundle 1", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 2", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 3", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + ] + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + del quotation.items[1] + quotation.save() + + for id, item in enumerate(quotation.packed_items): + expected_index = id + 1 + self.assertEqual(item.idx, expected_index) + test_records = frappe.get_test_records('Quotation') def enable_calculate_bundle_price(enable=1): From adfd519139e1010b87375c668ad52bcc155d9594 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:58:05 +0530 Subject: [PATCH 031/247] fix: Test Product Bundle price calculation when there are multiple Product Bundles --- .../doctype/quotation/test_quotation.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 02764b6c07..996015b1a0 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -302,6 +302,56 @@ class TestQuotation(unittest.TestCase): enable_calculate_bundle_price(enable=0) + def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Product Bundle 1", {"is_stock_item": 0}) + make_item("_Test Product Bundle 2", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Bundle Item 3", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle 1", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", + ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + + enable_calculate_bundle_price() + + item_list = [ + { + "item_code": "_Test Product Bundle 1", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Product Bundle 2", + "warehouse": "", + "qty": 1, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.packed_items[0].rate = 100 + quotation.packed_items[1].rate = 200 + quotation.packed_items[2].rate = 200 + quotation.packed_items[3].rate = 300 + quotation.save() + + expected_values = [300, 500] + + for item in quotation.items: + self.assertEqual(item.amount, expected_values[item.idx-1]) + + enable_calculate_bundle_price(enable=0) + def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.item.test_item import make_item From c9743185c69d305e6f7a5ced8ff611479abb1983 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 21:58:42 +0530 Subject: [PATCH 032/247] fix: Remove unnecessary comma --- erpnext/selling/doctype/quotation/test_quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 996015b1a0..aa83726304 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -394,7 +394,7 @@ class TestQuotation(unittest.TestCase): "rate": 400, "delivered_by_supplier": 1, "supplier": '_Test Supplier' - }, + } ] quotation = make_quotation(item_list=item_list, do_not_submit=1) From 5ba3b28d69c88675f41830b8a490882f5298dc07 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 25 Nov 2021 15:42:30 +0530 Subject: [PATCH 033/247] fix(refactor): Advance tds allocation to purchase invoice --- .../accounts/doctype/advance_tax/__init__.py | 0 .../doctype/advance_tax/advance_tax.json | 56 +++++++++++++ .../doctype/advance_tax/advance_tax.py | 9 ++ .../doctype/payment_entry/payment_entry.json | 12 +-- .../doctype/payment_entry/payment_entry.py | 33 +++----- .../purchase_invoice/purchase_invoice.json | 11 ++- .../purchase_invoice/purchase_invoice.py | 42 +++++++++- .../doctype/sales_invoice/sales_invoice.py | 2 - .../tax_withholding_category.py | 33 +++++++- erpnext/accounts/general_ledger.py | 20 +++++ erpnext/controllers/accounts_controller.py | 82 +------------------ 11 files changed, 179 insertions(+), 121 deletions(-) create mode 100644 erpnext/accounts/doctype/advance_tax/__init__.py create mode 100644 erpnext/accounts/doctype/advance_tax/advance_tax.json create mode 100644 erpnext/accounts/doctype/advance_tax/advance_tax.py diff --git a/erpnext/accounts/doctype/advance_tax/__init__.py b/erpnext/accounts/doctype/advance_tax/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.json b/erpnext/accounts/doctype/advance_tax/advance_tax.json new file mode 100644 index 0000000000..68706aba88 --- /dev/null +++ b/erpnext/accounts/doctype/advance_tax/advance_tax.json @@ -0,0 +1,56 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-25 10:24:39.836195", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "reference_detail", + "account_head", + "allocated_amount" + ], + "fields": [ + { + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Reference Name", + "options": "reference_type" + }, + { + "fieldname": "reference_detail", + "fieldtype": "Data", + "label": "Reference Detail" + }, + { + "fieldname": "account_head", + "fieldtype": "Link", + "label": "Account Head", + "options": "Account" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "label": "Allocated Amount", + "options": "party_account_currency" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-11-25 10:27:51.712286", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Advance Tax", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.py b/erpnext/accounts/doctype/advance_tax/advance_tax.py new file mode 100644 index 0000000000..2e784efd8f --- /dev/null +++ b/erpnext/accounts/doctype/advance_tax/advance_tax.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AdvanceTax(Document): + pass diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index ee2e319a6f..c8d1db91f5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -61,7 +61,6 @@ "taxes_and_charges_section", "purchase_taxes_and_charges_template", "sales_taxes_and_charges_template", - "advance_tax_account", "column_break_55", "apply_tax_withholding_amount", "tax_withholding_category", @@ -685,15 +684,6 @@ "fieldtype": "Section Break", "hide_border": 1 }, - { - "depends_on": "eval:doc.apply_tax_withholding_amount", - "description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices", - "fieldname": "advance_tax_account", - "fieldtype": "Link", - "label": "Advance Tax Account", - "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount", - "options": "Account" - }, { "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", @@ -730,7 +720,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-10-22 17:50:24.632806", + "modified": "2021-11-24 18:58:24.919764", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 26fd16a4fd..e524592382 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -20,7 +20,7 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_ban from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices from erpnext.controllers.accounts_controller import ( @@ -433,9 +433,6 @@ class PaymentEntry(AccountsController): if not self.apply_tax_withholding_amount: return - if not self.advance_tax_account: - frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - net_total = self.paid_amount for reference in self.get("references"): @@ -455,13 +452,12 @@ class PaymentEntry(AccountsController): 'net_total': net_total }) - tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) + tax_withholding_details, tax_deducted_on_advances = get_party_tax_withholding_details(args, self.tax_withholding_category) if not tax_withholding_details: return tax_withholding_details.update({ - 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -689,6 +685,7 @@ class PaymentEntry(AccountsController): self.add_deductions_gl_entries(gl_entries) self.add_tax_gl_entries(gl_entries) + gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) def add_party_gl_entries(self, gl_entries): @@ -752,7 +749,8 @@ class PaymentEntry(AccountsController): "against": self.party if self.payment_type=="Pay" else self.paid_to, "credit_in_account_currency": self.paid_amount, "credit": self.base_paid_amount, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "post_net_value": True }, item=self) ) if self.payment_type in ("Receive", "Internal Transfer"): @@ -782,14 +780,10 @@ class PaymentEntry(AccountsController): rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_to - payment_or_advance_account = self.get_party_account_for_taxes() + payment_account = self.get_party_account_for_taxes() tax_amount = d.tax_amount base_tax_amount = d.base_tax_amount - if self.advance_tax_account: - tax_amount = -1 * tax_amount - base_tax_amount = -1 * base_tax_amount - gl_entries.append( self.get_gl_dict({ "account": d.account_head, @@ -798,19 +792,21 @@ class PaymentEntry(AccountsController): dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, - "cost_center": d.cost_center + "cost_center": d.cost_center, + "post_net_value": True, }, account_currency, item=d)) - if not d.included_in_paid_amount or self.advance_tax_account: + if not d.included_in_paid_amount: gl_entries.append( self.get_gl_dict({ - "account": payment_or_advance_account, + "account": payment_account, "against": against, rev_dr_or_cr: tax_amount, rev_dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, + "post_net_value": True, }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -832,9 +828,7 @@ class PaymentEntry(AccountsController): ) def get_party_account_for_taxes(self): - if self.advance_tax_account: - return self.advance_tax_account - elif self.payment_type == 'Receive': + if self.payment_type == 'Receive': return self.paid_to elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_from @@ -1603,9 +1597,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.apply_tax_withholding_amount = 1 pe.tax_withholding_category = doc.tax_withholding_category - if not pe.advance_tax_account: - pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account') - return pe def get_bank_cash_account(doc, bank_account): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 03cbc4acbc..bd0116443f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -130,6 +130,7 @@ "allocate_advances_automatically", "get_advances", "advances", + "advance_tax", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", @@ -1408,13 +1409,21 @@ { "fieldname": "column_break_147", "fieldtype": "Column Break" + }, + { + "fieldname": "advance_tax", + "fieldtype": "Table", + "hidden": 1, + "label": "Advance Tax", + "options": "Advance Tax", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-10-12 20:55:16.145651", + "modified": "2021-11-25 13:31:02.716727", "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 62e3dc8346..516133ab63 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -427,6 +427,7 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.update_advance_tax_references() self.process_common_party_accounting() @@ -472,8 +473,6 @@ class PurchaseInvoice(BuyingController): self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) - self.allocate_advance_taxes(gl_entries) - gl_entries = make_regional_gl_entries(gl_entries, self) gl_entries = merge_similar_entries(gl_entries) @@ -1074,6 +1073,7 @@ class PurchaseInvoice(BuyingController): unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.update_advance_tax_references(cancel=1) def update_project(self): project_list = [] @@ -1150,7 +1150,10 @@ class PurchaseInvoice(BuyingController): if not self.tax_withholding_category: return - tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) + tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category) + + # Adjust TDS paid on advances + self.allocate_advance_tds(tax_withholding_details, advance_taxes) if not tax_withholding_details: return @@ -1174,6 +1177,39 @@ class PurchaseInvoice(BuyingController): # calculate totals again after applying TDS self.calculate_taxes_and_totals() + def allocate_advance_tds(self, tax_withholding_details, advance_taxes): + self.set('advance_tax', []) + for tax in advance_taxes: + allocated_amount = 0 + pending_amount = flt(tax.tax_amount - tax.allocated_amount) + if flt(tax_withholding_details.get('tax_amount')) >= pending_amount: + tax_withholding_details['tax_amount'] -= pending_amount + allocated_amount = pending_amount + elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount: + allocated_amount = tax_withholding_details['tax_amount'] + tax_withholding_details['tax_amount'] = 0 + + self.append('advance_tax', { + 'reference_type': 'Payment Entry', + 'reference_name': tax.parent, + 'reference_detail': tax.name, + 'account_head': tax.account_head, + 'allocated_amount': allocated_amount + }) + + def update_advance_tax_references(self, cancel=0): + for tax in self.get('advance_tax'): + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + if cancel: + frappe.qb.update(at).set( + at.allocated_amount, at.allocated_amount - tax.allocated_amount + ).where(at.name == tax.reference_detail).run() + else: + frappe.qb.update(at).set( + at.allocated_amount, at.allocated_amount + tax.allocated_amount + ).where(at.name == tax.reference_detail).run() + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get('amended_from'): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 59d46fc2e8..c4d59f1ef7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -842,8 +842,6 @@ class SalesInvoice(SellingController): self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) - self.allocate_advance_taxes(gl_entries) - self.make_item_gl_entries(gl_entries) self.make_discount_gl_entries(gl_entries) 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 dc1818a052..fe156cb029 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -95,7 +95,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') .format(tax_withholding_category, inv.company, party)) - tax_amount, tax_deducted = get_tax_amount( + tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( party_type, parties, inv, tax_details, posting_date, pan_no @@ -106,7 +106,10 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) - return tax_row + if inv.doctype == 'Purchase Invoice': + return tax_row, tax_deducted_on_advances + else: + return tax_row def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) @@ -194,6 +197,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, to_date=tax_details.to_date, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers + tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 if taxable_vouchers: @@ -223,7 +227,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if cint(tax_details.round_off_tax_amount): tax_amount = round(tax_amount) - return tax_amount, tax_deducted + return tax_amount, tax_deducted, tax_deducted_on_advances def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' @@ -281,6 +285,29 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] +def get_taxes_deducted_on_advances_allocated(inv, tax_details): + advances = [d.reference_name for d in inv.get('advances')] + tax_info = [] + + if advances: + pe = frappe.qb.DocType("Payment Entry").as_("pe") + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + tax_info = frappe.qb.from_(at).inner_join(pe).on( + pe.name == at.parent + ).select( + at.parent, at.name, at.tax_amount, at.allocated_amount + ).where( + pe.tax_withholding_category == tax_details.get('tax_withholding_category') + ).where( + at.parent.isin(advances) + ).where( + at.account_head == tax_details.account_head + ).run(as_dict=True) + + return tax_info + + def get_deducted_tax(taxable_vouchers, tax_details): # check if TDS / TCS account is already charged on taxable vouchers filters = { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 8ef7d7e103..1836db6477 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -73,8 +73,28 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency) entry.credit_in_account_currency = 0.0 + update_net_values(entry) + return gl_map +def update_net_values(entry): + # In some scenarios net value needs to be shown in the ledger + # This method updates net values as debit or credit + if entry.post_net_value and entry.debit and entry.credit: + if entry.debit > entry.credit: + entry.debit = entry.debit - entry.credit + entry.debit_in_account_currency = entry.debit_in_account_currency \ + - entry.credit_in_account_currency + entry.credit = 0 + entry.credit_in_account_currency = 0 + else: + entry.credit = entry.credit - entry.debit + entry.credit_in_account_currency = entry.credit_in_account_currency \ + - entry.debit_in_account_currency + + entry.debit = 0 + entry.debit_in_account_currency = 0 + def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 77503a8ee5..9cf2e33916 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -524,7 +524,8 @@ class AccountsController(TransactionBase): 'is_opening': self.get("is_opening") or "No", 'party_type': None, 'party': None, - 'project': self.get("project") + 'project': self.get("project"), + 'post_net_value': args.get('post_net_value') }) accounting_dimensions = get_accounting_dimensions() @@ -805,7 +806,6 @@ class AccountsController(TransactionBase): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries if self.doctype in ["Sales Invoice", "Purchase Invoice"]: - self.update_allocated_advance_taxes_on_cancel() if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): unlink_ref_doc_from_payment_entries(self) @@ -853,29 +853,6 @@ class AccountsController(TransactionBase): return tax_map - def update_allocated_advance_taxes_on_cancel(self): - if self.get('advances'): - tax_accounts = [d.account_head for d in self.get('taxes')] - allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'], - filters={'voucher_no': self.name, 'account': ('in', tax_accounts)}, - group_by='account', as_list=1)) - - tax_map = self.get_tax_map() - - for pe in self.get('advances'): - if pe.reference_type == 'Payment Entry': - pe = frappe.get_doc('Payment Entry', pe.reference_name) - for tax in pe.get('taxes'): - allocated_amount = flt(tax_map.get(tax.account_head)) - flt(allocated_tax_map.get(tax.account_head)) - if allocated_amount > tax.tax_amount: - allocated_amount = tax.tax_amount - - if allocated_amount: - frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount', - tax.allocated_amount - allocated_amount) - tax_map[tax.account_head] -= allocated_amount - allocated_tax_map[tax.account_head] -= allocated_amount - def get_amount_and_base_amount(self, item, enable_discount_accounting): amount = item.net_amount base_amount = item.base_net_amount @@ -959,61 +936,6 @@ class AccountsController(TransactionBase): }, item=self) ) - def allocate_advance_taxes(self, gl_entries): - tax_map = self.get_tax_map() - for pe in self.get("advances"): - if pe.reference_type == "Payment Entry" and \ - frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'): - pe = frappe.get_doc("Payment Entry", pe.reference_name) - advance_tax_account = pe.advance_tax_account - - for tax in pe.get("taxes"): - account_currency = get_account_currency(tax.account_head) - - if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - advance_tax_account = pe.advance_tax_account if pe.paid_from != pe.advance_tax_account \ - else self.credit_to - else: - dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - - party = self.supplier if self.doctype == "Purchase Invoice" else self.customer - unallocated_amount = tax.tax_amount - tax.allocated_amount - if tax_map.get(tax.account_head): - amount = tax_map.get(tax.account_head) - if amount < unallocated_amount: - unallocated_amount = amount - - gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": party, - dr_or_cr: unallocated_amount, - dr_or_cr + "_in_account_currency": unallocated_amount - if account_currency==self.company_currency - else unallocated_amount, - "cost_center": tax.cost_center - }, account_currency, item=tax)) - - gl_entries.append( - self.get_gl_dict({ - "account": advance_tax_account, - "against": party, - rev_dr_cr: unallocated_amount, - rev_dr_cr + "_in_account_currency": unallocated_amount - if account_currency==self.company_currency - else unallocated_amount, - "cost_center": self.get('cost_center') if advance_tax_account == self.get('credit_to') else tax.cost_center, - "party_type": 'Supplier' if advance_tax_account == self.get('credit_to') else '', - "party": self.get('supplier') if advance_tax_account == self.get('credit_to') else '', - }, account_currency, item=tax)) - - frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount", - tax.allocated_amount + unallocated_amount) - - tax_map[tax.account_head] -= unallocated_amount def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for From c9e79ef1f232ac517277d4c3fb6bf9d8ace58abf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 18:53:48 +0530 Subject: [PATCH 034/247] fix: Replace 'item' with 'item_code' in tests --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 81b4f6c449..ed5ae2cb4d 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -70,7 +70,7 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.stock_entry_type, "Material Issue") self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) - self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) def test_increase_in_asset_value_due_to_stock_consumption(self): @@ -139,7 +139,7 @@ def create_asset_repair(**args): asset_repair.stock_consumption = 1 asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { - "item": args.item or args.item_code or "_Test Item", + "item_code": args.item_code or "_Test Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, "consumed_quantity": args.qty or 1 }) @@ -158,7 +158,7 @@ def create_asset_repair(**args): }) stock_entry.append('items', { "t_warehouse": asset_repair.warehouse, - "item_code": asset_repair.stock_items[0].item, + "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity }) stock_entry.submit() From eea80b6c01df479fbbc6cbc13edc4a7e3fadc2c4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 19:01:22 +0530 Subject: [PATCH 035/247] fix: Create stock item --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index ed5ae2cb4d..7efb03083e 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -11,12 +11,14 @@ from erpnext.assets.doctype.asset.test_asset import ( create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.stock.doctype.item.test_item import create_item class TestAssetRepair(unittest.TestCase): def setUp(self): set_depreciation_settings_in_company() create_asset_data() + create_item("_Test Stock Item") frappe.db.sql("delete from `tabTax Rule`") def test_update_status(self): @@ -139,7 +141,7 @@ def create_asset_repair(**args): asset_repair.stock_consumption = 1 asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { - "item_code": args.item_code or "_Test Item", + "item_code": args.item_code or "_Test Stock Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, "consumed_quantity": args.qty or 1 }) From efac7b0904ff5cc24fdc525c399ada8509f07882 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 19:02:01 +0530 Subject: [PATCH 036/247] fix: Create setUpClass --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 7efb03083e..0ef2d800bb 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -15,7 +15,8 @@ from erpnext.stock.doctype.item.test_item import create_item class TestAssetRepair(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): set_depreciation_settings_in_company() create_asset_data() create_item("_Test Stock Item") From 6b75d1439f053a1d07ef54a3fdf55964aefa39c4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 22:10:24 +0530 Subject: [PATCH 037/247] fix: Add test for consumption of serialized Assets --- .../doctype/asset_repair/test_asset_repair.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 0ef2d800bb..34c7282ab2 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -5,6 +5,7 @@ import unittest import frappe from frappe.utils import flt, nowdate +from traceback import print_exc from erpnext.assets.doctype.asset.test_asset import ( create_asset, @@ -76,6 +77,25 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + def test_serialized_item_consumption(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError + + stock_entry = make_serialized_item() + serial_nos = stock_entry.get("items")[0].serial_no + serial_no = serial_nos.split("\n")[0] + + # should not raise any error + create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code, + warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1) + + # should raise error + asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC", + item_code = stock_entry.get("items")[0].item_code) + + asset_repair.repair_status = "Completed" + self.assertRaises(SerialNoRequiredError, asset_repair.submit) + def test_increase_in_asset_value_due_to_stock_consumption(self): asset = create_asset(calculate_depreciation = 1, submit=1) initial_asset_value = get_asset_value(asset) @@ -140,11 +160,12 @@ def create_asset_repair(**args): if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { "item_code": args.item_code or "_Test Stock Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, - "consumed_quantity": args.qty or 1 + "consumed_quantity": args.qty or 1, + "serial_no": args.serial_no }) asset_repair.insert(ignore_if_duplicate=True) From b28f137ee8d580a480bca0ed772283b62ae3c5b0 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 22:17:54 +0530 Subject: [PATCH 038/247] fix: Remove unused import --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 34c7282ab2..e54329c10b 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -5,7 +5,6 @@ import unittest import frappe from frappe.utils import flt, nowdate -from traceback import print_exc from erpnext.assets.doctype.asset.test_asset import ( create_asset, From c94f1ed39a40b019604a3476782f27a4ad4dab30 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 25 Nov 2021 22:18:57 +0530 Subject: [PATCH 039/247] fix: Change order of import statements --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index e54329c10b..7c0d05748e 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -77,8 +77,8 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) def test_serialized_item_consumption(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item stock_entry = make_serialized_item() serial_nos = stock_entry.get("items")[0].serial_no From f07f010962d51f43bc8f17048b4f3bb74cd925f1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 25 Nov 2021 23:58:16 +0530 Subject: [PATCH 040/247] fix: Add tests --- .../advance_taxes_and_charges.json | 11 ++-------- .../doctype/payment_entry/payment_entry.py | 16 ++------------ .../purchase_invoice/test_purchase_invoice.py | 22 ++++++++++--------- .../tax_withholding_category.py | 5 ++++- erpnext/controllers/accounts_controller.py | 9 ++++---- 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json index 4d63499431..05b284ae16 100644 --- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json +++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json @@ -25,8 +25,7 @@ "allocated_amount", "column_break_13", "base_tax_amount", - "base_total", - "base_allocated_amount" + "base_total" ], "fields": [ { @@ -168,12 +167,6 @@ "label": "Allocated Amount", "options": "currency" }, - { - "fieldname": "base_allocated_amount", - "fieldtype": "Currency", - "label": "Allocated Amount (Company Currency)", - "options": "Company:company:default_currency" - }, { "fetch_from": "account_head.account_currency", "fieldname": "currency", @@ -186,7 +179,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:46:58.373170", + "modified": "2021-11-25 11:10:10.945027", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Taxes and Charges", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e524592382..7aeb872b28 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -435,24 +435,16 @@ class PaymentEntry(AccountsController): net_total = self.paid_amount - for reference in self.get("references"): - net_total_for_tds = 0 - if reference.reference_doctype == 'Purchase Order': - net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) - - if net_total_for_tds: - net_total = net_total_for_tds - # Adding args as purchase invoice to get TDS amount args = frappe._dict({ 'company': self.company, - 'doctype': 'Purchase Invoice', + 'doctype': 'Payment Entry', 'supplier': self.party, 'posting_date': self.posting_date, 'net_total': net_total }) - tax_withholding_details, tax_deducted_on_advances = get_party_tax_withholding_details(args, self.tax_withholding_category) + tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) if not tax_withholding_details: return @@ -1593,10 +1585,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= }) pe.set_difference_amount() - if doc.doctype == 'Purchase Order' and doc.apply_tds: - pe.apply_tax_withholding_amount = 1 - pe.tax_withholding_category = doc.tax_withholding_category - return pe def get_bank_cash_account(doc, bank_account): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 78396a5e58..aa2408e092 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1160,25 +1160,21 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Order with TDS applied po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', posting_date='2021-09-15') - po.apply_tds = 1 - po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() po.submit() - # Update Unrealized Profit / Loss Account which is used as default advance tax account - frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC') - # Create Payment Entry Against the order payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name) payment_entry.paid_from = 'Cash - _TC' + payment_entry.apply_tax_withholding_amount = 1 + payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual' payment_entry.save() payment_entry.submit() # Check GLE for Payment Entry expected_gle = [ - ['_Test Account Excise Duty - _TC', 3000, 0], ['Cash - _TC', 0, 27000], - ['Creditors - _TC', 27000, 0], + ['Creditors - _TC', 30000, 0], ['TDS Payable - _TC', 0, 3000], ] @@ -1204,9 +1200,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Zero net effect on final TDS Payable on invoice expected_gle = [ ['_Test Account Cost for Goods Sold - _TC', 30000], - ['_Test Account Excise Duty - _TC', -3000], - ['Creditors - _TC', -27000], - ['TDS Payable - _TC', 0] + ['Creditors - _TC', -30000] ] gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount @@ -1219,6 +1213,14 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.amount) + payment_entry.load_from_db() + self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000) + + purchase_invoice.cancel() + + payment_entry.load_from_db() + self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date from `tabGL Entry` 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 fe156cb029..5bb9b93185 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -197,7 +197,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, to_date=tax_details.to_date, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers - tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) + tax_deducted_on_advances = 0 + + if inv.doctype == 'Purchase Invoice': + tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 if taxable_vouchers: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9cf2e33916..c5260295c4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -145,15 +145,16 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() + if self.doctype in ['Purchase Invoice', 'Sales Invoice']: + pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid" + if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): + self.set_advances() + if self.doctype == 'Purchase Invoice': self.calculate_paid_amount() # apply tax withholding only if checked and applicable self.set_tax_withholding() - if self.doctype in ['Purchase Invoice', 'Sales Invoice']: - pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid" - if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): - self.set_advances() self.set_advance_gain_or_loss() From 7f06c8ca5768fcd16c077888324cb59244ccf032 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 26 Nov 2021 10:27:57 +0530 Subject: [PATCH 041/247] fix: Incorrect indentation --- erpnext/controllers/accounts_controller.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c5260295c4..56e03e15ec 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -150,12 +150,6 @@ class AccountsController(TransactionBase): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() - if self.doctype == 'Purchase Invoice': - self.calculate_paid_amount() - # apply tax withholding only if checked and applicable - self.set_tax_withholding() - - self.set_advance_gain_or_loss() if self.is_return: @@ -165,6 +159,11 @@ class AccountsController(TransactionBase): self.set_inter_company_account() + if self.doctype == 'Purchase Invoice': + self.calculate_paid_amount() + # apply tax withholding only if checked and applicable + self.set_tax_withholding() + validate_regional(self) if self.doctype != 'Material Request': From ca8dec0cf2d8a74eed1db939f5865f7536431a25 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 26 Nov 2021 13:24:32 +0530 Subject: [PATCH 042/247] fix: Use `get_all` instead of `get_list` for child doctype (#28538) * fix(Student Attendance Tool): Use `get_all` instead of `get_list` for child doctype * fix(Course Schedule): incorrect fetch from value * fix: sider * fix(Gratuity): Use `get_all` instead of `get_list` for child doctype --- erpnext/education/api.py | 14 +- .../course_schedule/course_schedule.json | 449 ++---------------- .../student_attendance_tool.py | 30 +- erpnext/payroll/doctype/gratuity/gratuity.py | 2 +- .../payroll/doctype/gratuity/test_gratuity.py | 4 +- 5 files changed, 67 insertions(+), 432 deletions(-) diff --git a/erpnext/education/api.py b/erpnext/education/api.py index 53d1482f95..d9013b0816 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -125,7 +125,7 @@ def get_student_guardians(student): :param student: Student. """ - guardians = frappe.get_list("Student Guardian", fields=["guardian"] , + guardians = frappe.get_all("Student Guardian", fields=["guardian"] , filters={"parent": student}) return guardians @@ -137,10 +137,10 @@ def get_student_group_students(student_group, include_inactive=0): :param student_group: Student Group. """ if include_inactive: - students = frappe.get_list("Student Group Student", fields=["student", "student_name"] , + students = frappe.get_all("Student Group Student", fields=["student", "student_name"] , filters={"parent": student_group}, order_by= "group_roll_number") else: - students = frappe.get_list("Student Group Student", fields=["student", "student_name"] , + students = frappe.get_all("Student Group Student", fields=["student", "student_name"] , filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") return students @@ -164,7 +164,7 @@ def get_fee_components(fee_structure): :param fee_structure: Fee Structure. """ if fee_structure: - fs = frappe.get_list("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx") + fs = frappe.get_all("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx") return fs @@ -175,7 +175,7 @@ def get_fee_schedule(program, student_category=None): :param program: Program. :param student_category: Student Category """ - fs = frappe.get_list("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] , + fs = frappe.get_all("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] , filters={"parent": program, "student_category": student_category }, order_by= "idx") return fs @@ -220,7 +220,7 @@ def get_assessment_criteria(course): :param Course: Course """ - return frappe.get_list("Course Assessment Criteria", \ + return frappe.get_all("Course Assessment Criteria", fields=["assessment_criteria", "weightage"], filters={"parent": course}, order_by= "idx") @@ -253,7 +253,7 @@ def get_assessment_details(assessment_plan): :param Assessment Plan: Assessment Plan """ - return frappe.get_list("Assessment Plan Criteria", \ + return frappe.get_all("Assessment Plan Criteria", fields=["assessment_criteria", "maximum_score", "docstatus"], filters={"parent": assessment_plan}, order_by= "idx") diff --git a/erpnext/education/doctype/course_schedule/course_schedule.json b/erpnext/education/doctype/course_schedule/course_schedule.json index 8c6746bda8..38d9b508f0 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.json +++ b/erpnext/education/doctype/course_schedule/course_schedule.json @@ -1,520 +1,143 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, "autoname": "naming_series:", - "beta": 0, "creation": "2015-09-09 16:34:04.960369", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Document", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "student_group", + "instructor", + "instructor_name", + "column_break_2", + "naming_series", + "course", + "color", + "section_break_6", + "schedule_date", + "room", + "column_break_9", + "from_time", + "to_time", + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "student_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Student Group", - "length": 0, - "no_copy": 0, "options": "Student Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "instructor", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Instructor", - "length": 0, - "no_copy": 0, "options": "Instructor", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "instructor.Instructor_name", + "fetch_from": "instructor.instructor_name", "fieldname": "instructor_name", "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Instructor Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "naming_series", "fieldtype": "Select", - "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": "Naming Series", - "length": 0, - "no_copy": 0, "options": "EDU-CSH-.YYYY.-", - "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": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "course", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Course", - "length": 0, - "no_copy": 0, "options": "Course", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "color", "fieldtype": "Color", - "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": "Color", - "length": 0, - "no_copy": 0, - "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 + "print_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", "fieldname": "schedule_date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Schedule 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Schedule Date" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "room", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Room", - "length": 0, - "no_copy": 0, "options": "Room", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "From Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_time", "fieldtype": "Time", - "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": "To Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "title", "fieldtype": "Data", "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": "Title", - "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": "Title" } ], - "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, - "menu_index": 0, - "modified": "2018-08-21 14:44:51.827225", + "links": [], + "modified": "2021-11-24 11:57:08.164449", "modified_by": "Administrator", "module": "Education", "name": "Course Schedule", - "name_case": "", + "naming_rule": "By \"Naming Series\" field", "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": "Academics User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "restrict_to_domain": "Education", - "show_name_in_global_search": 0, "sort_field": "schedule_date", "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py index 4e3f98d655..7deb6b18da 100644 --- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py +++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py @@ -14,24 +14,36 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour student_list = [] student_attendance_list = [] - if based_on=="Course Schedule": + if based_on == "Course Schedule": student_group = frappe.db.get_value("Course Schedule", course_schedule, "student_group") if student_group: - student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \ + student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"], filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") if not student_list: - student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , + student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"], filters={"parent": student_group, "active": 1}, order_by= "group_roll_number") + table = frappe.qb.DocType("Student Attendance") + if course_schedule: - student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \ - course_schedule= %s''', (course_schedule), as_dict=1) + student_attendance_list = ( + frappe.qb.from_(table) + .select(table.student, table.status) + .where( + (table.course_schedule == course_schedule) + ) + ).run(as_dict=True) else: - student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \ - student_group= %s and date= %s and \ - (course_schedule is Null or course_schedule='')''', - (student_group, date), as_dict=1) + student_attendance_list = ( + frappe.qb.from_(table) + .select(table.student, table.status) + .where( + (table.student_group == student_group) + & (table.date == date) + & (table.course_schedule == "") | (table.course_schedule.isnull()) + ) + ).run(as_dict=True) for attendance in student_attendance_list: for student in student_list: diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index f563c08a04..476990a88e 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -193,7 +193,7 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen sal_slip = get_last_salary_slip(employee) if not sal_slip: frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) - component_and_amounts = frappe.get_list("Salary Detail", + component_and_amounts = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 78355cae64..93cba06da1 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -48,7 +48,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation - component_amount = frappe.get_list("Salary Detail", + component_amount = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, @@ -84,7 +84,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation - component_amount = frappe.get_list("Salary Detail", + component_amount = frappe.get_all("Salary Detail", filters={ "docstatus": 1, 'parent': sal_slip, From 210f593a492ba4332c526fac6579df9adc3afc1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 15:49:17 +0530 Subject: [PATCH 043/247] refactor: SLA form fields --- .../service_level_agreement.js | 31 ++++++++++ .../service_level_agreement.json | 61 ++++++++----------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index ae2080c3b5..7e260dbbf5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -22,10 +22,41 @@ frappe.ui.form.on('Service Level Agreement', { refresh: function(frm) { frm.trigger('fetch_status_fields'); frm.trigger('toggle_resolution_fields'); + frm.trigger('default_service_level_agreement'); + frm.trigger('entity'); + }, + + default_service_level_agreement: function(frm) { + const field = frm.get_field('default_service_level_agreement'); + if (frm.doc.default_service_level_agreement) { + field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type])); + } else { + field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type])); + } }, document_type: function(frm) { frm.trigger('fetch_status_fields'); + frm.trigger('default_service_level_agreement'); + }, + + entity_type: function(frm) { + frm.set_value('entity', undefined); + }, + + entity: function(frm) { + const field = frm.get_field('entity'); + if (frm.doc.entity) { + const and_descendants = frm.doc.entity_type != 'Customer' ? __(' or its descendants') : ''; + field.set_description( + __('SLA will be applied if {1} is set as {2}{3}', [ + frm.doc.document_type, frm.doc.entity_type, + frm.doc.entity, and_descendants + ]) + ); + } else { + field.set_description(''); + } }, fetch_status_fields: function(frm) { diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 5f470aad67..1698e2380f 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -6,22 +6,17 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "enabled", - "section_break_2", "document_type", - "default_service_level_agreement", "default_priority", "column_break_2", "service_level", - "holiday_list", - "entity_section", - "entity_type", - "column_break_10", - "entity", + "enabled", "filters_section", - "condition", + "default_service_level_agreement", + "entity_type", + "entity", "column_break_15", - "condition_description", + "condition", "agreement_details_section", "start_date", "column_break_7", @@ -31,8 +26,10 @@ "priorities", "status_details", "sla_fulfilled_on", + "column_break_22", "pause_sla_on", "support_and_resolution_section_break", + "holiday_list", "support_and_resolution" ], "fields": [ @@ -42,7 +39,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Service Level Name", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "fieldname": "holiday_list", @@ -56,10 +54,10 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: !doc.default_service_level_agreement", + "depends_on": "eval: doc.document_type", "fieldname": "agreement_details_section", "fieldtype": "Section Break", - "label": "Agreement Details" + "label": "Valid From" }, { "fieldname": "start_date", @@ -72,7 +70,6 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "end_date", "fieldtype": "Date", "label": "End Date" @@ -80,7 +77,7 @@ { "fieldname": "response_and_resolution_time_section", "fieldtype": "Section Break", - "label": "Response and Resolution Time" + "label": "Response and Resolution" }, { "fieldname": "support_and_resolution_section_break", @@ -90,6 +87,7 @@ { "fieldname": "support_and_resolution", "fieldtype": "Table", + "label": "Working Hours", "options": "Service Day", "reqd": 1 }, @@ -101,10 +99,7 @@ "reqd": 1 }, { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { + "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "entity", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -114,22 +109,12 @@ }, { "depends_on": "eval: !doc.default_service_level_agreement", - "fieldname": "entity_section", - "fieldtype": "Section Break", - "label": "Entity" - }, - { "fieldname": "entity_type", "fieldtype": "Select", "in_standard_filter": 1, "label": "Entity Type", "options": "\nCustomer\nCustomer Group\nTerritory" }, - { - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "default": "0", "fieldname": "default_service_level_agreement", @@ -152,7 +137,7 @@ { "fieldname": "document_type", "fieldtype": "Link", - "label": "Document Type", + "label": "Apply On", "options": "DocType", "reqd": 1, "set_only_once": 1 @@ -164,6 +149,7 @@ "label": "Enabled" }, { + "depends_on": "document_type", "fieldname": "status_details", "fieldtype": "Section Break", "label": "Status Details" @@ -182,28 +168,31 @@ "label": "Apply SLA for Resolution Time" }, { + "depends_on": "document_type", "fieldname": "filters_section", "fieldtype": "Section Break", - "label": "Assignment Condition" + "label": "Assignment Conditions" }, { "fieldname": "column_break_15", "fieldtype": "Column Break" }, { + "depends_on": "eval: !doc.default_service_level_agreement", + "description": "Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'", "fieldname": "condition", "fieldtype": "Code", "label": "Condition", - "options": "Python" + "max_height": "7rem", + "options": "PythonExpression" }, { - "fieldname": "condition_description", - "fieldtype": "HTML", - "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
" + "fieldname": "column_break_22", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2021-10-02 11:32:55.556024", + "modified": "2021-11-26 15:45:33.289911", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", From efec85d5cd59eacd8cce4d519caf914740cc6c12 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 26 Nov 2021 16:29:21 +0530 Subject: [PATCH 044/247] chore: correct __version__ on develop branch. (#28582) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 9d99ebb7bb..a5de50fb06 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.9.0' +__version__ = '14.0.0-dev' def get_default_company(user=None): '''Get default company for user''' From 7ff30a4b2b64df191836f0d7695c7007167fb076 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 26 Nov 2021 17:33:57 +0530 Subject: [PATCH 045/247] fix: incorrect balance for warehouses (#28583) --- .../warehouse_wise_item_balance_age_and_value.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index d3af5f61e3..4d1491b1b5 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -46,8 +46,8 @@ def execute(filters=None): item_balance.setdefault((item, item_map[item]["item_group"]), []) total_stock_value = 0.00 for wh in warehouse_list: - row += [qty_dict.bal_qty] if wh.name in warehouse else [0.00] - total_stock_value += qty_dict.bal_val if wh.name in warehouse else 0.00 + row += [qty_dict.bal_qty] if wh.name == warehouse else [0.00] + total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) item_value.setdefault((item, item_map[item]["item_group"]),[]) From a8c75b68628d29cb354ae3cb62424d3a01923586 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 20:16:21 +0530 Subject: [PATCH 046/247] refactor: application of SLA and its metrics --- erpnext/hooks.py | 3 +- .../service_level_agreement.py | 339 +++++++----------- 2 files changed, 137 insertions(+), 205 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a277ee035..186dbb5847 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,8 +341,7 @@ scheduler_events = { "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", - "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", - "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" + "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" ], "hourly_long": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index a25608b275..864640204b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -10,7 +10,6 @@ from frappe.core.utils import get_parent_doc from frappe.model.document import Document from frappe.utils import ( add_to_date, - cint, get_datetime, get_datetime_str, get_link_to_form, @@ -350,86 +349,113 @@ def set_documents_with_active_service_level_agreement(): def apply(doc, method=None): # Applies SLA to document on validate - if frappe.flags.in_patch or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - doc.doctype not in get_documents_with_active_service_level_agreement(): + if ( + frappe.flags.in_patch + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_setup_wizard + or doc.doctype not in get_documents_with_active_service_level_agreement() + ): return service_level_agreement = get_active_service_level_agreement_for(doc) - print(service_level_agreement) - if not service_level_agreement: return - set_sla_properties(doc, service_level_agreement) + process_sla(doc, service_level_agreement) -def set_sla_properties(doc, service_level_agreement): - if frappe.db.exists(doc.doctype, doc.name): - from_db = frappe.get_doc(doc.doctype, doc.name) - else: - from_db = frappe._dict({}) - - meta = frappe.get_meta(doc.doctype) - - if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ - not service_level_agreement.customer == doc.get("customer"): - frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name, - service_level_agreement.customer)) - - doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.get("priority") or service_level_agreement.default_priority - priority = get_priority(doc) +def process_sla(doc, service_level_agreement): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) - - if meta.has_field("service_level_agreement_creation"): + if doc.meta.has_field("service_level_agreement_creation"): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) + doc.service_level_agreement = service_level_agreement.name + doc.priority = doc.get("priority") or service_level_agreement.default_priority + + prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') + handle_status_change(doc, prev_status, service_level_agreement.apply_sla_for_resolution) + update_response_and_resolution_metrics(doc, service_level_agreement.apply_sla_for_resolution) + update_agreement_status(doc, service_level_agreement.apply_sla_for_resolution) + + +def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): + priority = get_response_and_resolution_duration(doc) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - - set_response_by_and_variance(doc, meta, start_date_time, priority) - if service_level_agreement.apply_sla_for_resolution: - set_resolution_by_and_variance(doc, meta, start_date_time, priority) - - update_status(doc, from_db, meta) + set_response_by_and_variance(doc, start_date_time, priority) + if apply_sla_for_resolution: + set_resolution_by_and_variance(doc, start_date_time, priority) -def update_status(doc, from_db, meta): - if meta.has_field("status"): - if meta.has_field("first_responded_on") and doc.status != "Open" and \ - from_db.status == "Open" and not doc.first_responded_on: +def get_fulfillment_statuses(service_level_agreement): + return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ + "parent": service_level_agreement + }, fields=["status"])] + + +def get_hold_statuses(service_level_agreement): + return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ + "parent": service_level_agreement + }, fields=["status"])] + + +def handle_status_change(doc, prev_status, apply_sla_for_resolution): + + if doc.status != "Open" and prev_status == "Open": + # status changed from Open to something else + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + # status changed to something other than Open doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("service_level_agreement") and doc.service_level_agreement: - # mark sla status as fulfilled based on the configuration - fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ - "parent": doc.service_level_agreement - }, fields=["status"])] + if doc.status == "Open" and prev_status != "Open": + # status changed from something else to Open + reset_resolution_metrics(doc) - if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses: - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, - "apply_sla_for_resolution") + handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution) + handle_hold_status(doc, prev_status) - if apply_sla_for_resolution and meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": - set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, meta) +def handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution): + fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) + if ( + doc.status in fulfillment_statuses + and prev_status not in fulfillment_statuses + and apply_sla_for_resolution + ): + # status changed to any fulfillment_statuses + if doc.meta.has_field("resolution_date"): + doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.meta.has_field("resolution_time"): + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) + set_user_resolution_time(doc) - if apply_sla_for_resolution: - set_resolution_time(doc, meta) - set_user_resolution_time(doc, meta) - if doc.status == "Open" and from_db.status != "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - # enable SLA and variance on Reopen - reset_metrics(doc, meta) - set_service_level_agreement_variance(doc.doctype, doc.name) +def handle_hold_status(doc, prev_status): + hold_statuses = get_hold_statuses(doc.service_level_agreement) + if doc.status in hold_statuses: + # reset if status is a hold status, regardless of previous status + reset_expected_response_and_resolution(doc) + if prev_status not in hold_statuses: + # set on_hold_since status changed from any non-hold status + # for eg. doc.status changed from Open to Replied + if doc.meta.has_field("on_hold_since"): + doc.on_hold_since = frappe.flags.current_time or now_datetime(doc.get("owner")) - handle_hold_time(doc, meta, from_db.status) + if doc.status not in hold_statuses and prev_status in hold_statuses: + # status changed to any non-hold status + # for eg. doc.status changed from Replied to Closed + if doc.meta.has_field("on_hold_since") and doc.on_hold_since: + cumulate_hold_time(doc) + doc.on_hold_since = None + + +def cumulate_hold_time(doc): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + on_hold_duration = time_diff_in_seconds(now_time, doc.on_hold_since) + doc.total_hold_time = (doc.total_hold_time or 0) + on_hold_duration def get_expected_time_for(parameter, service_level, start_date_time): @@ -500,41 +526,9 @@ def get_support_days(service_level): return support_days -def set_service_level_agreement_variance(doctype, doc=None): - - filters = {"status": "Open", "agreement_status": "Ongoing"} - - if doc: - filters = {"name": doc} - - for entry in frappe.get_all(doctype, filters=filters): - current_doc = frappe.get_doc(doctype, entry.name) - current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner")) - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement, - "apply_sla_for_resolution") - - if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) - else: - variance = round(time_diff_in_seconds(current_doc.response_by, current_doc.first_responded_on), 2) - - frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - - if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed - variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) - elif apply_sla_for_resolution and current_doc.get("resolution_date"): - variance = round(time_diff_in_seconds(current_doc.resolution_by, current_doc.get("resolution_date")), 2) - - frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - - -def set_user_resolution_time(doc, meta): +def set_user_resolution_time(doc): # total time taken by a user to close the issue apart from wait_time - if not meta.has_field("user_resolution_time"): + if not doc.meta.has_field("user_resolution_time"): return communications = frappe.get_all("Communication", filters={ @@ -567,7 +561,7 @@ def change_service_level_agreement_and_priority(self): frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) -def get_priority(doc): +def get_response_and_resolution_duration(doc): service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) priority.update({ @@ -596,115 +590,81 @@ def reset_service_level_agreement(doc, reason, user): doc.save() -def reset_metrics(doc, meta): - if meta.has_field("resolution_date"): +def reset_resolution_metrics(doc): + if doc.meta.has_field("resolution_date"): doc.resolution_date = None - if not meta.has_field("resolution_time"): + if doc.meta.has_field("resolution_time"): doc.resolution_time = None - if not meta.has_field("user_resolution_time"): + if doc.meta.has_field("user_resolution_time"): doc.user_resolution_time = None - if meta.has_field("agreement_status"): + if doc.meta.has_field("agreement_status"): doc.agreement_status = "Ongoing" -def set_resolution_time(doc, meta): - # total time taken from issue creation to closing - if not meta.has_field("resolution_time"): - return - - doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) - - # called via hooks on communication update def update_hold_time(doc, status): + if doc.communication_type == "Comment" or doc.sent_or_received != "Received": + return + parent = get_parent_doc(doc) if not parent: return - if doc.communication_type == "Comment": + if not parent.meta.has_field('service_level_agreement'): return - status_field = parent.meta.get_field("status") - if status_field: - options = (status_field.options or "").splitlines() + apply_sla_for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - # if status has a "Replied" option, then handle hold time - if ("Replied" in options) and doc.sent_or_received == "Received": - meta = frappe.get_meta(parent.doctype) - handle_hold_time(parent, meta, 'Replied') + handle_status_change(parent, 'Replied', apply_sla_for_resolution) + update_response_and_resolution_metrics(parent, apply_sla_for_resolution) + update_agreement_status(parent, apply_sla_for_resolution) + + parent.save() -def handle_hold_time(doc, meta, status): - if meta.has_field("service_level_agreement") and doc.service_level_agreement: - # set response and resolution variance as None as the issue is on Hold for status as Replied - hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ - "parent": doc.service_level_agreement - }, fields=["status"])] +def reset_expected_response_and_resolution(doc): + update_values = {} - if not hold_statuses: - return - - if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: - apply_hold_status(doc, meta) - - # calculate hold time when status is changed from any hold status to any non-hold status - if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: - reset_hold_status_and_update_hold_time(doc, meta) - - -def apply_hold_status(doc, meta): - update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))} - - if meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 + if doc.meta.has_field("resolution_by") and not doc.resolution_date: + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 doc.db_set(update_values) -def reset_hold_status_and_update_hold_time(doc, meta): - hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - last_hold_time = 0 - update_values = {} +def set_response_by_and_variance(doc, start_date_time, priority): + if doc.meta.has_field("response_by"): + doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + if doc.meta.has_field("total_hold_time") and doc.total_hold_time: + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.total_hold_time)) - if meta.has_field("on_hold_since") and doc.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time + if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.meta.has_field("response_by_variance") and doc.get('first_responded_on'): + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - # add hold time to response by variance - if meta.has_field("first_responded_on") and not doc.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time +def set_resolution_by_and_variance(doc, start_date_time, priority): + if doc.meta.has_field("resolution_by"): + doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + if doc.meta.has_field("total_hold_time") and doc.total_hold_time: + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.total_hold_time)) - # add hold time to resolution by variance - if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"): - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) + if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - - update_values['on_hold_since'] = None - - doc.db_set(update_values) + if doc.meta.has_field("resolution_by_variance") and doc.get('resolution_date'): + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.get('resolution_date')), 2) def get_service_level_agreement_fields(): @@ -808,45 +768,37 @@ def update_agreement_status_on_custom_status(doc): meta = frappe.get_meta(doc.doctype) now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - if meta.has_field("first_responded_on") and doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and doc.first_responded_on: # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) - if meta.has_field("resolution_date") and not doc.resolution_date: + if doc.meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - if meta.has_field("resolution_date") and doc.resolution_date: + if doc.meta.has_field("resolution_date") and doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) - if meta.has_field("agreement_status"): + if doc.meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" -def update_agreement_status(doc, meta): - if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \ - doc.service_level_agreement and doc.agreement_status == "Ongoing": - - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, - "apply_sla_for_resolution") - +def update_agreement_status(doc, apply_sla_for_resolution): + if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): - if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0: - + if doc.meta.has_field("response_by_variance") and doc.meta.has_field("resolution_by_variance"): + if doc.response_by_variance < 0 or doc.resolution_by_variance < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" else: - if meta.has_field("response_by_variance") and \ - cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0: + if doc.meta.has_field("response_by_variance") and doc.response_by_variance < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" @@ -862,25 +814,6 @@ def get_time_in_timedelta(time): return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) -def set_response_by_and_variance(doc, meta, start_date_time, priority): - if meta.has_field("response_by"): - doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - - if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - elif meta.has_field("response_by_variance") and doc.get('first_responded_on'): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - -def set_resolution_by_and_variance(doc, meta, start_date_time, priority): - if meta.has_field("resolution_by"): - doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - - if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - def now_datetime(user): dt = convert_utc_to_user_timezone(datetime.utcnow(), user) return dt.replace(tzinfo=None) From 214d0e367f5290dd82bc2f1dc7cdbd8e02951283 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 20:20:38 +0530 Subject: [PATCH 047/247] fix: remove leading whitespace --- .../doctype/service_level_agreement/service_level_agreement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 7e260dbbf5..bfbffe22ad 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Service Level Agreement', { entity: function(frm) { const field = frm.get_field('entity'); if (frm.doc.entity) { - const and_descendants = frm.doc.entity_type != 'Customer' ? __(' or its descendants') : ''; + const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : ''; field.set_description( __('SLA will be applied if {1} is set as {2}{3}', [ frm.doc.document_type, frm.doc.entity_type, From 267cc3585013c5a9454df0c25b65d90bf8fef171 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 27 Nov 2021 16:47:45 +0530 Subject: [PATCH 048/247] fix: failing tests --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 864640204b..565f05083b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -642,8 +642,8 @@ def reset_expected_response_and_resolution(doc): def set_response_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.total_hold_time: - doc.response_by = add_to_date(doc.response_by, seconds=round(doc.total_hold_time)) + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) @@ -656,8 +656,8 @@ def set_response_by_and_variance(doc, start_date_time, priority): def set_resolution_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.total_hold_time: - doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.total_hold_time)) + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) From 79f8159ab95e357aa7b094901a9da36a44281c1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 27 Nov 2021 17:14:58 +0530 Subject: [PATCH 049/247] test: issue closing after being on hold --- erpnext/support/doctype/issue/test_issue.py | 26 +++++++++++++++++++ .../service_level_agreement.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index ab9a444bc3..0559b15649 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -142,6 +142,32 @@ class TestIssue(TestSetUp): issue.reload() self.assertEqual(flt(issue.total_hold_time, 2), 2700) + def test_issue_close_after_on_hold(self): + creation = get_datetime("2021-11-01 19:00") + + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + # send a reply within SLA + creation = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = creation + issue.reload() + issue.status = 'Replied' + issue.save() + + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + + # close the issue after being on hold for 20 days + frappe.flags.current_time = get_datetime("2021-11-22 01:00") + issue.status = 'Closed' + issue.save() + + self.assertEqual(issue.resolution_by, get_datetime('2021-11-22 06:00:00')) + self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00')) + self.assertEqual(issue.agreement_status, 'Fulfilled') + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm # all dates are in the mm-dd-yyyy format diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 565f05083b..9c1e536078 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -642,7 +642,7 @@ def reset_expected_response_and_resolution(doc): def set_response_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): From 69a17b9e51cdf4c3214b8d70a2fed3e4e13a81b4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 27 Nov 2021 20:07:02 +0530 Subject: [PATCH 050/247] refactor: move Bin queries to qb/orm (#28522) --- erpnext/stock/doctype/bin/bin.py | 135 ++++++++++++++++++------------- 1 file changed, 81 insertions(+), 54 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 48b1cc5396..da9c66d996 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -4,6 +4,8 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import Case +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import flt, nowdate @@ -19,34 +21,42 @@ class Bin(Document): - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) def get_first_sle(self): - sle = frappe.db.sql(""" - select * from `tabStock Ledger Entry` - where item_code = %s - and warehouse = %s - order by timestamp(posting_date, posting_time) asc, creation asc - limit 1 - """, (self.item_code, self.warehouse), as_dict=1) - return sle and sle[0] or None + sle = frappe.qb.DocType("Stock Ledger Entry") + first_sle = ( + frappe.qb.from_(sle) + .select("*") + .where((sle.item_code == self.item_code) & (sle.warehouse == self.warehouse)) + .orderby(sle.posting_date, sle.posting_time, sle.creation) + .limit(1) + ).run(as_dict=True) + + return first_sle and first_sle[0] or None def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' - self.reserved_qty_for_production = frappe.db.sql(''' - SELECT - CASE WHEN ifnull(skip_transfer, 0) = 0 THEN - SUM(item.required_qty - item.transferred_qty) - ELSE - SUM(item.required_qty - item.consumed_qty) - END - FROM `tabWork Order` pro, `tabWork Order Item` item - WHERE - item.item_code = %s - and item.parent = pro.name - and pro.docstatus = 1 - and item.source_warehouse = %s - and pro.status not in ("Stopped", "Completed") - and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) - ''', (self.item_code, self.warehouse))[0][0] + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + self.reserved_qty_for_production = ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Case() + .when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty)) + .else_(Sum(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == self.item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == self.warehouse) + & (wo.status.notin(["Stopped", "Completed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 self.set_projected_qty() @@ -55,36 +65,53 @@ class Bin(Document): def update_reserved_qty_for_sub_contracting(self): #reserved qty - reserved_qty_for_sub_contract = frappe.db.sql(''' - select ifnull(sum(itemsup.required_qty),0) - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup - where - itemsup.rm_item_code = %s - and itemsup.parent = po.name - and po.docstatus = 1 - and po.is_subcontracted = 'Yes' - and po.status != 'Closed' - and po.per_received < 100 - and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0] - #Get Transferred Entries - materials_transferred = frappe.db.sql(""" - select - ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) - from - `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po - where - se.docstatus=1 - and se.purpose='Send to Subcontractor' - and ifnull(se.purchase_order, '') !='' - and (sed.item_code = %(item)s or sed.original_item = %(item)s) - and se.name = sed.parent - and se.purchase_order = po.name - and po.docstatus = 1 - and po.is_subcontracted = 'Yes' - and po.status != 'Closed' - and po.per_received < 100 - """, {'item': self.item_code})[0][0] + po = frappe.qb.DocType("Purchase Order") + supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") + + reserved_qty_for_sub_contract = ( + frappe.qb + .from_(po) + .from_(supplied_item) + .select(Sum(Coalesce(supplied_item.required_qty, 0))) + .where( + (supplied_item.rm_item_code == self.item_code) + & (po.name == supplied_item.parent) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + & (supplied_item.reserve_warehouse == self.warehouse) + ) + ).run()[0][0] or 0.0 + + se = frappe.qb.DocType("Stock Entry") + se_item = frappe.qb.DocType("Stock Entry Detail") + + materials_transferred = ( + frappe.qb + .from_(se) + .from_(se_item) + .from_(po) + .select(Sum( + Case() + .when(se.is_return == 1, se_item.transfer_qty * -1) + .else_(se_item.transfer_qty) + )) + .where( + (se.docstatus == 1) + & (se.purpose == "Send to Subcontractor") + & (Coalesce(se.purchase_order, "") != "") + & ((se_item.item_code == self.item_code) + | (se_item.original_item == self.item_code)) + & (se.name == se_item.parent) + & (po.name == se.purchase_order) + & (po.docstatus == 1) + & (po.is_subcontracted == "Yes") + & (po.status != "Closed") + & (po.per_received < 100) + ) + ).run()[0][0] or 0.0 if reserved_qty_for_sub_contract > materials_transferred: reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred @@ -160,4 +187,4 @@ def update_qty(bin_name, args): 'indented_qty': indented_qty, 'planned_qty': planned_qty, 'projected_qty': projected_qty - }) \ No newline at end of file + }) From cdaf0a04cf92d27a5fae7a298ee8397b0b321491 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Sat, 27 Nov 2021 12:03:35 -0300 Subject: [PATCH 051/247] fix(ux): allow translations (#28455) * Translation * Translations Co-authored-by: xdlumertz --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 6 +++--- erpnext/controllers/stock_controller.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7aeb872b28..c1b056b9c7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -339,7 +339,7 @@ class PaymentEntry(AccountsController): for k, v in no_oustanding_refs.items(): frappe.msgprint( _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") - .format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount")) + .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount"))) + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -611,7 +611,7 @@ class PaymentEntry(AccountsController): if not total_negative_outstanding: frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice") - .format(self.payment_type, ("to" if self.party_type=="Customer" else "from"), + .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")), self.party_type), InvalidPaymentEntry) elif paid_amount - additional_charges > total_negative_outstanding: @@ -1092,7 +1092,7 @@ def get_outstanding_reference_documents(args): if not data: frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.") - .format(args.get("party_type").lower(), frappe.bold(args.get("party")))) + .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))) return data diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 516133ab63..48b5cb9459 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -728,7 +728,7 @@ class PurchaseInvoice(BuyingController): "account": self.stock_received_but_not_billed, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), - "remarks": self.remarks or "Accounting Entry for Stock", + "remarks": self.remarks or _("Accounting Entry for Stock"), "cost_center": self.cost_center, "project": item.project or self.project }, item=item) @@ -936,7 +936,7 @@ class PurchaseInvoice(BuyingController): "cost_center": tax.cost_center, "against": self.supplier, "credit": valuation_tax[tax.name], - "remarks": self.remarks or "Accounting Entry for Stock" + "remarks": self.remarks or _("Accounting Entry for Stock") }, item=tax)) @property diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a1f3ee4b06..4538675d07 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -978,7 +978,7 @@ frappe.ui.form.on('Sales Invoice', { } if (frm.doc.is_debit_note) { - frm.set_df_property('return_against', 'label', 'Adjustment Against'); + frm.set_df_property('return_against', 'label', __('Adjustment Against')); } if (frappe.boot.active_domains.includes("Healthcare")) { @@ -988,10 +988,10 @@ frappe.ui.form.on('Sales Invoice', { if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) { frm.add_custom_button(__('Healthcare Services'), function() { get_healthcare_services_to_invoice(frm); - },"Get Items From"); + },__("Get Items From")); frm.add_custom_button(__('Prescriptions'), function() { get_drugs_to_invoice(frm); - },"Get Items From"); + },__("Get Items From")); } } else { diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ae170945aa..7073e32f53 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -134,7 +134,7 @@ class StockController(AccountsController): "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), - "remarks": self.get("remarks") or "Accounting Entry for Stock", + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": flt(sle.stock_value_difference, precision), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) @@ -143,7 +143,7 @@ class StockController(AccountsController): "account": expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, - "remarks": self.get("remarks") or "Accounting Entry for Stock", + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": flt(sle.stock_value_difference, precision), "project": item_row.get("project") or self.get("project"), "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No" From 43038aab7952a9e613ed92b14f5b14212d6667ca Mon Sep 17 00:00:00 2001 From: hrwx Date: Sat, 27 Nov 2021 21:46:13 +0000 Subject: [PATCH 052/247] fix: do not add gst fields if no indian company --- erpnext/patches.txt | 2 +- .../v13_0/create_gst_payment_entry_fields.py | 45 ++++++++++--------- erpnext/www/shop-by-category/__init__.py | 0 3 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e475229125..897e70ce25 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -287,7 +287,7 @@ erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost erpnext.patches.v13_0.validate_options_for_data_field -erpnext.patches.v13_0.create_gst_payment_entry_fields +erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v13_0.fix_invoice_statuses erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py index 7e6d67ce93..416694559c 100644 --- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py +++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py @@ -9,24 +9,29 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') frappe.reload_doc('accounts', 'doctype', 'payment_entry') - custom_fields = { - 'Payment Entry': [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) - ] - } + if frappe.db.exists('Company', {'country': 'India'}): + custom_fields = { + 'Payment Entry': [ + dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', + print_hide=1, collapsible=1), + dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', + print_hide=1, options='Address'), + dict(fieldname='company_gstin', label='Company GSTIN', + fieldtype='Data', insert_after='company_address', + fetch_from='company_address.gstin', print_hide=1, read_only=1), + dict(fieldname='place_of_supply', label='Place of Supply', + fieldtype='Data', insert_after='company_gstin', + print_hide=1, read_only=1), + dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', + print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), + dict(fieldname='customer_gstin', label='Customer GSTIN', + fieldtype='Data', insert_after='customer_address', + fetch_from='customer_address.gstin', print_hide=1, read_only=1) + ] + } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) + else: + fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin'] + for field in fields: + frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") \ No newline at end of file diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From de6f104b740d4854663a6246a4a3b2a31b1034be Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sat, 27 Nov 2021 23:16:34 +0000 Subject: [PATCH 053/247] Delete __init__.py --- erpnext/www/shop-by-category/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From f8623390241affb06f278ef5700aa0294b5cc726 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 Nov 2021 19:07:39 +0530 Subject: [PATCH 054/247] fix: use get_all instead of get_list for child tables --- .../bank_reconciliation_tool/bank_reconciliation_tool.py | 2 +- .../report/bom_stock_calculated/bom_stock_calculated.py | 2 +- erpnext/regional/report/eway_bill/eway_bill.py | 4 ++-- erpnext/regional/report/vat_audit_report/vat_audit_report.py | 2 +- erpnext/stock/doctype/item/test_item.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 5cbf00b2c6..e7371fbe43 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -434,7 +434,7 @@ def get_pi_matching_query(amount_condition): def get_ec_matching_query(bank_account, company, amount_condition): # get matching Expense Claim query - mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", + mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"])] mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' company_currency = get_company_currency(company) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index cf19cbf6a2..090a3e74fc 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -89,7 +89,7 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) def get_manufacturer_records(): - details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) + details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) manufacture_details = frappe._dict() for detail in details: dic = manufacture_details.setdefault(detail.get('parent'), {}) diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index 91a47674d7..f3fe5e8848 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -106,14 +106,14 @@ def set_address_details(row, special_characters): row.update({'ship_to_state': row.to_state}) def set_taxes(row, filters): - taxes = frappe.get_list("Sales Taxes and Charges", + taxes = frappe.get_all("Sales Taxes and Charges", filters={ 'parent': row.dn_id }, fields=('item_wise_tax_detail', 'account_head')) account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"] - taxes_list = frappe.get_list("GST Account", + taxes_list = frappe.get_all("GST Account", filters={ "parent": "GST Settings", "company": filters.company diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 5a281a4cbb..17e50648b3 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -41,7 +41,7 @@ class VATAuditReport(object): return self.columns, self.data def get_sa_vat_accounts(self): - self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", + self.sa_vat_accounts = frappe.get_all("South Africa VAT Account", filters = {"parent": self.filters.company}, pluck="account") if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 7237178b15..8b1224bd3e 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -488,7 +488,7 @@ class TestItem(ERPNextTestCase): item_doc.save() # Check values saved correctly - barcodes = frappe.get_list( + barcodes = frappe.get_all( 'Item Barcode', fields=['barcode', 'barcode_type'], filters={'parent': item_code}) From c7701ace80e5ce62e6a41db170b4e1f461ce970f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 29 Nov 2021 12:36:47 +0530 Subject: [PATCH 055/247] chore: correct docstrings --- erpnext/stock/doctype/item/item.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 5daabe817b..c9b8a3734e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -222,10 +222,11 @@ class Item(WebsiteGenerator): 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5)) def validate_website_image(self): + """Validate if the website image is a public file""" + if frappe.flags.in_import: return - """Validate if the website image is a public file""" auto_set_website_image = False if not self.website_image and self.image: auto_set_website_image = True @@ -255,10 +256,11 @@ class Item(WebsiteGenerator): self.website_image = None def make_thumbnail(self): + """Make a thumbnail of `website_image`""" + if frappe.flags.in_import: return - """Make a thumbnail of `website_image`""" import requests.exceptions if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): From d1746caa02c84d22793b9a21c49582f372eca1a9 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 24 Nov 2021 02:54:43 +0100 Subject: [PATCH 056/247] refactor(KSA VAT): QR Code as per ZATKA specification --- erpnext/regional/saudi_arabia/utils.py | 85 ++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index a2f634ee22..558c7ee1d7 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,7 +1,10 @@ import io import os +from base64 import b64encode import frappe +from frappe import _ +from frappe.utils.data import add_to_date, get_time, getdate from pyqrcode import create as qr_create from erpnext import get_region @@ -28,24 +31,80 @@ def create_qr_code(doc, method): for field in meta.get_image_fields(): if field.fieldname == 'qr_code': - from urllib.parse import urlencode + ''' TLV conversion for + 1. Seller's Name + 2. VAT Number + 3. Time Stamp + 4. Invoice Amount + 5. VAT Amount + ''' + tlv_array = [] + # Sellers Name - # Creating public url to print format - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value") + '''TODO: Fix arabic conversion''' + # seller_name = frappe.db.get_value( + # 'Company', + # doc.company, + # 'company_name_in_arabic') - # System Language - language = frappe.get_system_settings('language') + # if not seller_name: + # frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) - params = urlencode({ - 'format': default_print_format or 'Standard', - '_lang': language, - 'key': doc.get_signature() - }) + seller_name = doc.company + tag = bytes([1]).hex() + length = bytes([len(seller_name)]).hex() + value = seller_name.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # VAT Number + tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + if not tax_id: + frappe.throw(_('Tax ID missing for {} in the company document'.format(doc.company))) + + # tax_id = '310122393500003' # + tag = bytes([2]).hex() + length = bytes([len(tax_id)]).hex() + value = tax_id.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Time Stamp + posting_date = getdate(doc.posting_date) + time = get_time(doc.posting_time) + seconds = time.hour * 60 * 60 + time.minute * 60 + time.second + time_stamp = add_to_date(posting_date, seconds=seconds) + time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + + # time_stamp = '2022-04-25T15:30:00Z' # + tag = bytes([3]).hex() + length = bytes([len(time_stamp)]).hex() + value = time_stamp.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Invoice Amount + invoice_amount = str(doc.total) + # invoice_amount = '1000.00' # + tag = bytes([4]).hex() + length = bytes([len(invoice_amount)]).hex() + value = invoice_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # VAT Amount + vat_amount = str(doc.total_taxes_and_charges) + + # vat_amount = '150.00' # + tag = bytes([5]).hex() + length = bytes([len(vat_amount)]).hex() + value = vat_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) + + # Joining bytes into one + tlv_buff = ''.join(tlv_array) + + # base64 conversion for QR Code + base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - # creating qr code for the url - url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?{ params }" qr_image = io.BytesIO() - url = qr_create(url, error='L') + url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) # making file From de784d8bfe31008adf735ec065627ea4aea2cbd8 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 24 Nov 2021 03:03:17 +0100 Subject: [PATCH 057/247] refactor: comments removed --- erpnext/regional/saudi_arabia/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 558c7ee1d7..c0f4e31b23 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -61,7 +61,6 @@ def create_qr_code(doc, method): if not tax_id: frappe.throw(_('Tax ID missing for {} in the company document'.format(doc.company))) - # tax_id = '310122393500003' # tag = bytes([2]).hex() length = bytes([len(tax_id)]).hex() value = tax_id.encode('utf-8').hex() @@ -74,7 +73,6 @@ def create_qr_code(doc, method): time_stamp = add_to_date(posting_date, seconds=seconds) time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') - # time_stamp = '2022-04-25T15:30:00Z' # tag = bytes([3]).hex() length = bytes([len(time_stamp)]).hex() value = time_stamp.encode('utf-8').hex() @@ -82,7 +80,6 @@ def create_qr_code(doc, method): # Invoice Amount invoice_amount = str(doc.total) - # invoice_amount = '1000.00' # tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() value = invoice_amount.encode('utf-8').hex() @@ -91,7 +88,6 @@ def create_qr_code(doc, method): # VAT Amount vat_amount = str(doc.total_taxes_and_charges) - # vat_amount = '150.00' # tag = bytes([5]).hex() length = bytes([len(vat_amount)]).hex() value = vat_amount.encode('utf-8').hex() From 31b9b84fdf9dfefc2c629fc208bbe5330df43556 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sat, 27 Nov 2021 14:28:13 +0100 Subject: [PATCH 058/247] fix: KSA VAT QR Code arabic conversion --- erpnext/regional/saudi_arabia/utils.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index c0f4e31b23..516b87c75d 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -41,18 +41,16 @@ def create_qr_code(doc, method): tlv_array = [] # Sellers Name - '''TODO: Fix arabic conversion''' - # seller_name = frappe.db.get_value( - # 'Company', - # doc.company, - # 'company_name_in_arabic') + seller_name = frappe.db.get_value( + 'Company', + doc.company, + 'company_name_in_arabic') - # if not seller_name: - # frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) + if not seller_name: + frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) - seller_name = doc.company tag = bytes([1]).hex() - length = bytes([len(seller_name)]).hex() + length = bytes([len(seller_name.encode('utf-8'))]).hex() value = seller_name.encode('utf-8').hex() tlv_array.append(''.join([tag, length, value])) From f3f7ed6f0d7d6e758da5eee77a22f48c6fd9bec3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 29 Nov 2021 13:43:56 +0530 Subject: [PATCH 059/247] fix: Translations --- erpnext/regional/saudi_arabia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 516b87c75d..1051315cbe 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -47,7 +47,7 @@ def create_qr_code(doc, method): 'company_name_in_arabic') if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document'.format(doc.company))) + frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) tag = bytes([1]).hex() length = bytes([len(seller_name.encode('utf-8'))]).hex() @@ -57,7 +57,7 @@ def create_qr_code(doc, method): # VAT Number tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document'.format(doc.company))) + frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) tag = bytes([2]).hex() length = bytes([len(tax_id)]).hex() From baf41fdc9c3b6280a766254c30673c534bf72878 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 29 Nov 2021 14:23:06 +0530 Subject: [PATCH 060/247] fix: Employee Advance paid amount not updated on PE cancellation (#28572) * fix: employee advance paid amount not updated on PE cancellation * fix: convert raw sql queries to qb * test: Employee Advance Paid Amount on PE cancellation * chore: disable no copy for sanctioned amount in Expense Claim --- .../employee_advance/employee_advance.py | 45 ++++++++++++------- .../employee_advance/test_employee_advance.py | 18 ++++++++ .../expense_claim_detail.json | 3 +- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 8a8e8dba74..7aac2b63ed 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import flt, nowdate import erpnext @@ -41,24 +42,34 @@ class EmployeeAdvance(Document): self.status = "Cancelled" def set_total_advance_paid(self): - paid_amount = frappe.db.sql(""" - select ifnull(sum(debit), 0) as paid_amount - from `tabGL Entry` - where against_voucher_type = 'Employee Advance' - and against_voucher = %s - and party_type = 'Employee' - and party = %s - """, (self.name, self.employee), as_dict=1)[0].paid_amount + gle = frappe.qb.DocType("GL Entry") - return_amount = frappe.db.sql(""" - select ifnull(sum(credit), 0) as return_amount - from `tabGL Entry` - where against_voucher_type = 'Employee Advance' - and voucher_type != 'Expense Claim' - and against_voucher = %s - and party_type = 'Employee' - and party = %s - """, (self.name, self.employee), as_dict=1)[0].return_amount + paid_amount = ( + frappe.qb.from_(gle) + .select(Sum(gle.debit).as_("paid_amount")) + .where( + (gle.against_voucher_type == 'Employee Advance') + & (gle.against_voucher == self.name) + & (gle.party_type == 'Employee') + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].paid_amount or 0 + + return_amount = ( + frappe.qb.from_(gle) + .select(Sum(gle.credit).as_("return_amount")) + .where( + (gle.against_voucher_type == 'Employee Advance') + & (gle.voucher_type != 'Expense Claim') + & (gle.against_voucher == self.name) + & (gle.party_type == 'Employee') + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].return_amount or 0 if paid_amount != 0: paid_amount = flt(paid_amount) / flt(self.exchange_rate) diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 4ecfa60eb7..5f2e720eb4 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -34,6 +34,24 @@ class TestEmployeeAdvance(unittest.TestCase): journal_entry1 = make_payment_entry(advance) self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit) + def test_paid_amount_on_pe_cancellation(self): + employee_name = make_employee("_T@employe.advance") + advance = make_employee_advance(employee_name) + + pe = make_payment_entry(advance) + pe.submit() + + advance.reload() + + self.assertEqual(advance.paid_amount, 1000) + self.assertEqual(advance.status, "Paid") + + pe.cancel() + advance.reload() + + self.assertEqual(advance.paid_amount, 0) + self.assertEqual(advance.status, "Unpaid") + def test_repay_unclaimed_amount_from_salary(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index 70a48f93b7..6edbcb5c39 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -94,7 +94,6 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Sanctioned Amount", - "no_copy": 1, "oldfieldname": "sanctioned_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -120,7 +119,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-11-26 14:23:45.539922", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", From c0cc72ec1d839db4bb510efb22ed17e2e4033e8a Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 29 Nov 2021 15:05:06 +0530 Subject: [PATCH 061/247] fix: incorrect discount amount set when item is replaced (#28556) --- erpnext/stock/get_item_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e00382bec1..cd180a42ca 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -299,7 +299,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "warehouse": warehouse, "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , - "discount_account": None or get_default_discount_account(args, item_defaults), + "discount_account": get_default_discount_account(args, item_defaults), "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), 'has_serial_no': item.has_serial_no, 'has_batch_no': item.has_batch_no, @@ -317,6 +317,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "net_rate": 0.0, "net_amount": 0.0, "discount_percentage": 0.0, + "discount_amount": 0.0, "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), "update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0, "delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, From af6fc2977098d51c990ee8a63fc6315122166be8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 29 Nov 2021 16:18:35 +0530 Subject: [PATCH 062/247] fix: KSA print format for invoices not having item codes --- .../print_format/ksa_vat_invoice/ksa_vat_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 681f72fd30..8e9a72897d 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -10,14 +10,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set tax_amount = frappe.utils.flt(data_object[item.item_code][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[item.item_code][0])-%}\n {{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-22 10:40:24.716932", + "modified": "2021-11-29 13:47:37.870818", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", From 570ae422a84088a9d37fc1ef79264d0793a25505 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Nov 2021 16:25:30 +0530 Subject: [PATCH 063/247] Update work_order_consumed_materials.py --- .../work_order_consumed_materials.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index 8d2505ad95..a56c71064d 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -2,7 +2,6 @@ # For license information, please see license.txt import frappe -import json from frappe import _ def execute(filters=None): @@ -128,4 +127,4 @@ def get_columns(): "fieldtype": "Float", "width": 100 } - ] \ No newline at end of file + ] From 7fac9b8e9ccec5d310c701eb1b25a81f7c662745 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 29 Nov 2021 16:38:34 +0530 Subject: [PATCH 064/247] fix: changed fieldtype from int to float for the field Batch Size in the work order --- .../doctype/work_order_operation/work_order_operation.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 647c14b33d..4e1a464cb0 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -178,8 +178,9 @@ }, { "fieldname": "batch_size", - "fieldtype": "Int", - "label": "Batch Size" + "fieldtype": "Float", + "label": "Batch Size", + "read_only": 1 }, { "fieldname": "sequence_id", @@ -200,7 +201,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-24 04:52:54.295168", + "modified": "2021-11-29 16:37:18.824489", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", From 3d2efb76d39910b271325554a9d841e74ea0230f Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 29 Nov 2021 17:52:55 +0530 Subject: [PATCH 065/247] fix: linters issue --- .../work_order_consumed_materials.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index a56c71064d..052834807e 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -4,6 +4,7 @@ import frappe from frappe import _ + def execute(filters=None): columns, data = [], [] columns = get_columns() From 4382040fb64e333b33fefb2e97ad96ba3e5d52a3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 29 Nov 2021 18:25:47 +0530 Subject: [PATCH 066/247] fix: Move trigger from on trash to on cancel --- erpnext/hooks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4f00ef817a..6f9b08acb7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -256,12 +256,10 @@ doc_events = { ], "on_cancel": [ "erpnext.regional.italy.utils.sales_invoice_on_cancel", - "erpnext.erpnext_integrations.taxjar_integration.delete_transaction" - ], - "on_trash": [ - "erpnext.regional.check_deletion_permission", + "erpnext.erpnext_integrations.taxjar_integration.delete_transaction", "erpnext.regional.saudi_arabia.utils.delete_qr_code_file" ], + "on_trash": "erpnext.regional.check_deletion_permission", "validate": [ "erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.update_taxable_values" From 2f6a6afaa8f839722cc2dd2a7efe442448250b96 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 29 Nov 2021 20:35:49 +0530 Subject: [PATCH 067/247] feat(pos): Total item qty field to POS screen (#28331) --- erpnext/public/scss/point-of-sale.scss | 5 +++++ .../page/point_of_sale/pos_item_cart.js | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 1677e9b3de..7a3854cc61 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -495,6 +495,11 @@ font-size: var(--text-md); } + > .item-qty-total-container { + @extend .net-total-container; + padding: 5px 0px 0px 0px; + } + > .taxes-container { display: none; flex-direction: column; diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index a5b2d50041..4920584d95 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -100,6 +100,10 @@ erpnext.PointOfSale.ItemCart = class { `
${this.get_discount_icon()} ${__('Add Discount')}
+
+
${__('Total Items')}
+
0.00
+
${__("Net Total")}
0.00
@@ -142,6 +146,7 @@ erpnext.PointOfSale.ItemCart = class { this.$numpad_section.prepend( `
+
` @@ -470,6 +475,7 @@ erpnext.PointOfSale.ItemCart = class { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.net_total); + this.render_total_item_qty(frm.doc.items); const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); @@ -487,6 +493,21 @@ erpnext.PointOfSale.ItemCart = class { ); } + render_total_item_qty(items) { + var total_item_qty = 0; + items.map((item) => { + total_item_qty = total_item_qty + item.qty; + }); + + this.$totals_section.find('.item-qty-total-container').html( + `
${__('Total Quantity')}
${total_item_qty}
` + ); + + this.$numpad_section.find('.numpad-item-qty-total').html( + `
${__('Total Quantity')}: ${total_item_qty}
` + ); + } + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total-container').html( From 4458b2481351e6784492686e2e6ccf02ee5c8bc5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 30 Nov 2021 10:15:41 +0530 Subject: [PATCH 068/247] fix: allow creating Shift Assignment for same day (#28613) --- erpnext/hr/doctype/shift_assignment/shift_assignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 4e829a3dbd..517730281f 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -19,8 +19,8 @@ class ShiftAssignment(Document): validate_active_employee(self.employee) self.validate_overlapping_dates() - if self.end_date and self.end_date <= self.start_date: - frappe.throw(_("End Date must not be lesser than Start Date")) + if self.end_date: + self.validate_from_to_dates('start_date', 'end_date') def validate_overlapping_dates(self): if not self.name: From aeb62af544090576944b51d667fc60ec42b42ab4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 30 Nov 2021 11:59:43 +0530 Subject: [PATCH 069/247] fix(ux): remove attachment limits --- erpnext/selling/doctype/quotation/quotation.json | 4 +--- erpnext/stock/doctype/item/item.json | 3 +-- .../doctype/stock_reconciliation/stock_reconciliation.json | 6 ++++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index ad788e5c8b..ee5b0ea760 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -961,9 +961,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "max_attachments": 1, - "migration_hash": "75a86a19f062c2257bcbc8e6e31c7f1e", - "modified": "2021-10-21 12:58:55.514512", + "modified": "2021-11-30 01:33:21.106073", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4b314a00a4..cd97ec31a4 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1028,8 +1028,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "max_attachments": 1, - "modified": "2021-10-27 21:04:00.324786", + "modified": "2021-11-30 01:33:06.572442", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index b7d1497319..3402972bb8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-03-28 10:35:31", "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", @@ -153,11 +154,12 @@ "icon": "fa fa-upload-alt", "idx": 1, "is_submittable": 1, - "max_attachments": 1, - "modified": "2020-04-08 17:02:47.196206", + "links": [], + "modified": "2021-11-30 01:33:51.437194", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { From 9610086d0c084808455e38a4f950b8b9f68f90bc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 30 Nov 2021 13:24:23 +0530 Subject: [PATCH 070/247] feat: Show Zero Values filter in consolidated financial statement --- .../consolidated_financial_statement.js | 5 +++++ .../consolidated_financial_statement.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index e24a5f9918..d3e836afd1 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Include Default Book Entries"), "fieldtype": "Check", "default": 1 + }, + { + "fieldname": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" } ], "formatter": function(value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index c71bc17ca7..01799d5804 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import ( get_cash_flow_accounts, ) from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary -from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts +from erpnext.accounts.report.financial_statements import ( + filter_out_zero_value_rows, + get_fiscal_year_data, + sort_accounts, +) from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( get_chart_data as get_pl_chart_data, ) @@ -265,7 +269,7 @@ def get_columns(companies, filters): return columns def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): - accounts, accounts_by_name = get_account_heads(root_type, + accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) if not accounts: return [] @@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) + out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values")) + if out: add_total_row(out, root_type, balance_must_be, companies, company_currency) @@ -370,7 +376,7 @@ def get_account_heads(root_type, companies, filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - return accounts, accounts_by_name + return accounts, accounts_by_name, parent_children_map def update_parent_account_names(accounts): """Update parent_account_name in accounts list. From d0f4f03b668ed08e6685d46e95ee9aca4eea5b13 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 30 Nov 2021 14:04:47 +0530 Subject: [PATCH 071/247] fix: Employee Transfer and Project Profitability test cases (#28633) * fix: Employee Transfer testcases * fix: Project Profitability test case --- .../test_employee_transfer.py | 72 +++++++++---------- .../test_project_profitability.py | 19 ++--- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py index 287dfba35b..64eee402fe 100644 --- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py @@ -2,7 +2,6 @@ # See license.txt import unittest -from datetime import date import frappe from frappe.utils import add_days, getdate @@ -12,16 +11,14 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeTransfer(unittest.TestCase): def setUp(self): - make_employee("employee2@transfers.com") - make_employee("employee3@transfers.com") create_company() - create_employee() - create_employee_transfer() def tearDown(self): frappe.db.rollback() def test_submit_before_transfer_date(self): + make_employee("employee2@transfers.com") + transfer_obj = frappe.get_doc({ "doctype": "Employee Transfer", "employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"), @@ -43,6 +40,8 @@ class TestEmployeeTransfer(unittest.TestCase): self.assertEqual(transfer.docstatus, 1) def test_new_employee_creation(self): + make_employee("employee3@transfers.com") + transfer = frappe.get_doc({ "doctype": "Employee Transfer", "employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"), @@ -63,60 +62,51 @@ class TestEmployeeTransfer(unittest.TestCase): self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left") def test_employee_history(self): - name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name") - doc = frappe.get_doc("Employee",name) + employee = make_employee("employee4@transfers.com", + company="Test Company", + date_of_birth=getdate("30-09-1980"), + date_of_joining=getdate("01-10-2021"), + department="Accounts - TC", + designation="Accountant" + ) + transfer = create_employee_transfer(employee) + count = 0 department = ["Accounts - TC", "Management - TC"] designation = ["Accountant", "Manager"] - dt = [getdate("01-10-2021"), date.today()] + dt = [getdate("01-10-2021"), getdate()] - for data in doc.internal_work_history: + employee = frappe.get_doc("Employee", employee) + for data in employee.internal_work_history: self.assertEqual(data.department, department[count]) self.assertEqual(data.designation, designation[count]) self.assertEqual(data.from_date, dt[count]) count = count + 1 - data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"]) - doc = frappe.get_doc("Employee Transfer", data[0]["name"]) - doc.cancel() - employee_doc = frappe.get_doc("Employee",name) + transfer.cancel() + employee.reload() - for data in employee_doc.internal_work_history: + for data in employee.internal_work_history: self.assertEqual(data.designation, designation[0]) self.assertEqual(data.department, department[0]) self.assertEqual(data.from_date, dt[0]) -def create_employee(): - doc = frappe.get_doc({ - "doctype": "Employee", - "first_name": "John", - "company": "Test Company", - "gender": "Male", - "date_of_birth": getdate("30-09-1980"), - "date_of_joining": getdate("01-10-2021"), - "department": "Accounts - TC", - "designation": "Accountant" - }) - - doc.save() def create_company(): - exists = frappe.db.exists("Company", "Test Company") - if not exists: - doc = frappe.get_doc({ - "doctype": "Company", - "company_name": "Test Company", - "default_currency": "INR", - "country": "India" - }) + if not frappe.db.exists("Company", "Test Company"): + frappe.get_doc({ + "doctype": "Company", + "company_name": "Test Company", + "default_currency": "INR", + "country": "India" + }).insert() - doc.save() -def create_employee_transfer(): +def create_employee_transfer(employee): doc = frappe.get_doc({ "doctype": "Employee Transfer", - "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"), - "transfer_date": date.today(), + "employee": employee, + "transfer_date": getdate(), "transfer_details": [ { "property": "Designation", @@ -134,4 +124,6 @@ def create_employee_transfer(): }) doc.save() - doc.submit() \ No newline at end of file + doc.submit() + + return doc \ No newline at end of file diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 2cf9d38bd0..04156902f7 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,7 +1,7 @@ import unittest import frappe -from frappe.utils import add_days, getdate, nowdate +from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.timesheet.test_timesheet import ( @@ -13,21 +13,26 @@ from erpnext.projects.report.project_profitability.project_profitability import class TestProjectProfitability(unittest.TestCase): - def setUp(self): + frappe.db.sql('delete from `tabTimesheet`') emp = make_employee('test_employee_9@salary.com', company='_Test Company') + if not frappe.db.exists('Salary Component', 'Timesheet Component'): frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert() + make_salary_structure_for_timesheet(emp, company='_Test Company') - self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) + date = getdate() + + self.timesheet = make_timesheet(emp, is_billable=1) self.salary_slip = make_salary_slip(self.timesheet.name) - holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate()) + + holidays = self.salary_slip.get_holidays_for_employee(date, date) if holidays: frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) self.salary_slip.submit() self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') - self.sales_invoice.due_date = nowdate() + self.sales_invoice.due_date = date self.sales_invoice.submit() frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) @@ -63,6 +68,4 @@ class TestProjectProfitability(unittest.TestCase): self.assertEqual(fractional_cost, row.fractional_cost) def tearDown(self): - frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel() - frappe.get_doc("Salary Slip", self.salary_slip.name).cancel() - frappe.get_doc("Timesheet", self.timesheet.name).cancel() + frappe.db.rollback() From e10ab1626c1264d9d5a25216068b5d064726d59e Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 30 Nov 2021 13:24:18 +0100 Subject: [PATCH 072/247] feat: Grant commission on certain items only (#27467) Co-authored-by: Sagar Vora --- .../doctype/pos_invoice/pos_invoice.json | 10 ++- .../pos_invoice_item/pos_invoice_item.json | 11 +++- .../doctype/sales_invoice/sales_invoice.json | 11 +++- .../sales_invoice/test_sales_invoice.py | 23 +++++++ .../sales_invoice_item.json | 11 +++- .../sales_partners_commission.json | 41 ++++++------ erpnext/controllers/accounts_controller.py | 7 ++- erpnext/controllers/selling_controller.py | 28 ++++++--- .../doctype/sales_order/sales_order.json | 10 ++- .../sales_order_item/sales_order_item.json | 11 +++- erpnext/selling/sales_common.js | 63 ++++++++++--------- .../doctype/delivery_note/delivery_note.json | 8 +++ .../delivery_note_item.json | 10 ++- erpnext/stock/doctype/item/item.json | 9 ++- erpnext/stock/get_item_details.py | 3 +- 15 files changed, 190 insertions(+), 66 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index bff8587278..0c6e7edeb0 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -171,6 +171,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -1561,16 +1562,23 @@ "label": "Coupon Code", "options": "Coupon Code", "print_hide": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-27 20:12:57.306772", + "modified": "2021-10-05 12:11:53.871828", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 8b71eb02fd..3f85668ede 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -46,6 +46,7 @@ "base_amount", "pricing_rules", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -800,14 +801,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-04 17:34:49.924531", + "modified": "2021-10-05 12:23:47.506290", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 93e32f1a18..545abf77e6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -182,6 +182,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -2019,6 +2020,12 @@ "label": "Total Billing Hours", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -2031,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-11 20:19:38.667508", + "modified": "2021-10-21 20:19:38.667508", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2086,4 +2093,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 37bea70f16..6a488ea96e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2385,6 +2385,29 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_sales_commission(self): + si = frappe.copy_doc(test_records[0]) + item = copy.deepcopy(si.get('items')[0]) + item.update({ + "qty": 1, + "rate": 500, + "grant_commission": 1 + }) + si.append("items", item) + + # Test valid values + for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): + si.commission_rate = commission_rate + si.save() + self.assertEqual(si.amount_eligible_for_commission, 500) + self.assertEqual(si.total_commission, total_commission) + + # Test invalid values + for commission_rate in (101, -1): + si.reload() + si.commission_rate = commission_rate + self.assertRaises(frappe.ValidationError, si.save) + def test_sales_invoice_submission_post_account_freezing_date(self): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index b90f3f0904..ae9ac35729 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -47,6 +47,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -828,15 +829,23 @@ "fieldtype": "Link", "label": "Discount Account", "options": "Account" + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-19 13:41:53.435827", + "modified": "2021-10-05 12:24:54.968907", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json index a740de3572..9dd4e437f7 100644 --- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json +++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json @@ -1,27 +1,30 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-05-06 12:28:23", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-03-06 05:52:57.645281", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Partners Commission", - "owner": "Administrator", - "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", - "ref_doctype": "Sales Invoice", - "report_name": "Sales Partners Commission", - "report_type": "Query Report", + "add_total_row": 0, + "columns": [], + "creation": "2013-05-06 12:28:23", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "modified": "2021-10-06 06:26:07.881340", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Partners Commission", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", + "ref_doctype": "Sales Invoice", + "report_name": "Sales Partners Commission", + "report_type": "Query Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } ] -} +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0ee884e30c..2c92820a74 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -250,7 +250,12 @@ class AccountsController(TransactionBase): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals calculate_taxes_and_totals(self) - if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: + if self.doctype in ( + 'Sales Order', + 'Delivery Note', + 'Sales Invoice', + 'POS Invoice', + ): self.calculate_commission() self.calculate_contribution() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index dad3ed7093..cc773b7596 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -120,13 +120,27 @@ class SellingController(StockController): self.in_words = money_in_words(amount, self.currency) def calculate_commission(self): - if self.meta.get_field("commission_rate"): - self.round_floats_in(self, ["base_net_total", "commission_rate"]) - if self.commission_rate > 100.0: - throw(_("Commission rate cannot be greater than 100")) + if not self.meta.get_field("commission_rate"): + return - self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0, - self.precision("total_commission")) + self.round_floats_in( + self, ("amount_eligible_for_commission", "commission_rate") + ) + + if not (0 <= self.commission_rate <= 100.0): + throw("{} {}".format( + _(self.meta.get_label("commission_rate")), + _("must be between 0 and 100"), + )) + + self.amount_eligible_for_commission = sum( + item.base_net_amount for item in self.items if item.grant_commission + ) + + self.total_commission = flt( + self.amount_eligible_for_commission * self.commission_rate / 100.0, + self.precision("total_commission") + ) def calculate_contribution(self): if not self.meta.get_field("sales_team"): @@ -138,7 +152,7 @@ class SellingController(StockController): self.round_floats_in(sales_person) sales_person.allocated_amount = flt( - self.base_net_total * sales_person.allocated_percentage / 100.0, + self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, self.precision("allocated_amount", sales_person)) if sales_person.commission_rate: diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7c7ed9a960..7e99a06243 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -134,6 +134,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1507,16 +1508,23 @@ "fieldtype": "Small Text", "label": "Dispatch Address", "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:09:51.515542", + "modified": "2021-10-05 12:16:40.775704", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 1e5590e748..95f6c4e96d 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -48,6 +48,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_24", "net_rate", "net_amount", @@ -789,15 +790,23 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:15:05.803091", + "modified": "2021-10-05 12:27:25.014789", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 20504789aa..e2e0db4044 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -157,25 +157,19 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran commission_rate() { this.calculate_commission(); - refresh_field("total_commission"); } total_commission() { - if(this.frm.doc.base_net_total) { - frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]); + frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); - if(this.frm.doc.base_net_total < this.frm.doc.total_commission) { - var msg = (__("[Error]") + " " + - __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission", - this.frm.doc.name)) + " > " + - __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name))); - frappe.msgprint(msg); - throw msg; - } + const { amount_eligible_for_commission } = this.frm.doc; + if(!amount_eligible_for_commission) return; - this.frm.set_value("commission_rate", - flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total)); - } + this.frm.set_value( + "commission_rate", flt( + this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission + ) + ); } allocated_percentage(doc, cdt, cdn) { @@ -185,7 +179,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran sales_person.allocated_percentage = flt(sales_person.allocated_percentage, precision("allocated_percentage", sales_person)); - sales_person.allocated_amount = flt(this.frm.doc.base_net_total * + sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, precision("allocated_amount", sales_person)); refresh_field(["allocated_amount"], sales_person); @@ -259,28 +253,39 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } calculate_commission() { - if(this.frm.fields_dict.commission_rate) { - if(this.frm.doc.commission_rate > 100) { - var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) + - " " + __("cannot be greater than 100"); - frappe.msgprint(msg); - throw msg; - } + if(!this.frm.fields_dict.commission_rate) return; - this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0, - precision("total_commission")); + if(this.frm.doc.commission_rate > 100) { + this.frm.set_value("commission_rate", 100); + frappe.throw(`${__(frappe.meta.get_label( + this.frm.doc.doctype, "commission_rate", this.frm.doc.name + ))} ${__("cannot be greater than 100")}`); } + + this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( + (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 + ) + + this.frm.doc.total_commission = flt( + this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, + precision("total_commission") + ); + + refresh_field(["amount_eligible_for_commission", "total_commission"]); } calculate_contribution() { var me = this; $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { frappe.model.round_floats_in(sales_person); - if(sales_person.allocated_percentage) { - sales_person.allocated_amount = flt( - me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0, - precision("allocated_amount", sales_person)); - } + if (!sales_person.allocated_percentage) return; + + sales_person.allocated_amount = flt( + me.frm.doc.amount_eligible_for_commission + * sales_person.allocated_percentage + / 100.0, + precision("allocated_amount", sales_person) + ); }); } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ad1b3b43ae..e78d5010eb 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -145,6 +145,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1302,6 +1303,12 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-truck", @@ -1312,6 +1319,7 @@ "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index a96c29925e..51c88bed61 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49,6 +49,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_25", "net_rate", "net_amount", @@ -753,13 +754,20 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:12:44.018872", + "modified": "2021-10-06 12:12:44.018872", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index cd97ec31a4..5469a9fc25 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -88,6 +88,7 @@ "sales_details", "sales_uom", "is_sales_item", + "grant_commission", "column_break3", "max_discount", "deferred_revenue", @@ -1020,6 +1021,12 @@ "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" + }, + { + "default": "1", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission" } ], "has_web_view": 1, @@ -1028,7 +1035,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-30 01:33:06.572442", + "modified": "2021-11-30 02:33:06.572442", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cd180a42ca..9889a22b35 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -327,7 +327,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_uom": args.get("weight_uom") or item.get("weight_uom"), + "grant_commission": item.get("grant_commission") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): From 0854c183aaa6e443d30be72f8532954986a4d3e5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 30 Nov 2021 17:57:53 +0530 Subject: [PATCH 073/247] chore: remove duplicate code (#28646) [skip ci] --- erpnext/hr/doctype/employee/employee.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 79e8f6140a..88e5ca9d4c 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -96,15 +96,8 @@ class Employee(NestedSet): 'user': self.user_id }) - if employee_user_permission_exists: return - - employee_user_permission_exists = frappe.db.exists('User Permission', { - 'allow': 'Employee', - 'for_value': self.name, - 'user': self.user_id - }) - - if employee_user_permission_exists: return + if employee_user_permission_exists: + return add_user_permission("Employee", self.name, self.user_id) set_user_permission_if_allowed("Company", self.company, self.user_id) From 071118fa4ebac9ec9d7ed435331568f1feb196f0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Nov 2021 13:15:20 +0000 Subject: [PATCH 074/247] fix: Unable to search project by project name in Sales Invoice (bp #28648) (cherry picked from commit 08b7c856b2ee94d1f8ac2c019c556eef4d7dd7da) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../doctype/sales_invoice/sales_invoice.js | 9 --------- erpnext/controllers/queries.py | 14 +++++++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 4538675d07..39dfd8d76d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -516,15 +516,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { } } -// project name -//-------------------------- -cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) { - return{ - query: "erpnext.controllers.queries.get_project_name", - filters: {'customer': doc.customer} - } -} - // Income Account in Details Table // -------------------------------- cur_frm.set_query("income_account", "items", function(doc) { diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 76a7d1a95f..dc04dab84c 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -539,6 +539,10 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) dimension_filters = get_dimension_filter_map() dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) query_filters = [] + or_filters = [] + fields = ['name'] + + searchfields = frappe.get_meta(doctype).get_search_fields() meta = frappe.get_meta(doctype) if meta.is_tree: @@ -550,8 +554,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) if meta.has_field('company'): query_filters.append(['company', '=', filters.get('company')]) - if txt: - query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt]) + for field in searchfields: + or_filters.append([field, 'LIKE', "%%%s%%" % txt]) + fields.append(field) if dimension_filters: if dimension_filters['allow_or_restrict'] == 'Allow': @@ -566,10 +571,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) query_filters.append(['name', query_selector, dimensions]) - output = frappe.get_list(doctype, filters=query_filters) - result = [d.name for d in output] + output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1) - return [(d,) for d in set(result)] + return [tuple(d) for d in set(output)] @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 0963fceede32b6a5fd273283e21393cbaf03c091 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 30 Nov 2021 22:05:29 +0530 Subject: [PATCH 075/247] fix: Taxjar Nexus list visible only if child table is visible --- .../taxjar_settings/taxjar_settings.json | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json index 23ccb7e4da..ae1f36e73f 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json @@ -20,7 +20,6 @@ "configuration_cb", "shipping_account_head", "section_break_12", - "nexus_address", "nexus" ], "fields": [ @@ -87,15 +86,11 @@ "fieldtype": "Column Break" }, { + "depends_on": "nexus", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Nexus List" }, - { - "fieldname": "nexus_address", - "fieldtype": "HTML", - "label": "Nexus Address" - }, { "fieldname": "nexus", "fieldtype": "Table", @@ -107,20 +102,21 @@ "fieldname": "configuration_cb", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" } ], "issingle": 1, "links": [], - "modified": "2021-11-08 18:02:29.232090", + "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f", + "modified": "2021-11-30 11:17:24.647979", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "TaxJar Settings", From a473e1dbe9c82724eb17502e5e7fbf62f2cf7cb7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 01:27:34 +0530 Subject: [PATCH 076/247] fix: Add item to packing list --- .../stock/doctype/packed_item/packed_item.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 0ce8ee4584..e4091c40dc 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -106,15 +106,34 @@ def cleanup_packing_list(doc, parent_items): if not delete_list: return doc - index = 1 packed_items = doc.get("packed_items") doc.set("packed_items", []) for d in packed_items: if d not in delete_list: - d.idx = index - doc.append("packed_items", d) - index += 1 + add_item_to_packing_list(doc, d) + +def add_item_to_packing_list(doc, packed_item): + doc.append("packed_items", { + 'parent_item': packed_item.parent_item, + 'item_code': packed_item.item_code, + 'item_name': packed_item.item_name, + 'uom': packed_item.uom, + 'qty': packed_item.qty, + 'rate': packed_item.rate, + 'conversion_factor': packed_item.conversion_factor, + 'description': packed_item.description, + 'warehouse': packed_item.warehouse, + 'batch_no': packed_item.batch_no, + 'actual_batch_qty': packed_item.actual_batch_qty, + 'serial_no': packed_item.serial_no, + 'target_warehouse': packed_item.target_warehouse, + 'actual_qty': packed_item.actual_qty, + 'projected_qty': packed_item.projected_qty, + 'incoming_rate': packed_item.incoming_rate, + 'prevdoc_doctype': packed_item.prevdoc_doctype, + 'parent_detail_docname': packed_item.parent_detail_docname + }) def update_product_bundle_price(doc, parent_items): """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" From ab280f714222f0f2c31db3b4c1194bad9630a8a9 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 1 Dec 2021 11:01:49 +0530 Subject: [PATCH 077/247] fix: update modified timestamp for delivery note #28657 fix: update `modified` timestamp for delivery note --- erpnext/stock/doctype/delivery_note/delivery_note.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index e78d5010eb..55a4c956a6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1315,7 +1315,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-10-08 14:29:13.428984", + "modified": "2021-10-09 14:29:13.428984", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From ecc5de6159723bc0845bf67387d566ce43047d18 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 1 Dec 2021 11:54:59 +0530 Subject: [PATCH 078/247] fix: removing db call for variables --- .../doctype/taxjar_settings/taxjar_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py index b9f24b65b3..d4bbe881d0 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py @@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client class TaxJarSettings(Document): def on_update(self): - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") - TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") - TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") + TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions + TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax + TAXJAR_SANDBOX_MODE = self.is_sandbox fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'}) fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden') From 01d8b8519f7a537ada08250446e76847ff544cdc Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 1 Dec 2021 12:11:30 +0530 Subject: [PATCH 079/247] fix: cannot load company form (#28535) --- erpnext/setup/doctype/company/company.js | 7 ++++--- erpnext/setup/doctype/company/company.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 95ca3867ee..91f60fbd4e 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -12,6 +12,10 @@ frappe.ui.form.on("Company", { } }); } + + frm.call('check_if_transactions_exist').then(r => { + frm.toggle_enable("default_currency", (!r.message)); + }); }, setup: function(frm) { erpnext.company.setup_queries(frm); @@ -87,9 +91,6 @@ frappe.ui.form.on("Company", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} - frm.toggle_enable("default_currency", (frm.doc.__onload && - !frm.doc.__onload.transactions_exist)); - if (frappe.perm.has_perm("Cost Center", 0, 'read')) { frm.add_custom_button(__('Cost Centers'), function() { frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index dedd2d3f55..e739739458 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -22,8 +22,8 @@ class Company(NestedSet): def onload(self): load_address_and_contact(self, "company") - self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist() + @frappe.whitelist() def check_if_transactions_exist(self): exists = False for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", From 3708cf17adb7a67ed64a5d03d3bedca0b896899a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 1 Dec 2021 12:14:06 +0530 Subject: [PATCH 080/247] fix(POS Profile): replace `cur_frm` with `frm` (#28390) --- .../doctype/pos_profile/pos_profile.js | 177 +++++++++--------- 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index efdeb1a5e8..813d20dbf9 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -3,22 +3,20 @@ {% include "erpnext/public/js/controllers/accounts.js" %} -frappe.ui.form.on("POS Profile", "onload", function(frm) { - frm.set_query("selling_price_list", function() { - return { filters: { selling: 1 } }; - }); - - frm.set_query("tc_name", function() { - return { filters: { selling: 1 } }; - }); - - erpnext.queries.setup_queries(frm, "Warehouse", function() { - return erpnext.queries.warehouse(frm.doc); - }); -}); - frappe.ui.form.on('POS Profile', { setup: function(frm) { + frm.set_query("selling_price_list", function() { + return { filters: { selling: 1 } }; + }); + + frm.set_query("tc_name", function() { + return { filters: { selling: 1 } }; + }); + + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); + }); + frm.set_query("print_format", function() { return { filters: [ @@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', { }; }); - frm.set_query("account_for_change_amount", function() { + frm.set_query("account_for_change_amount", function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + is_group: 0, + company: doc.company } }; }); @@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', { }); frm.set_query('company_address', function(doc) { - if(!doc.company) { + if (!doc.company) { frappe.throw(__('Please set Company')); } @@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', { }; }); + frm.set_query('income_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'is_group': 0, + 'company': doc.company, + 'account_type': "Income Account" + } + }; + }); + + frm.set_query('cost_center', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'company': doc.company, + 'is_group': 0 + } + }; + }); + + frm.set_query('expense_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + "report_type": "Profit and Loss", + "company": doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("select_print_heading", function() { + return { + filters: [ + ['Print Heading', 'docstatus', '!=', 2] + ] + }; + }); + + frm.set_query("write_off_account", function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'is_group': 0, + 'company': doc.company + } + }; + }); + + frm.set_query("write_off_cost_center", function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { - if(frm.doc.company) { + if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } }, @@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', { frm.toggle_display('expense_account', erpnext.is_perpetual_inventory_enabled(frm.doc.company)); } -}) - -// Income Account -// -------------------------------- -cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company, - 'account_type': "Income Account" - } - }; -}; - - -// Cost Center -// ----------------------------- -cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'company': doc.company, - 'is_group': 0 - } - }; -}; - - -// Expense Account -// ----------------------------- -cur_frm.fields_dict["expense_account"].get_query = function(doc) { - return { - filters: { - "report_type": "Profit and Loss", - "company": doc.company, - "is_group": 0 - } - }; -}; - -// ------------------ Get Print Heading ------------------------------------ -cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) { - return{ - filters:[ - ['Print Heading', 'docstatus', '!=', 2] - ] - }; -}; - -cur_frm.fields_dict.write_off_account.get_query = function(doc) { - return{ - filters:{ - 'report_type': 'Profit and Loss', - 'is_group': 0, - 'company': doc.company - } - }; -}; - -// Write off cost center -// ----------------------- -cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company - } - }; -}; +}); From 445966ab80293b58657fd1c8505f27c51a962bd1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Dec 2021 16:34:05 +0530 Subject: [PATCH 081/247] test: timeout certain tests in work order to avoid stuck tests (#28666) [skip ci] --- .../doctype/work_order/test_work_order.py | 5 ++++- erpnext/tests/utils.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f4a88dc459..f590d680d3 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -21,9 +21,10 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin +from erpnext.tests.utils import ERPNextTestCase, timeout -class TestWorkOrder(unittest.TestCase): +class TestWorkOrder(ERPNextTestCase): def setUp(self): self.warehouse = '_Test Warehouse 2 - _TC' self.item = '_Test Item' @@ -376,6 +377,7 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(len(ste.additional_costs), 1) self.assertEqual(ste.total_additional_costs, 1000) + @timeout(seconds=60) def test_job_card(self): stock_entries = [] bom = frappe.get_doc('BOM', { @@ -769,6 +771,7 @@ class TestWorkOrder(unittest.TestCase): total_pl_qty ) + @timeout(seconds=60) def test_job_card_scrap_item(self): items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 91df5480e3..fbf25948a7 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import copy +import signal import unittest from contextlib import contextmanager from typing import Any, Dict, NewType, Optional @@ -135,3 +136,23 @@ def execute_script_report( report_execute_fn(filter_with_optional_param) return report_data + + +def timeout(seconds=30, error_message="Test timed out."): + """ Timeout decorator to ensure a test doesn't run for too long. + + adapted from https://stackoverflow.com/a/2282656""" + def decorator(func): + def _handle_timeout(signum, frame): + raise Exception(error_message) + + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.alarm(seconds) + try: + result = func(*args, **kwargs) + finally: + signal.alarm(0) + return result + return wrapper + return decorator From fdffa037b5aa7fe368caa44c365e679401263c96 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Dec 2021 17:31:17 +0530 Subject: [PATCH 082/247] test: dynamic fiscal year creation in tests (#28667) --- .../doctype/fiscal_year/test_fiscal_year.py | 28 +++++++- .../doctype/fiscal_year/test_records.json | 69 ------------------- 2 files changed, 27 insertions(+), 70 deletions(-) delete mode 100644 erpnext/accounts/doctype/fiscal_year/test_records.json diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index bc8c6abeff..69e13a407d 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -5,10 +5,10 @@ import unittest import frappe +from frappe.utils import now_datetime from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate -test_records = frappe.get_test_records('Fiscal Year') test_ignore = ["Company"] class TestFiscalYear(unittest.TestCase): @@ -25,3 +25,29 @@ class TestFiscalYear(unittest.TestCase): }) self.assertRaises(FiscalYearIncorrectDate, fy.insert) + + +def test_record_generator(): + test_records = [ + { + "doctype": "Fiscal Year", + "year": "_Test Short Fiscal Year 2011", + "is_short_year": 1, + "year_end_date": "2011-04-01", + "year_start_date": "2011-12-31" + } + ] + + start = 2012 + end = now_datetime().year + 5 + for year in range(start, end): + test_records.append({ + "doctype": "Fiscal Year", + "year": f"_Test Fiscal Year {year}", + "year_start_date": f"{year}-01-01", + "year_end_date": f"{year}-12-31" + }) + + return test_records + +test_records = test_record_generator() diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json deleted file mode 100644 index 44052535cb..0000000000 --- a/erpnext/accounts/doctype/fiscal_year/test_records.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "doctype": "Fiscal Year", - "year": "_Test Short Fiscal Year 2011", - "is_short_year": 1, - "year_end_date": "2011-04-01", - "year_start_date": "2011-12-31" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2012", - "year_end_date": "2012-12-31", - "year_start_date": "2012-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2013", - "year_end_date": "2013-12-31", - "year_start_date": "2013-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2014", - "year_end_date": "2014-12-31", - "year_start_date": "2014-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2015", - "year_end_date": "2015-12-31", - "year_start_date": "2015-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2016", - "year_end_date": "2016-12-31", - "year_start_date": "2016-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2017", - "year_end_date": "2017-12-31", - "year_start_date": "2017-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2018", - "year_end_date": "2018-12-31", - "year_start_date": "2018-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2019", - "year_end_date": "2019-12-31", - "year_start_date": "2019-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2020", - "year_end_date": "2020-12-31", - "year_start_date": "2020-01-01" - }, - { - "doctype": "Fiscal Year", - "year": "_Test Fiscal Year 2021", - "year_end_date": "2021-12-31", - "year_start_date": "2021-01-01" - } -] From 22cc8d22462d50ef134651e7df2c36564fb7edf6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 21:46:09 +0530 Subject: [PATCH 083/247] fix: Fix depreciation_amount calculation --- erpnext/regional/india/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9746fdef84..0de8347d35 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -847,12 +847,12 @@ def update_taxable_values(doc, method): doc.get('items')[item_count - 1].taxable_value += diff def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) + depreciation_left = flt(row.total_number_of_depreciations) if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - + depreciation_amount = (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / depreciation_left # if the Depreciation Schedule is being modified after Asset Repair From 5c3d4caedacbdfc48256a8554c7747a753ace7e2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 21:48:47 +0530 Subject: [PATCH 084/247] fix: Create Depreciation Schedules properly for existing Assets --- erpnext/assets/doctype/asset/asset.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2..d6af487e21 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -192,8 +192,7 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = flt(d.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -241,7 +240,7 @@ class Asset(AccountsController): break # For first row - if has_pro_rata and n==0: + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, self.available_for_use_date, d.depreciation_start_date) @@ -254,7 +253,7 @@ class Asset(AccountsController): if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount @@ -402,10 +401,11 @@ class Asset(AccountsController): # to ensure that final accumulated depreciation amount is accurate def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): - depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) - if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: - depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: + depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row return depreciation_amount_for_last_row @@ -850,12 +850,12 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) + depreciation_left = flt(row.total_number_of_depreciations) if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - + depreciation_amount = (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / depreciation_left # if the Depreciation Schedule is being modified after Asset Repair From de002005acc509d025b642b8de0823b2e807f11e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 22:04:56 +0530 Subject: [PATCH 085/247] fix: Modify has_pro_rata() to include existing assets --- erpnext/assets/doctype/asset/asset.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d6af487e21..e37fb6631a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -353,7 +353,12 @@ class Asset(AccountsController): # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if not existing asset, from_date = available_for_use_date + # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + from_date = self.get_modified_available_for_use_date(row) + days = date_diff(row.depreciation_start_date, from_date) + 1 # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) @@ -363,6 +368,9 @@ class Asset(AccountsController): return has_pro_rata + def get_modified_available_for_use_date(self, row): + return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation)) + def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") From 774ac852c95d2a63ef5cde24e46c9f0fece36505 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 1 Dec 2021 22:51:55 +0530 Subject: [PATCH 086/247] fix: Test if depreciation schedules are set up properly for existing assets --- erpnext/assets/doctype/asset/test_asset.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec7..0e8ceb54a7 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -409,19 +409,18 @@ class TestDepreciationMethods(AssetSetup): calculate_depreciation = 1, available_for_use_date = "2030-06-06", is_existing_asset = 1, - number_of_depreciations_booked = 1, - opening_accumulated_depreciation = 40000, + number_of_depreciations_booked = 2, + opening_accumulated_depreciation = 47095.89, expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", + depreciation_start_date = "2032-12-31", total_number_of_depreciations = 3, frequency_of_depreciation = 12 ) self.assertEqual(asset.status, "Draft") expected_schedules = [ - ["2030-12-31", 14246.58, 54246.58], - ["2031-12-31", 25000.00, 79246.58], - ["2032-06-06", 10753.42, 90000.00] + ["2032-12-31", 30000.0, 77095.89], + ["2033-06-06", 12904.11, 90000.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules")] From 828769ca707460c5f04ddf8a5900b57188d0856f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:09:15 +0530 Subject: [PATCH 087/247] fix: Remove unnecessary variable --- erpnext/assets/doctype/asset/asset.py | 4 +--- erpnext/regional/india/utils.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e37fb6631a..333906a815 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -858,13 +858,11 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / depreciation_left + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0de8347d35..12d8d209a6 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -847,13 +847,11 @@ def update_taxable_values(doc, method): doc.get('items')[item_count - 1].taxable_value += diff def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / depreciation_left + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: From 6f1cf94c9fb2c7f7abda9802c4868b427a339ed9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:18:30 +0530 Subject: [PATCH 088/247] fix: Fix 'Adjust Asset Value' button --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2..a053c7fab9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,7 +724,7 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.get_doc("Asset Value Adjustment") + asset_maintenance = frappe.new_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, From 5b224f841b0419d0250f02549300e0754fa816b5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:19:35 +0530 Subject: [PATCH 089/247] fix: Rename variable --- erpnext/assets/doctype/asset/asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a053c7fab9..ff5964be68 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,13 +724,13 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") - asset_maintenance.update({ + asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") + asset_value_adjustment.update({ "asset": asset, "company": company, "asset_category": asset_category }) - return asset_maintenance + return asset_value_adjustment @frappe.whitelist() def transfer_asset(args): From 4629308d94b4fc2a667a7eb7d526b2a810bb486a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:22:18 +0530 Subject: [PATCH 090/247] fix: Rename function --- erpnext/assets/doctype/asset/asset.js | 6 +++--- erpnext/assets/doctype/asset/asset.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d..9fa50a9ade 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', { if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_adjustment"); + frm.trigger("create_asset_value_adjustment"); }, __("Manage")); } @@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', { }); }, - create_asset_adjustment: function(frm) { + create_asset_value_adjustment: function(frm) { frappe.call({ args: { "asset": frm.doc.name, "asset_category": frm.doc.asset_category, "company": frm.doc.company }, - method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", + method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, callback: function(r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ff5964be68..f2b02cb9c5 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -723,7 +723,7 @@ def create_asset_repair(asset, asset_name): return asset_repair @frappe.whitelist() -def create_asset_adjustment(asset, asset_category, company): +def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") asset_value_adjustment.update({ "asset": asset, From 427938f87bd06ff0803ce7c1a079cb455fd4f96d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Dec 2021 11:25:47 +0530 Subject: [PATCH 091/247] fix: new item form tour for home onboarding --- .../form_tour/item_general/item_general.json | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 erpnext/stock/form_tour/item_general/item_general.json diff --git a/erpnext/stock/form_tour/item_general/item_general.json b/erpnext/stock/form_tour/item_general/item_general.json new file mode 100644 index 0000000000..b468d270de --- /dev/null +++ b/erpnext/stock/form_tour/item_general/item_general.json @@ -0,0 +1,79 @@ +{ + "creation": "2021-12-02 10:37:55.433087", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-12-02 10:37:55.433087", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item General", + "owner": "Administrator", + "reference_doctype": "Item", + "save_on_complete": 1, + "steps": [ + { + "description": "Enter code for the Item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Code", + "parent_field": "", + "position": "Right", + "title": "Item Code" + }, + { + "description": "Enter name for the Item", + "field": "", + "fieldname": "item_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Name", + "parent_field": "", + "position": "Right", + "title": "Item Name" + }, + { + "description": "Select an Item Group", + "field": "", + "fieldname": "item_group", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Group", + "parent_field": "", + "position": "Right", + "title": "Item Group" + }, + { + "description": "This is the default measuring unit that you will use for your product. It could be Nos, Kgs, Meters, etc.", + "field": "", + "fieldname": "stock_uom", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Unit of Measure", + "parent_field": "", + "position": "Right", + "title": "Default Unit of Measurement" + }, + { + "description": "When creating an Item, entering a value for this field will automatically create an Item Price at the backend. Entering a value after the Item has been saved will not work. In this case, the Item Price is created from any transactions with the Item.", + "field": "", + "fieldname": "standard_rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Standard Selling Rate", + "parent_field": "", + "position": "Left", + "title": "Standard Selling Rate" + } + ], + "title": "Item General" +} \ No newline at end of file From a108df8efc9e26fcece1f2f742902e20c9dd30c4 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Dec 2021 11:27:37 +0530 Subject: [PATCH 092/247] fix: assigned proper form tour to onboarding steps --- erpnext/assets/module_onboarding/assets/assets.json | 2 +- .../onboarding_step/asset_category/asset_category.json | 2 +- .../assets/onboarding_step/asset_item/asset_item.json | 9 +++++---- .../onboarding_step/asset_purchase/asset_purchase.json | 2 +- .../fixed_asset_accounts/fixed_asset_accounts.json | 2 +- erpnext/setup/module_onboarding/home/home.json | 2 +- .../onboarding_step/company_set_up/company_set_up.json | 2 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_quotation/create_a_quotation.json | 2 +- .../create_a_supplier/create_a_supplier.json | 2 +- .../onboarding_step/create_an_item/create_an_item.json | 3 ++- .../setup/onboarding_step/data_import/data_import.json | 2 +- erpnext/setup/onboarding_step/letterhead/letterhead.json | 2 +- .../onboarding_step/navigation_help/navigation_help.json | 2 +- 14 files changed, 19 insertions(+), 17 deletions(-) diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json index e6df88b000..796245df0c 100644 --- a/erpnext/assets/module_onboarding/assets/assets.json +++ b/erpnext/assets/module_onboarding/assets/assets.json @@ -13,7 +13,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset", "idx": 0, "is_complete": 0, - "modified": "2021-08-24 17:50:41.573281", + "modified": "2021-12-02 11:24:37.963746", "modified_by": "Administrator", "module": "Assets", "name": "Assets", diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json index 033e86669c..58f322eecf 100644 --- a/erpnext/assets/onboarding_step/asset_category/asset_category.json +++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 12:49:37.665239", + "modified": "2021-11-23 10:02:03.242127", "modified_by": "Administrator", "name": "Asset Category", "owner": "Administrator", diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json index 8a174c5b77..13e3e2e838 100644 --- a/erpnext/assets/onboarding_step/asset_item/asset_item.json +++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json @@ -1,21 +1,22 @@ { - "action": "Show Form Tour", + "action": "Create Entry", "action_label": "Let's create a new Asset item", "creation": "2021-08-13 14:27:07.277167", "description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ", "docstatus": 0, "doctype": "Onboarding Step", + "form_tour": "Item", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-16 13:59:18.362233", + "modified": "2021-12-02 11:23:48.158504", "modified_by": "Administrator", "name": "Asset Item", "owner": "Administrator", "reference_document": "Item", - "show_form_tour": 0, - "show_full_form": 0, + "show_form_tour": 1, + "show_full_form": 1, "title": "Create an Asset Item", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json index 54611edc29..69fa337ae0 100644 --- a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json +++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 17:26:57.180637", + "modified": "2021-11-23 10:02:03.235498", "modified_by": "Administrator", "name": "Asset Purchase", "owner": "Administrator", diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json index cebee7a7ea..2fc6c46c14 100644 --- a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json +++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 17:46:37.646174", + "modified": "2021-11-23 10:02:03.229566", "modified_by": "Administrator", "name": "Fixed Asset Accounts", "owner": "Administrator", diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index fbff98ff8a..e3868163cf 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,7 +25,7 @@ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2021-11-24 16:40:15.154081", + "modified": "2021-12-02 11:24:49.108119", "modified_by": "Administrator", "module": "Setup", "name": "Home", diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json index dc85189764..57a3a3c2ed 100644 --- a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:41:01.620963", + "modified": "2021-11-24 18:01:53.528032", "modified_by": "Administrator", "name": "Company Set Up", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index c3162f2b87..3f67809773 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:20.882675", + "modified": "2021-11-24 18:01:53.508901", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json index 12d4372470..039d32b69e 100644 --- a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:55:28.950596", + "modified": "2021-12-02 11:16:01.553277", "modified_by": "Administrator", "name": "Create a Quotation", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json index cc87d97ae4..02e8a4021a 100644 --- a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:36.656949", + "modified": "2021-11-24 18:01:53.499200", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json index 0d650de665..177525e5fb 100644 --- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -5,12 +5,13 @@ "description": "Item is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n", "docstatus": 0, "doctype": "Onboarding Step", + "form_tour": "Item General", "idx": 0, "intro_video_url": "", "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:51:21.446969", + "modified": "2021-12-02 11:16:01.451477", "modified_by": "Administrator", "name": "Create an Item", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json index 5f4e8db4ab..f4628b7f08 100644 --- a/erpnext/setup/onboarding_step/data_import/data_import.json +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:54:37.622063", + "modified": "2021-11-24 18:01:53.480435", "modified_by": "Administrator", "name": "Data import", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json index b8d430365f..05ca5ca628 100644 --- a/erpnext/setup/onboarding_step/letterhead/letterhead.json +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 16:19:25.379798", + "modified": "2021-11-24 18:01:53.459675", "modified_by": "Administrator", "name": "Letterhead", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json index 1271a86cb9..1cc58cb08b 100644 --- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:40.641949", + "modified": "2021-11-24 18:01:53.490470", "modified_by": "Administrator", "name": "Navigation Help", "owner": "Administrator", From 9bc28210c9c4730790611d2ec61588173dbed2d5 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Thu, 2 Dec 2021 11:31:38 +0530 Subject: [PATCH 093/247] fix: Make buttons translatable (#28679) --- erpnext/assets/doctype/asset/asset.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d..c2b1bbcf14 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { - frm.add_custom_button("Transfer Asset", function() { + frm.add_custom_button(__("Transfer Asset"), function() { erpnext.asset.transfer_asset(frm); }, __("Manage")); - frm.add_custom_button("Scrap Asset", function() { + frm.add_custom_button(__("Scrap Asset"), function() { erpnext.asset.scrap_asset(frm); }, __("Manage")); - frm.add_custom_button("Sell Asset", function() { + frm.add_custom_button(__("Sell Asset"), function() { frm.trigger("make_sales_invoice"); }, __("Manage")); } else if (frm.doc.status=='Scrapped') { - frm.add_custom_button("Restore Asset", function() { + frm.add_custom_button(__("Restore Asset"), function() { erpnext.asset.restore_asset(frm); }, __("Manage")); } @@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', { } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("View General Ledger", function() { + frm.add_custom_button(__("View General Ledger"), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.available_for_use_date, From 4889661e5c5577a89bee491326a1c5c246cd0d4c Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 2 Dec 2021 11:37:08 +0530 Subject: [PATCH 094/247] fix: actual tax conversion in case of multicurrency invoices (#28539) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0cfc008c13..773d53c552 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1106,7 +1106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe $.each(this.frm.doc.taxes || [], function(i, d) { if(d.charge_type == "Actual") { frappe.model.set_value(d.doctype, d.name, "tax_amount", - flt(d.tax_amount) / flt(exchange_rate)); + flt(d.base_tax_amount) / flt(exchange_rate)); } }); } From 6f67bbca69a1dcd7a9dd9918f0c656704ecc14a7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 13:39:05 +0530 Subject: [PATCH 095/247] refactor(SLA): remove response_by_variance & resolution_by_variance --- erpnext/support/doctype/issue/issue.json | 22 +- erpnext/support/doctype/issue/issue.py | 4 +- erpnext/support/doctype/issue/test_issue.py | 109 +++++-- .../service_level_agreement.py | 269 ++++++++---------- .../test_service_level_agreement.py | 68 ++--- 5 files changed, 221 insertions(+), 251 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 75b6d0f4f9..1da22fd58f 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -24,12 +24,10 @@ "service_level_section", "service_level_agreement", "response_by", - "response_by_variance", "reset_service_level_agreement", "cb", "agreement_status", "resolution_by", - "resolution_by_variance", "service_level_agreement_creation", "on_hold_since", "total_hold_time", @@ -44,8 +42,6 @@ "opening_date", "opening_time", "resolution_date", - "resolution_time", - "user_resolution_time", "additional_info", "lead", "contact", @@ -317,22 +313,6 @@ "fieldtype": "Check", "label": "Via Customer Portal" }, - { - "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, - { - "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", @@ -395,7 +375,7 @@ "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", + "options": "First Response Due\nResolution Due\nFulfilled\nFailed", "read_only": 1 }, { diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 0dc3639f1e..d5e5b78288 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -87,11 +87,9 @@ class Issue(Document): if replicated_issue.service_level_agreement: replicated_issue.service_level_agreement_creation = now_datetime() replicated_issue.service_level_agreement = None - replicated_issue.agreement_status = "Ongoing" + replicated_issue.agreement_status = "First Response Due" replicated_issue.response_by = None - replicated_issue.response_by_variance = None replicated_issue.resolution_by = None - replicated_issue.resolution_by_variance = None replicated_issue.reset_issue_metrics() frappe.get_doc(replicated_issue).insert() diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 0559b15649..da9953df55 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -83,30 +83,6 @@ class TestIssue(TestSetUp): self.assertEqual(issue.agreement_status, 'Fulfilled') - def test_issue_metrics(self): - creation = get_datetime("2020-03-04 4:00") - - issue = make_issue(creation, index=1) - create_communication(issue.name, "test@example.com", "Received", creation) - - creation = get_datetime("2020-03-04 4:15") - create_communication(issue.name, "test@admin.com", "Sent", creation) - - creation = get_datetime("2020-03-04 5:00") - create_communication(issue.name, "test@example.com", "Received", creation) - - creation = get_datetime("2020-03-04 5:05") - create_communication(issue.name, "test@admin.com", "Sent", creation) - - frappe.flags.current_time = get_datetime("2020-03-04 5:05") - issue.reload() - issue.status = 'Closed' - issue.save() - - self.assertEqual(issue.avg_response_time, 600) - self.assertEqual(issue.resolution_time, 3900) - self.assertEqual(issue.user_resolution_time, 1200) - def test_hold_time_on_replied(self): creation = get_datetime("2020-03-04 4:00") @@ -143,16 +119,15 @@ class TestIssue(TestSetUp): self.assertEqual(flt(issue.total_hold_time, 2), 2700) def test_issue_close_after_on_hold(self): - creation = get_datetime("2021-11-01 19:00") + frappe.flags.current_time = get_datetime("2021-11-01 19:00") - issue = make_issue(creation, index=1) - create_communication(issue.name, "test@example.com", "Received", creation) + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) # send a reply within SLA - creation = get_datetime("2021-11-02 11:00") - create_communication(issue.name, "test@admin.com", "Sent", creation) + frappe.flags.current_time = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) - frappe.flags.current_time = creation issue.reload() issue.status = 'Replied' issue.save() @@ -168,6 +143,75 @@ class TestIssue(TestSetUp): self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00')) self.assertEqual(issue.agreement_status, 'Fulfilled') + def test_issue_open_after_closed(self): + + # Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs + frappe.flags.current_time = get_datetime("2021-11-01 13:00") + issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + self.assertEquals(issue.agreement_status, 'First Response Due') + self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00")) + self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00")) + + # Replied on → 2 pm + frappe.flags.current_time = get_datetime("2021-11-01 14:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + issue.reload() + issue.status = 'Replied' + issue.save() + self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.on_hold_since, frappe.flags.current_time) + self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + + # Customer Replied → 3 pm + frappe.flags.current_time = get_datetime("2021-11-01 15:00") + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + issue.reload() + self.assertEquals(issue.status, 'Open') + # Hold Time + 1 Hrs + self.assertEquals(issue.total_hold_time, 3600) + # Resolution By should increase by one hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00")) + + # Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm + frappe.flags.current_time = get_datetime("2021-11-01 16:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + issue.reload() + issue.status = 'Replied' + issue.save() + self.assertEquals(issue.agreement_status, 'Resolution Due') + + # Customer Closed → 10 pm + frappe.flags.current_time = get_datetime("2021-11-01 22:00") + issue.status = 'Closed' + issue.save() + # Hold Time + 6 Hrs + self.assertEquals(issue.total_hold_time, 3600 + 21600) + # Resolution By should increase by 6 hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00")) + self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.resolution_date, frappe.flags.current_time) + + # Customer Open → 3 am i.e after resolution by is crossed + frappe.flags.current_time = get_datetime("2021-11-02 03:00") + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + issue.reload() + # Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm) + self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000) + # Resolution By should increase by 5 hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertFalse(issue.resolution_date) + + # We Closed → 4 am, SLA should be Fulfilled + frappe.flags.current_time = get_datetime("2021-11-02 04:00") + issue.status = 'Closed' + issue.save() + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.resolution_date, frappe.flags.current_time) + + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm # all dates are in the mm-dd-yyyy format @@ -386,7 +430,10 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue -def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None, do_not_insert=False): + if not frappe.db.exists('Issue Type', issue_type): + frappe.get_doc(dict(doctype='Issue Type', name=issue_type)).insert() + issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9c1e536078..e2326ac2df 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -358,36 +358,102 @@ def apply(doc, method=None): ): return - service_level_agreement = get_active_service_level_agreement_for(doc) + sla = get_active_service_level_agreement_for(doc) - if not service_level_agreement: + if not sla: return - process_sla(doc, service_level_agreement) + process_sla(doc, sla) -def process_sla(doc, service_level_agreement): +def process_sla(doc, sla): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) if doc.meta.has_field("service_level_agreement_creation"): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.get("priority") or service_level_agreement.default_priority + doc.service_level_agreement = sla.name + doc.priority = doc.get("priority") or sla.default_priority prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') - handle_status_change(doc, prev_status, service_level_agreement.apply_sla_for_resolution) - update_response_and_resolution_metrics(doc, service_level_agreement.apply_sla_for_resolution) - update_agreement_status(doc, service_level_agreement.apply_sla_for_resolution) + handle_status_change(doc, prev_status, sla.apply_sla_for_resolution) + update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution) + update_agreement_status(doc, sla.apply_sla_for_resolution) -def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): - priority = get_response_and_resolution_duration(doc) - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - set_response_by_and_variance(doc, start_date_time, priority) - if apply_sla_for_resolution: - set_resolution_by_and_variance(doc, start_date_time, priority) +def handle_status_change(doc, prev_status, apply_sla_for_resolution): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + + hold_statuses = get_hold_statuses(doc.service_level_agreement) + fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) + + def is_hold_status(status): + return status in hold_statuses + + def is_fulfilled_status(status): + return status in fulfillment_statuses + + def is_open_status(status): + return status not in hold_statuses and status not in fulfillment_statuses + + def calculate_hold_hours(): + # In case issue was closed and after few days it has been opened + # The hold time should be calculated from resolution_date + + on_hold_since = doc.resolution_date or doc.on_hold_since + if on_hold_since: + current_hold_hours = time_diff_in_seconds(now_time, on_hold_since) + doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours + doc.on_hold_since = None + + if is_open_status(prev_status) and not is_open_status(doc.status): + # status changed from Open to something else + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + doc.first_responded_on = now_time + + # Open to Replied + if is_open_status(prev_status) and is_hold_status(doc.status): + # Issue is on hold -> Set on_hold_since + doc.on_hold_since = now_time + + # Replied to Open + if is_hold_status(prev_status) and is_open_status(doc.status): + # Issue was on hold -> Calculate Total Hold Time + calculate_hold_hours() + # Issue is open -> reset resolution_date + reset_expected_response_and_resolution(doc) + reset_resolution_metrics(doc) + + # Open to Closed + if is_open_status(prev_status) and is_fulfilled_status(doc.status): + # Issue is closed -> Set resolution_date + doc.resolution_date = now_time + set_resolution_time(doc) + + # Closed to Open + if is_fulfilled_status(prev_status) and is_open_status(doc.status): + # Issue was closed -> Calculate Total Hold Time from resolution_date + calculate_hold_hours() + # Issue is open -> reset resolution_date + reset_expected_response_and_resolution(doc) + reset_resolution_metrics(doc) + + # Closed to Replied + if is_fulfilled_status(prev_status) and is_hold_status(doc.status): + # Issue was closed -> Calculate Total Hold Time from resolution_date + calculate_hold_hours() + # Issue is on hold -> Set on_hold_since + doc.on_hold_since = now_time + + # Replied to Closed + if is_hold_status(prev_status) and is_fulfilled_status(doc.status): + # Issue was on hold -> Calculate Total Hold Time + calculate_hold_hours() + # Issue is closed -> Set resolution_date + if apply_sla_for_resolution: + doc.resolution_date = now_time + set_resolution_time(doc) def get_fulfillment_statuses(service_level_agreement): @@ -402,60 +468,12 @@ def get_hold_statuses(service_level_agreement): }, fields=["status"])] -def handle_status_change(doc, prev_status, apply_sla_for_resolution): - - if doc.status != "Open" and prev_status == "Open": - # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: - # status changed to something other than Open - doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if doc.status == "Open" and prev_status != "Open": - # status changed from something else to Open - reset_resolution_metrics(doc) - - handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution) - handle_hold_status(doc, prev_status) - - -def handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution): - fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) - if ( - doc.status in fulfillment_statuses - and prev_status not in fulfillment_statuses - and apply_sla_for_resolution - ): - # status changed to any fulfillment_statuses - if doc.meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.meta.has_field("resolution_time"): - doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) - set_user_resolution_time(doc) - - -def handle_hold_status(doc, prev_status): - hold_statuses = get_hold_statuses(doc.service_level_agreement) - if doc.status in hold_statuses: - # reset if status is a hold status, regardless of previous status - reset_expected_response_and_resolution(doc) - if prev_status not in hold_statuses: - # set on_hold_since status changed from any non-hold status - # for eg. doc.status changed from Open to Replied - if doc.meta.has_field("on_hold_since"): - doc.on_hold_since = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if doc.status not in hold_statuses and prev_status in hold_statuses: - # status changed to any non-hold status - # for eg. doc.status changed from Replied to Closed - if doc.meta.has_field("on_hold_since") and doc.on_hold_since: - cumulate_hold_time(doc) - doc.on_hold_since = None - - -def cumulate_hold_time(doc): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - on_hold_duration = time_diff_in_seconds(now_time, doc.on_hold_since) - doc.total_hold_time = (doc.total_hold_time or 0) + on_hold_duration +def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): + priority = get_response_and_resolution_duration(doc) + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + set_response_by(doc, start_date_time, priority) + if apply_sla_for_resolution: + set_resolution_by(doc, start_date_time, priority) def get_expected_time_for(parameter, service_level, start_date_time): @@ -526,7 +544,11 @@ def get_support_days(service_level): return support_days -def set_user_resolution_time(doc): +def set_resolution_time(doc): + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + if doc.meta.has_field("resolution_time"): + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time) + # total time taken by a user to close the issue apart from wait_time if not doc.meta.has_field("user_resolution_time"): return @@ -544,7 +566,7 @@ def set_user_resolution_time(doc): pending_time.append(wait_time) total_pending_time = sum(pending_time) - resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation) + resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time) doc.user_resolution_time = resolution_time_in_secs - total_pending_time @@ -562,11 +584,11 @@ def change_service_level_agreement_and_priority(self): def get_response_and_resolution_duration(doc): - service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) - priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) + sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) + priority = sla.get_service_level_agreement_priority(doc.priority) priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list + "support_and_resolution": sla.support_and_resolution, + "holiday_list": sla.holiday_list }) return priority @@ -585,8 +607,6 @@ def reset_service_level_agreement(doc, reason, user): }).insert(ignore_permissions=True) doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement) - doc.agreement_status = "Ongoing" doc.save() @@ -616,56 +636,37 @@ def update_hold_time(doc, status): if not parent.meta.has_field('service_level_agreement'): return - apply_sla_for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') + for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - handle_status_change(parent, 'Replied', apply_sla_for_resolution) - update_response_and_resolution_metrics(parent, apply_sla_for_resolution) - update_agreement_status(parent, apply_sla_for_resolution) + handle_status_change(parent, 'Replied', for_resolution) + update_response_and_resolution_metrics(parent, for_resolution) + update_agreement_status(parent, for_resolution) parent.save() def reset_expected_response_and_resolution(doc): update_values = {} - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None - update_values['response_by_variance'] = 0 - if doc.meta.has_field("resolution_by") and not doc.resolution_date: update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - doc.db_set(update_values) -def set_response_by_and_variance(doc, start_date_time, priority): +def set_response_by(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) - if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - if doc.meta.has_field("response_by_variance") and doc.get('first_responded_on'): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - - -def set_resolution_by_and_variance(doc, start_date_time, priority): +def set_resolution_by(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) - if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - if doc.meta.has_field("resolution_by_variance") and doc.get('resolution_date'): - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.get('resolution_date')), 2) - def get_service_level_agreement_fields(): return [ @@ -693,17 +694,11 @@ def get_service_level_agreement_fields(): "label": "Response By", "read_only": 1 }, - { - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, { "fieldname": "first_responded_on", "fieldtype": "Datetime", "label": "First Responded On", + "no_copy": 1, "read_only": 1 }, { @@ -725,11 +720,11 @@ def get_service_level_agreement_fields(): "read_only": 1 }, { - "default": "Ongoing", + "default": "First Response Due", "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", + "options": "First Response Due\nResolution Due\nFulfilled\nFailed", "read_only": 1 }, { @@ -738,13 +733,6 @@ def get_service_level_agreement_fields(): "label": "Resolution By", "read_only": 1 }, - { - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", @@ -765,43 +753,28 @@ def get_service_level_agreement_fields(): def update_agreement_status_on_custom_status(doc): # Update Agreement Fulfilled status using Custom Scripts for Custom Status - - meta = frappe.get_meta(doc.doctype) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: - # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - - if doc.meta.has_field("first_responded_on") and doc.first_responded_on: - # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) - - if doc.meta.has_field("resolution_date") and not doc.resolution_date: - # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - if doc.meta.has_field("resolution_date") and doc.resolution_date: - # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) - - if doc.meta.has_field("agreement_status"): - doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" + update_agreement_status(doc) def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("response_by_variance") and doc.meta.has_field("resolution_by_variance"): - if doc.response_by_variance < 0 or doc.resolution_by_variance < 0: - doc.agreement_status = "Failed" - else: - doc.agreement_status = "Fulfilled" - else: - if doc.meta.has_field("response_by_variance") and doc.response_by_variance < 0: - doc.agreement_status = "Failed" - else: + if not doc.first_responded_on: + doc.agreement_status = "First Response Due" + elif not doc.resolution_date: + doc.agreement_status = "Resolution Due" + elif get_datetime(doc.resolution_date) <= get_datetime(doc.resolution_by): doc.agreement_status = "Fulfilled" + else: + doc.agreement_status = "Failed" + else: + if not doc.first_responded_on: + doc.agreement_status = "First Response Due" + elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): + doc.agreement_status = "Fulfilled" + else: + doc.agreement_status = "Failed" def is_holiday(date, holidays): diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index cfbe7446c0..ce564c4dae 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -220,42 +220,6 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - def test_changing_of_variance_after_response(self): - # create lead - doctype = "Lead" - lead_sla = create_service_level_agreement( - default_service_level_agreement=1, - holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, - doctype=doctype, - sla_fulfilled_on=[{"status": "Replied"}], - apply_sla_for_resolution=0 - ) - creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2) - self.assertEqual(lead.service_level_agreement, lead_sla.name) - - # set lead as replied to set first responded on - frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) - lead.reload() - lead.status = 'Replied' - lead.save() - lead.reload() - self.assertEqual(lead.agreement_status, 'Fulfilled') - - # check response_by_variance - self.assertEqual(lead.first_responded_on, frappe.flags.current_time) - self.assertEqual(lead.response_by_variance, 1800.0) - - # make a change on the document & - # check response_by_variance is unchanged - frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30) - lead.status = 'Open' - lead.save() - lead.reload() - self.assertEqual(lead.response_by_variance, 1800.0) - def test_service_level_agreement_filters(self): doctype = "Lead" lead_sla = create_service_level_agreement( @@ -295,7 +259,8 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ return service_level_agreement def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1): + entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1, + service_level=None, start_time="10:00:00", end_time="18:00:00"): make_holiday_list() make_priorities() @@ -312,7 +277,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "doctype": "Service Level Agreement", "enabled": 1, "document_type": doctype, - "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"), + "service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"), "default_service_level_agreement": default_service_level_agreement, "condition": condition, "default_priority": "Medium", @@ -345,28 +310,28 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "support_and_resolution": [ { "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, } ] }) @@ -443,6 +408,13 @@ def create_service_level_agreements_for_issues(): create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, holiday_list="__Test Holiday List", + entity_type=None, entity=None, response_time=14400, resolution_time=21600, + service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59", + condition="doc.issue_type == 'Critical'" + ) + def make_holiday_list(): holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") if not holiday_list: From a1cedc3ea027820f185c163333b69e5669cccd93 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 14:48:24 +0530 Subject: [PATCH 096/247] fix: failing tests --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- .../test_service_level_agreement.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index e2326ac2df..62b21474f0 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -647,9 +647,9 @@ def update_hold_time(doc, status): def reset_expected_response_and_resolution(doc): update_values = {} - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): update_values['response_by'] = None - if doc.meta.has_field("resolution_by") and not doc.resolution_date: + if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'): update_values['resolution_by'] = None doc.db_set(update_values) @@ -760,7 +760,7 @@ def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: doc.agreement_status = "First Response Due" elif not doc.resolution_date: doc.agreement_status = "Resolution Due" @@ -769,7 +769,7 @@ def update_agreement_status(doc, apply_sla_for_resolution): else: doc.agreement_status = "Failed" else: - if not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: doc.agreement_status = "First Response Due" elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): doc.agreement_status = "Fulfilled" diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index ce564c4dae..b07c862c7b 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -351,7 +351,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list if sla: frappe.delete_doc("Service Level Agreement", sla, force=1) - return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) + return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True) def create_customer(): From a37c99a23d83e9c68cd0404a64dba12b2c86ce41 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 2 Dec 2021 14:51:04 +0530 Subject: [PATCH 097/247] fix: dont requeue repost immediately and clear progress (#28684) --- .../repost_item_valuation/repost_item_valuation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 965a32d92d..01cceb176b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -54,9 +54,11 @@ class RepostItemValuation(Document): @frappe.whitelist() def restart_reposting(self): - self.set_status('Queued') - frappe.enqueue(repost, timeout=1800, queue='long', - job_name='repost_sle', now=True, doc=self) + self.set_status('Queued', write=False) + self.current_index = 0 + self.distinct_item_and_warehouse = None + self.items_to_be_repost = None + self.db_update() def deduplicate_similar_repost(self): """ Deduplicate similar reposts based on item-warehouse-posting combination.""" From 6736a89b4ee9b2f9125a29378700dd9a547ac19e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 15:54:02 +0530 Subject: [PATCH 098/247] fix: failing tests --- .../service_level_agreement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 62b21474f0..662e42cc9f 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -409,7 +409,7 @@ def handle_status_change(doc, prev_status, apply_sla_for_resolution): if is_open_status(prev_status) and not is_open_status(doc.status): # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.first_responded_on = now_time # Open to Replied @@ -760,18 +760,18 @@ def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.agreement_status = "First Response Due" - elif not doc.resolution_date: + elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'): doc.agreement_status = "Resolution Due" - elif get_datetime(doc.resolution_date) <= get_datetime(doc.resolution_by): + elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" else: - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.agreement_status = "First Response Due" - elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): + elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" From 107fb43d6a0ba219181ab1cd54c0e90af1936911 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 29 Nov 2021 23:53:28 +0530 Subject: [PATCH 099/247] fix: Paid showing in AR/AP report (cherry picked from commit 5e7ce5370f6af634f7674772529cf4933114f3ef) --- .../accounts_receivable.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 88bcdad710..353f9087f1 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -109,7 +109,11 @@ class ReceivablePayableReport(object): invoiced = 0.0, paid = 0.0, credit_note = 0.0, - outstanding = 0.0 + outstanding = 0.0, + invoiced_in_account_currency = 0.0, + paid_in_account_currency = 0.0, + credit_note_in_account_currency = 0.0, + outstanding_in_account_currency = 0.0 ) self.get_invoices(gle) @@ -150,21 +154,28 @@ class ReceivablePayableReport(object): # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) + gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) + if gle_balance > 0: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency else: # invoice row.invoiced += gle_balance + row.invoiced_in_account_currency += gle_balance_in_account_currency else: # payment or credit note for receivables if self.is_invoice(gle): # stand alone debit / credit note row.credit_note -= gle_balance + row.credit_note_in_account_currency -= gle_balance_in_account_currency else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency + if gle.cost_center: row.cost_center = str(gle.cost_center) @@ -216,8 +227,13 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) + row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ + row.credit_note_in_account_currency, self.currency_precision) + row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: + + if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ + (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -583,12 +599,14 @@ class ReceivablePayableReport(object): else: select_fields = "debit, credit" + doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" + remarks = ", remarks" if self.filters.get("show_remarks") else "" self.gl_entries = frappe.db.sql(""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, {0} {remarks} + against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} from `tabGL Entry` where @@ -596,8 +614,8 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {1} {2} {3}""" - .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""" + .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -718,6 +736,13 @@ class ReceivablePayableReport(object): # get the balance of the GL (debit - credit) or reverse balance based on report type return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) + def get_gle_balance_in_account_currency(self, gle): + # get the balance of the GL (debit - credit) or reverse balance based on report type + return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + + def get_reverse_balance_in_account_currency(self, gle): + return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') From f12be3001d9f6e2088b7e06680398c62de12fe14 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 30 Nov 2021 20:34:53 +0530 Subject: [PATCH 100/247] fix: Taxes and Charges template not getting copied from Purchase Order/Receipt to Invoice (cherry picked from commit 6a75e8d283bb76214af832bb0a29c20eefae328c) --- erpnext/accounts/party.py | 10 ++++++---- erpnext/public/js/utils/party.js | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e904768b38..6b4b43d30b 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -68,10 +68,12 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - if not party_details.get("taxes_and_charges"): - party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, - billing_address=party_address, shipping_address=shipping_address) + tax_template = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) + + if tax_template: + party_details['taxes_and_charges'] = tax_template if cint(fetch_payment_terms_template): party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a492b32a9f..c26a154046 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -77,6 +77,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); + args.taxes_and_charges = frm.doc.taxes_and_charges; } } if (!args || !args.party) return; From 35e2bd89c0505e1c8e51042dc47e7bba6fb4fb43 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 2 Dec 2021 17:17:56 +0530 Subject: [PATCH 101/247] fix: India utils code cleanup (cherry picked from commit 56c626adbfbd04e7e9063f6500ec380f7bfb3da4) --- erpnext/public/js/utils/party.js | 1 - erpnext/regional/india/utils.py | 25 +++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index c26a154046..a492b32a9f 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -77,7 +77,6 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); - args.taxes_and_charges = frm.doc.taxes_and_charges; } } if (!args || !args.party) return; diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9746fdef84..626ab7a909 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -206,28 +206,18 @@ def get_regional_address_details(party_details, doctype, company): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - get_tax_template_based_on_category(master_doctype, company, party_details) - - if party_details.get('taxes_and_charges'): - return party_details - - if not party_details.company_gstin: - return party_details + tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) - if party_details.get('taxes_and_charges'): - return party_details - - if not party_details.supplier_gstin: - return party_details + if tax_template_by_category: + party_details.get['taxes_and_charges'] = tax_template_by_category + return if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return party_details - if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): @@ -237,6 +227,7 @@ def get_regional_address_details(party_details, doctype, company): if not default_tax: return party_details + party_details["taxes_and_charges"] = default_tax party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) @@ -268,9 +259,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details): default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, 'name') - if default_tax: - party_details["taxes_and_charges"] = default_tax - party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + return default_tax def get_tax_template(master_doctype, company, is_inter_state, state_code): tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], From 7314aee3943ef76a2e16a0f56e81df8089bc04dd Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 3 Dec 2021 11:29:51 +0530 Subject: [PATCH 102/247] fix: qrcode image name for invoices with special chars (#28699) --- erpnext/regional/saudi_arabia/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 1051315cbe..ba55efc6af 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,6 +1,7 @@ import io import os from base64 import b64encode +from urllib.parse import quote import frappe from frappe import _ @@ -101,8 +102,9 @@ def create_qr_code(doc, method): url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) + urlencoded_name = quote(doc.name) # making file - filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__") + filename = f"QR-CODE-{urlencoded_name}.png".replace(os.path.sep, "__") _file = frappe.get_doc({ "doctype": "File", "file_name": filename, From 1e550d3e46c633e24ba6f49f2883a506aa96b471 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 3 Dec 2021 11:36:53 +0530 Subject: [PATCH 103/247] fix: failing tests --- erpnext/support/doctype/issue/test_issue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index da9953df55..d566f33f24 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -430,9 +430,11 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue -def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None, do_not_insert=False): - if not frappe.db.exists('Issue Type', issue_type): - frappe.get_doc(dict(doctype='Issue Type', name=issue_type)).insert() +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): + if issue_type and not frappe.db.exists('Issue Type', issue_type): + doc = frappe.new_doc('Issue Type') + doc.name = issue_type + doc.insert() issue = frappe.get_doc({ "doctype": "Issue", From 0b1808e1eeac31a292da88bc16e9a9ce7b812bc1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 3 Dec 2021 11:46:14 +0530 Subject: [PATCH 104/247] fix(Non Profit): fetch memberships for 80G certificate by from date only (#28700) --- .../tax_exemption_80g_certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py index 9a72410f67..0f0897841b 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document): memberships = frappe.db.get_all('Membership', { 'member': self.member, 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], - 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'membership_status': ('!=', 'Cancelled') }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') From f2ffddf059b972a547a74e0dc0c19099190ef3e1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 3 Dec 2021 14:32:52 +0530 Subject: [PATCH 105/247] fix: Invocie amount in KSA E Invoice QR Code --- erpnext/regional/saudi_arabia/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index ba55efc6af..e9fcce81cc 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -78,7 +78,7 @@ def create_qr_code(doc, method): tlv_array.append(''.join([tag, length, value])) # Invoice Amount - invoice_amount = str(doc.total) + invoice_amount = str(doc.grand_total) tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() value = invoice_amount.encode('utf-8').hex() From 97060c45e96b191c18041822895abfac2178f84a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 11:50:38 +0530 Subject: [PATCH 106/247] refactor: replace misleading variable name --- erpnext/stock/stock_ledger.py | 4 ++-- erpnext/stock/utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9d409827db..4011d10807 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -803,9 +803,9 @@ class update_entries_after(object): def update_bin(self): # update bin for each warehouse for warehouse, data in self.data.items(): - bin_record = get_or_make_bin(self.item_code, warehouse) + bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_record, { + frappe.db.set_value('Bin', bin_name, { "valuation_rate": data.valuation_rate, "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8031c58b81..bf014c1340 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -187,7 +187,7 @@ def get_bin(item_code, warehouse): bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code, warehouse) -> str: +def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: @@ -206,8 +206,8 @@ def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: - bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) - update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher) else: frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) From cef84c25a74db9a05d2ce989ed761e9f491a1e67 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 12:18:59 +0530 Subject: [PATCH 107/247] refactor: simplify the way SLEs are submitted --- erpnext/stock/doctype/bin/bin.py | 32 ++++--------------------- erpnext/stock/stock_ledger.py | 40 +++++++++++++++++++++++++++----- erpnext/stock/utils.py | 1 + 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index da9c66d996..a33134b491 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe.query_builder import Case from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import flt, nowdate +from frappe.utils import flt class Bin(Document): @@ -127,33 +127,11 @@ def on_doctype_update(): def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): - '''Called from erpnext.stock.utils.update_bin''' + """WARNING: This function is deprecated. Inline this function instead of using it.""" + from erpnext.stock.stock_ledger import repost_current_voucher + update_qty(bin_name, args) - - if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle - - if not args.get("posting_date"): - args["posting_date"] = nowdate() - - if args.get("is_cancelled") and via_landed_cost_voucher: - return - - # Reposts only current voucher SL Entries - # Updates valuation rate, stock value, stock queue for current transaction - update_entries_after({ - "item_code": args.get('item_code'), - "warehouse": args.get('warehouse'), - "posting_date": args.get("posting_date"), - "posting_time": args.get("posting_time"), - "voucher_type": args.get("voucher_type"), - "voucher_no": args.get("voucher_no"), - "sle_id": args.get('name'), - "creation": args.get('creation') - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - - # update qty in future sle and Validate negative qty - update_qty_in_future_sle(args, allow_negative_stock) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) def get_bin_details(bin_name): return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4011d10807..d78632a0f3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,9 +7,10 @@ import json import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext +from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -17,19 +18,15 @@ from erpnext.stock.utils import ( ) -# future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass _exceptions = frappe.local('stockledger_exceptions') -# _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: - from erpnext.stock.utils import update_bin - cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) @@ -64,7 +61,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # preserve previous_qty_after_transaction for qty reposting args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") - update_bin(args, allow_negative_stock, via_landed_cost_voucher) + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + if is_stock_item: + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_bin_qty(bin_name, args) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) + else: + frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + +def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": + if not args.get("posting_date"): + args["posting_date"] = nowdate() + + if args.get("is_cancelled") and via_landed_cost_voucher: + return + + # Reposts only current voucher SL Entries + # Updates valuation rate, stock value, stock queue for current transaction + update_entries_after({ + "item_code": args.get('item_code'), + "warehouse": args.get('warehouse'), + "posting_date": args.get("posting_date"), + "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), + "voucher_no": args.get("voucher_no"), + "sle_id": args.get('name'), + "creation": args.get('creation') + }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + # update qty in future sle and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def get_args_for_future_sle(row): return frappe._dict({ diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index bf014c1340..72d8098d44 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -203,6 +203,7 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str: return bin_record def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): + """WARNING: This function is deprecated. Inline this function instead of using it.""" from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: From 7f3e6d149aecf99e102efdc97fc632a78d6a1a95 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 3 Dec 2021 14:12:00 +0530 Subject: [PATCH 108/247] fix: incorrect outgoing rates when material_consumption enabled --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d31e65a4cc..a38dfa5062 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -545,7 +545,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): + if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) From 7511a9ed315852363ce0eea04b7d9ae34f1a89cb Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 3 Dec 2021 16:13:44 +0530 Subject: [PATCH 109/247] fix(ksa): qrcode for invoices with special chars (#28715) --- erpnext/regional/saudi_arabia/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index e9fcce81cc..7d00d8b392 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,7 +1,6 @@ import io import os from base64 import b64encode -from urllib.parse import quote import frappe from frappe import _ @@ -102,9 +101,10 @@ def create_qr_code(doc, method): url = qr_create(base64_string, error='L') url.png(qr_image, scale=2, quiet_zone=1) - urlencoded_name = quote(doc.name) + name = frappe.generate_hash(doc.name, 5) + # making file - filename = f"QR-CODE-{urlencoded_name}.png".replace(os.path.sep, "__") + filename = f"QRCode-{name}.png".replace(os.path.sep, "__") _file = frappe.get_doc({ "doctype": "File", "file_name": filename, From defa01edac2ebb52047f4fe060f23a9e81db0d3c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 3 Dec 2021 16:22:10 +0530 Subject: [PATCH 110/247] fix: undo removing of resolution_time fields --- erpnext/support/doctype/issue/issue.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 1da22fd58f..38b5395adf 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -42,6 +42,8 @@ "opening_date", "opening_time", "resolution_date", + "resolution_time", + "user_resolution_time", "additional_info", "lead", "contact", @@ -345,16 +347,16 @@ "read_only": 1 }, { - "fieldname": "resolution_time", - "fieldtype": "Duration", - "label": "Resolution Time", - "read_only": 1 + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 }, { - "fieldname": "user_resolution_time", - "fieldtype": "Duration", - "label": "User Resolution Time", - "read_only": 1 + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 }, { "fieldname": "on_hold_since", From 35346de1628c0be87e24bb730be70eef3422d7fe Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 3 Dec 2021 17:21:49 +0530 Subject: [PATCH 111/247] test: added tests for manufacture stock entry when material_consumption is enabled --- .../doctype/stock_entry/test_stock_entry.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 067946785a..5ef07705d8 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -41,6 +41,7 @@ def get_sle(**args): class TestStockEntry(unittest.TestCase): def tearDown(self): frappe.set_user("Administrator") + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") def test_fifo(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -582,6 +583,65 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) + def test_work_order_manufacture_with_material_consumption(self): + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as _make_stock_entry, + ) + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") + + bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", + "is_default": 1, "docstatus": 1}) + + work_order = frappe.new_doc("Work Order") + work_order.update({ + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC" + }) + work_order.insert() + work_order.submit() + + make_stock_entry(item_code="_Test Item", + target="Stores - _TC", qty=10, basic_rate=5000.0) + make_stock_entry(item_code="_Test Item Home Desktop 100", + target="Stores - _TC", qty=10, basic_rate=1000.0) + + + s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) + for d in s.get("items"): + d.s_warehouse = "Stores - _TC" + s.insert() + s.submit() + + # When Stock Entry has RM and FG + s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1)) + s.save() + rm_cost = 0 + for d in s.get('items'): + if d.s_warehouse: + rm_cost += d.amount + fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount + self.assertEqual(fg_cost, + flt(rm_cost - scrap_cost, 2)) + + # When Stock Entry has only FG + Scrap + s.items.pop(0) + s.items.pop(0) + s.submit() + + rm_cost = 0 + for d in s.get('items'): + if d.s_warehouse: + rm_cost += d.amount + self.assertEqual(rm_cost, 0) + expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) + fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", From 72dbc3d6b8e10fc6ee9f7cd8132da90fe9cdb3bb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 15:19:12 +0530 Subject: [PATCH 112/247] fix!: dont allow renaming warehouse primary key --- .../stock/doctype/warehouse/test_warehouse.py | 59 ------------------- .../stock/doctype/warehouse/warehouse.json | 3 +- erpnext/stock/doctype/warehouse/warehouse.py | 52 ---------------- 3 files changed, 1 insertion(+), 113 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index ca92936a1d..26db2642e4 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase): self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) - def test_warehouse_renaming(self): - create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory") - account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1") - self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) - - # Rename with abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Rename without abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1") - - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Another rename with multiple dashes - if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"): - frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company") - - def test_warehouse_merging(self): - company = "_Test Company with perpetual inventory" - create_warehouse("Test Warehouse for Merging 1", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - create_warehouse("Test Warehouse for Merging 2", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1", - qty=1, rate=100, company=company) - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1", - qty=1, rate=100, company=company) - - existing_bin_qty = ( - cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty")) - + cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")) - ) - - frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1", - "Test Warehouse for Merging 2 - TCP1", merge=True) - - self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1")) - - bin_qty = frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty") - - self.assertEqual(bin_qty, existing_bin_qty) - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Merging 2 - TCP1"})) - def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 9b9093261c..05076b51a3 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_rename": 1, "creation": "2013-03-07 18:50:32", "description": "A logical Warehouse against which stock entries are made.", "doctype": "DocType", @@ -245,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-04-09 19:54:56.263965", + "modified": "2021-12-03 04:40:06.414630", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index b9dbc38880..9cfad86f14 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint, flt from frappe.utils.nestedset import NestedSet -import erpnext from erpnext.stock import get_warehouse_account @@ -68,57 +67,6 @@ class Warehouse(NestedSet): return frappe.db.sql("""select name from `tabWarehouse` where parent_warehouse = %s limit 1""", self.name) - def before_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).before_rename(old_name, new_name, merge) - - # Add company abbr if not provided - new_warehouse = erpnext.encode_company_abbr(new_name, self.company) - - if merge: - if not frappe.db.exists("Warehouse", new_warehouse): - frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse)) - - if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"): - frappe.throw(_("Both Warehouse must belong to same Company")) - - return new_warehouse - - def after_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).after_rename(old_name, new_name, merge) - - new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name) - self.db_set("warehouse_name", new_warehouse_name) - - if merge: - self.recalculate_bin_qty(new_name) - - def get_new_warehouse_name_without_abbr(self, name): - company_abbr = frappe.get_cached_value('Company', self.company, "abbr") - parts = name.rsplit(" - ", 1) - - if parts[-1].lower() == company_abbr.lower(): - name = parts[0] - - return name - - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - - repost_stock_for_items = frappe.db.sql_list("""select distinct item_code - from tabBin where warehouse=%s""", new_name) - - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name) - - for item_code in repost_stock_for_items: - repost_stock(item_code, new_name) - - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 - def convert_to_group_or_ledger(self): if self.is_group: self.convert_to_ledger() From 5caf411be3ffcb5638cf2d3a3cedca233ec9f317 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 15:47:16 +0530 Subject: [PATCH 113/247] fix: remove autocommit from item rename --- erpnext/stock/doctype/item/item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c9b8a3734e..decf522d2f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -724,7 +724,6 @@ class Item(WebsiteGenerator): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -738,7 +737,6 @@ class Item(WebsiteGenerator): repost_stock(new_name, warehouse) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 @frappe.whitelist() def copy_specification_from_item_group(self): From ba5a7ffd60d1c4ae336878f8faf1ae482b5eee5b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 3 Dec 2021 18:58:29 +0530 Subject: [PATCH 114/247] fix: weird item sorting by `idx` --- erpnext/stock/doctype/item/item.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 5469a9fc25..4f4e69105a 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1035,7 +1035,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-30 02:33:06.572442", + "modified": "2021-12-03 08:32:03.869294", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1103,7 +1103,7 @@ "search_fields": "item_name,description,item_group,customer_code", "show_name_in_global_search": 1, "show_preview_popup": 1, - "sort_field": "idx desc,modified desc", + "sort_field": "modified", "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 From 67a001d87602cb589b5edb606c03be2d2130d594 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 4 Dec 2021 19:19:03 +0530 Subject: [PATCH 115/247] fix: Better Error logging fordeferred revenue/expense booking --- erpnext/accounts/deferred_revenue.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 5df8269f75..76c833a24e 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -375,11 +375,12 @@ def make_gl_entries(doc, credit_account, debit_account, against, except Exception as e: if frappe.flags.in_test: raise e + traceback = frappe.get_traceback() + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) else: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) - + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True def send_mail(deferred_process): @@ -449,7 +450,7 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, except Exception: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True From 0ba4fcee2a6091922b452b3ca8ad9b72606b814f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 4 Dec 2021 19:25:44 +0530 Subject: [PATCH 116/247] fix: Commit joural entries --- erpnext/accounts/deferred_revenue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 76c833a24e..1afed127b2 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -447,6 +447,8 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, if submit: journal_entry.submit() + + frappe.db.commit() except Exception: frappe.db.rollback() traceback = frappe.get_traceback() From 3c64e201cc36f309b16de1c83c1e4d27aa6a2826 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 4 Dec 2021 20:05:37 +0530 Subject: [PATCH 117/247] fix: Log error before throwing exception --- erpnext/accounts/deferred_revenue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 1afed127b2..22c81ddd46 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -374,9 +374,9 @@ def make_gl_entries(doc, credit_account, debit_account, against, frappe.db.commit() except Exception as e: if frappe.flags.in_test: - raise e traceback = frappe.get_traceback() frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) + raise e else: frappe.db.rollback() traceback = frappe.get_traceback() From 0ef42d10002f9e2a69a8e29e851bf4f2e087d422 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 12:44:20 +0530 Subject: [PATCH 118/247] fix(patch): create only component type field instead of running the whole setup (#28734) --- .../patches/v13_0/check_is_income_tax_component.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index b3ef5af100..5e1df14d4e 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -3,9 +3,9 @@ import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field import erpnext -from erpnext.regional.india.setup import setup def execute(): @@ -30,7 +30,14 @@ def execute(): frappe.reload_doc('Regional', 'Report', report) if erpnext.get_region() == "India": - setup(patch=True) + create_custom_field('Salary Component', + dict(fieldname='component_type', + label='Component Type', + fieldtype='Select', + insert_after='description', + options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax', + depends_on='eval:doc.type == "Deduction"') + ) if frappe.db.exists("Salary Component", "Income Tax"): frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1) From 36a2d8ee0df17ce2f23a2afe5f63b406aaeb2ace Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 14:42:27 +0530 Subject: [PATCH 119/247] feat: Exit Interview --- erpnext/hr/doctype/exit_interview/__init__.py | 0 .../doctype/exit_interview/exit_interview.js | 20 ++ .../exit_interview/exit_interview.json | 190 ++++++++++++++++++ .../doctype/exit_interview/exit_interview.py | 17 ++ .../exit_interview/test_exit_interview.py | 8 + 5 files changed, 235 insertions(+) create mode 100644 erpnext/hr/doctype/exit_interview/__init__.py create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.js create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.json create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.py create mode 100644 erpnext/hr/doctype/exit_interview/test_exit_interview.py diff --git a/erpnext/hr/doctype/exit_interview/__init__.py b/erpnext/hr/doctype/exit_interview/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js new file mode 100644 index 0000000000..d8ce7018d5 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -0,0 +1,20 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exit Interview', { + refresh: function(frm) { + + }, + + employee: function(frm) { + frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date').then(({ relieving_date }) => { + if (!relieving_date) { + frappe.throw({ + message: __('Please set the relieving date for employee {0}', + ['' + frm.doc.employee + '']), + title: __('Relieving Date Missing') + }); + } + }); + } +}); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json new file mode 100644 index 0000000000..696820ee49 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -0,0 +1,190 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2021-12-05 13:56:36.241690", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "column_break_5", + "company", + "date", + "employee_details_section", + "department", + "designation", + "reports_to", + "column_break_9", + "date_of_joining", + "relieving_date", + "exit_questionnaire_section", + "ref_doctype", + "column_break_10", + "reference_document_name", + "interview_summary_section", + "interviewers", + "text_editor_12" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fetch_from": "employee.relieving_date", + "fieldname": "relieving_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Relieving Date", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "exit_questionnaire_section", + "fieldtype": "Section Break", + "label": "Exit Questionnaire" + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_document_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Document Name", + "options": "ref_doctype" + }, + { + "fieldname": "interview_summary_section", + "fieldtype": "Section Break", + "label": "Interview Details" + }, + { + "fieldname": "text_editor_12", + "fieldtype": "Text Editor", + "label": "Interview Summary" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "interviewers", + "fieldtype": "Table MultiSelect", + "label": "Interviewers", + "options": "Interviewer", + "reqd": 1 + }, + { + "fetch_from": "employee.date_of_joining", + "fieldname": "date_of_joining", + "fieldtype": "Date", + "label": "Date of Joining", + "read_only": 1 + }, + { + "fieldname": "reports_to", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "employee_details_section", + "fieldtype": "Section Break", + "label": "Employee Details" + }, + { + "fetch_from": "employee.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "HR-EXIT-INT-" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-12-05 14:25:40.416023", + "modified_by": "Administrator", + "module": "HR", + "name": "Exit Interview", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py new file mode 100644 index 0000000000..677141b697 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form + +class ExitInterview(Document): + def validate(self): + self.validate_relieving_date() + + def validate_relieving_date(self): + if not frappe.db.get_value('Employee', self.employee, 'relieving_date'): + frappe.throw(_('Please set the relieving date for employee {0}').format( + get_link_to_form('Employee', self.employee)), + title=_('Relieving Date Missing')) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py new file mode 100644 index 0000000000..daf3d66290 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestExitInterview(unittest.TestCase): + pass From 7412accf6d4d6b932b24e40828a886b12111c1b4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:02:19 +0530 Subject: [PATCH 120/247] feat: sending Exit Questionnaire --- .../doctype/exit_interview/exit_interview.js | 38 +++++++++++++++++-- .../exit_interview/exit_interview.json | 21 +++++++++- .../doctype/exit_interview/exit_interview.py | 36 ++++++++++++++++++ .../hr/doctype/hr_settings/hr_settings.json | 29 +++++++++++++- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index d8ce7018d5..31c961063f 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -3,12 +3,16 @@ frappe.ui.form.on('Exit Interview', { refresh: function(frm) { - + if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent) { + frm.add_custom_button(__('Send Exit Questionnaire'), function () { + frm.trigger('send_exit_questionnaire'); + }); + } }, employee: function(frm) { - frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date').then(({ relieving_date }) => { - if (!relieving_date) { + frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => { + if (!message.relieving_date) { frappe.throw({ message: __('Please set the relieving date for employee {0}', ['' + frm.doc.employee + '']), @@ -16,5 +20,33 @@ frappe.ui.form.on('Exit Interview', { }); } }); + }, + + send_exit_questionnaire: function(frm) { + frappe.db.get_value('HR Settings', 'HR Settings', + ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => { + if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) { + frappe.throw({ + message: __('Please set {0} and {1} in {2}.', + ['Exit Questionnaire Web Form'.bold(), + 'Notification Template'.bold(), + 'HR Settings'] + ), + title: __('Settings Missing') + }); + } else { + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + args: { + 'exit_interview': frm.doc.name + }, + callback: function(r) { + if (!r.exc) { + frm.refresh_field('questionnaire_email_sent'); + } + } + }); + } + }); } }); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 696820ee49..0712b3d2a5 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -5,11 +5,13 @@ "creation": "2021-12-05 13:56:36.241690", "doctype": "DocType", "editable_grid": 1, + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "naming_series", "employee", "employee_name", + "email", "column_break_5", "company", "date", @@ -22,6 +24,7 @@ "relieving_date", "exit_questionnaire_section", "ref_doctype", + "questionnaire_email_sent", "column_break_10", "reference_document_name", "interview_summary_section", @@ -130,6 +133,7 @@ "read_only": 1 }, { + "fetch_from": "employee.reports_to", "fieldname": "reports_to", "fieldtype": "Link", "in_standard_filter": 1, @@ -159,11 +163,25 @@ "fieldtype": "Select", "label": "Naming Series", "options": "HR-EXIT-INT-" + }, + { + "default": "0", + "fieldname": "questionnaire_email_sent", + "fieldtype": "Check", + "label": "Questionnaire Email Sent", + "read_only": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email ID", + "options": "Email", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-05 14:25:40.416023", + "modified": "2021-12-05 16:50:05.933394", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", @@ -183,6 +201,7 @@ "write": 1 } ], + "sender_field": "email", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 677141b697..a59146a898 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -6,12 +6,48 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import get_link_to_form +from erpnext.hr.doctype.employee.employee import get_employee_email + + class ExitInterview(Document): def validate(self): self.validate_relieving_date() + self.set_employee_email() def validate_relieving_date(self): if not frappe.db.get_value('Employee', self.employee, 'relieving_date'): frappe.throw(_('Please set the relieving date for employee {0}').format( get_link_to_form('Employee', self.employee)), title=_('Relieving Date Missing')) + + def set_employee_email(self): + employee = frappe.get_doc('Employee', self.employee) + self.email = get_employee_email(employee) + + +@frappe.whitelist() +def send_exit_questionnaire(exit_interview): + exit_interview = frappe.get_doc('Exit Interview', exit_interview) + context = exit_interview.as_dict() + + employee = frappe.get_doc('Employee', exit_interview.employee) + context.update(employee.as_dict()) + + email = get_employee_email(employee) + template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') + template = frappe.get_doc('Email Template', template_name) + + if email: + frappe.sendmail( + recipients=email, + subject=template.subject, + message=frappe.render_template(template.response, context), + reference_doctype=exit_interview.doctype, + reference_name=exit_interview.name + ) + frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email), + title='Success', indicator='green') + exit_interview.db_set('questionnaire_email_sent', True) + exit_interview.notify_update() + else: + frappe.msgprint(_('Email IDs for employee not found.')) \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 5148435c13..f9a3e05fc3 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -36,7 +36,11 @@ "remind_before", "column_break_4", "send_interview_feedback_reminder", - "feedback_reminder_notification_template" + "feedback_reminder_notification_template", + "employee_exit_section", + "exit_questionnaire_web_form", + "column_break_34", + "exit_questionnaire_notification_template" ], "fields": [ { @@ -226,13 +230,34 @@ "fieldname": "check_vacancies", "fieldtype": "Check", "label": "Check Vacancies On Job Offer Creation" + }, + { + "fieldname": "employee_exit_section", + "fieldtype": "Section Break", + "label": "Employee Exit Settings" + }, + { + "fieldname": "exit_questionnaire_web_form", + "fieldtype": "Link", + "label": "Exit Questionnaire Web Form", + "options": "Web Form" + }, + { + "fieldname": "exit_questionnaire_notification_template", + "fieldtype": "Link", + "label": "Exit Questionnaire Notification Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-10-01 23:46:11.098236", + "modified": "2021-12-05 14:48:10.884253", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", From 235b707417316dc787fe4fb154f6db81896a9c13 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:06:29 +0530 Subject: [PATCH 121/247] feat: add default Exit Questionnaire email template --- ...t_questionnaire_notification_template.html | 16 +++++++++++ erpnext/patches.txt | 1 + ...xit_questionnaire_notification_template.py | 27 +++++++++++++++++++ .../setup_wizard/operations/defaults_setup.py | 2 ++ .../operations/install_fixtures.py | 5 ++++ 5 files changed, 51 insertions(+) create mode 100644 erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html create mode 100644 erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py diff --git a/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html new file mode 100644 index 0000000000..0317b1a102 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html @@ -0,0 +1,16 @@ +

Exit Questionnaire

+
+ +

+ Dear {{ employee_name }}, +

+ + Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us. + Request you to take out a few minutes to fill up this Exit Questionnaire. + + {% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %} + {% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %} + +

+ {{ _('Submit Now') }} +

diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce25..717965a569 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template \ No newline at end of file diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py new file mode 100644 index 0000000000..8b1752b2c7 --- /dev/null +++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py @@ -0,0 +1,27 @@ +import os + +import frappe +from frappe import _ + + +def execute(): + frappe.reload_doc("email", "doctype", "email_template") + frappe.reload_doc("hr", "doctype", "hr_settings") + + template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification")) + if not template: + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html")) + + template = frappe.get_doc({ + "doctype": "Email Template", + "name": _("Exit Questionnaire Notification"), + "response": response, + "subject": _("Exit Questionnaire Notification"), + "owner": frappe.session.user, + }).insert(ignore_permissions=True) + template = template.name + + hr_settings = frappe.get_doc("HR Settings") + hr_settings.exit_questionnaire_notification_template = template + hr_settings.save() diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index e4b1fa26ae..ca1f57eb1d 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -68,6 +68,8 @@ def set_default_settings(args): hr_settings.send_interview_feedback_reminder = 1 hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") + + hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification") hr_settings.save() def set_no_copy_fields_in_variant_settings(): diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 503aeacd01..323a7940b8 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -278,6 +278,11 @@ def install(country=None): records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response, 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}] + response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + + records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response, + 'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}] + base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) From 1347187a30b1c485d5d602bd5aca78c3efa11563 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:20:39 +0530 Subject: [PATCH 122/247] feat: track status and final decision (Retained/Exit Confirmed) --- .../exit_interview/exit_interview.json | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 0712b3d2a5..bed0e776f1 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -14,6 +14,7 @@ "email", "column_break_5", "company", + "status", "date", "employee_details_section", "department", @@ -29,7 +30,9 @@ "reference_document_name", "interview_summary_section", "interviewers", - "text_editor_12" + "text_editor_12", + "employee_status_section", + "employee_status" ], "fields": [ { @@ -72,7 +75,6 @@ { "fieldname": "company", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Company", "options": "Company", @@ -84,6 +86,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Date", + "mandatory_depends_on": "eval:doc.status==='Scheduled';", "reqd": 1 }, { @@ -168,6 +171,7 @@ "default": "0", "fieldname": "questionnaire_email_sent", "fieldtype": "Check", + "in_standard_filter": 1, "label": "Questionnaire Email Sent", "read_only": 1 }, @@ -177,11 +181,33 @@ "label": "Email ID", "options": "Email", "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Pending\nScheduled\nCompleted", + "reqd": 1 + }, + { + "fieldname": "employee_status_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "employee_status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Final Decision", + "mandatory_depends_on": "eval:doc.status==='Completed';", + "options": "\nEmployee Retained\nExit Confirmed" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-05 16:50:05.933394", + "modified": "2021-12-05 17:17:20.033950", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", From 09fdfed1633110db19ffb978263c57b143349195 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 18:28:23 +0530 Subject: [PATCH 123/247] fix: make Exit Interview submittable --- .../doctype/exit_interview/exit_interview.json | 17 ++++++++++++++--- .../hr/doctype/exit_interview/exit_interview.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index bed0e776f1..d4d4aaceea 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -32,7 +32,8 @@ "interviewers", "text_editor_12", "employee_status_section", - "employee_status" + "employee_status", + "amended_from" ], "fields": [ { @@ -188,7 +189,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Pending\nScheduled\nCompleted", + "options": "Pending\nScheduled\nCompleted\nCancelled", "reqd": 1 }, { @@ -203,11 +204,21 @@ "label": "Final Decision", "mandatory_depends_on": "eval:doc.status==='Completed';", "options": "\nEmployee Retained\nExit Confirmed" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Exit Interview", + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, + "is_submittable": 1, "links": [], - "modified": "2021-12-05 17:17:20.033950", + "modified": "2021-12-05 17:49:44.839277", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index a59146a898..878380b9ec 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -12,6 +12,7 @@ from erpnext.hr.doctype.employee.employee import get_employee_email class ExitInterview(Document): def validate(self): self.validate_relieving_date() + self.validate_duplicate_interview() self.set_employee_email() def validate_relieving_date(self): @@ -20,10 +21,21 @@ class ExitInterview(Document): get_link_to_form('Employee', self.employee)), title=_('Relieving Date Missing')) + def validate_duplicate_interview(self): + doc = frappe.db.exists('Exit Interview', {'employee': self.employee, 'name': ('!=', self.name)}) + if doc: + frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( + get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), + title=_('Duplicate Document')) + def set_employee_email(self): employee = frappe.get_doc('Employee', self.employee) self.email = get_employee_email(employee) + def on_submit(self): + if self.status != 'Completed': + frappe.throw(_('Only Completed documents can be submitted')) + @frappe.whitelist() def send_exit_questionnaire(exit_interview): From e30187f2469bb82fe1ed98abaa4f126053ba9371 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 19:32:33 +0530 Subject: [PATCH 124/247] feat: bulk questionnaire sending --- .../doctype/exit_interview/exit_interview.js | 32 ++----- .../exit_interview/exit_interview.json | 6 +- .../doctype/exit_interview/exit_interview.py | 90 ++++++++++++++----- .../exit_interview/exit_interview_list.js | 27 ++++++ 4 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview_list.js diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index 31c961063f..849e8542d2 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -23,29 +23,15 @@ frappe.ui.form.on('Exit Interview', { }, send_exit_questionnaire: function(frm) { - frappe.db.get_value('HR Settings', 'HR Settings', - ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => { - if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) { - frappe.throw({ - message: __('Please set {0} and {1} in {2}.', - ['Exit Questionnaire Web Form'.bold(), - 'Notification Template'.bold(), - 'HR Settings'] - ), - title: __('Settings Missing') - }); - } else { - frappe.call({ - method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', - args: { - 'exit_interview': frm.doc.name - }, - callback: function(r) { - if (!r.exc) { - frm.refresh_field('questionnaire_email_sent'); - } - } - }); + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + args: { + 'interviews': [frm.doc] + }, + callback: function(r) { + if (!r.exc) { + frm.refresh_field('questionnaire_email_sent'); + } } }); } diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index d4d4aaceea..4b396402db 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -87,8 +87,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Date", - "mandatory_depends_on": "eval:doc.status==='Scheduled';", - "reqd": 1 + "mandatory_depends_on": "eval:doc.status==='Scheduled';" }, { "fieldname": "exit_questionnaire_section", @@ -174,6 +173,7 @@ "fieldtype": "Check", "in_standard_filter": 1, "label": "Questionnaire Email Sent", + "no_copy": 1, "read_only": 1 }, { @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-05 17:49:44.839277", + "modified": "2021-12-05 18:56:34.856854", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 878380b9ec..b2ada4d83c 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -38,28 +38,76 @@ class ExitInterview(Document): @frappe.whitelist() -def send_exit_questionnaire(exit_interview): - exit_interview = frappe.get_doc('Exit Interview', exit_interview) - context = exit_interview.as_dict() +def send_exit_questionnaire(interviews): + interviews = get_interviews(interviews) + validate_questionnaire_settings() - employee = frappe.get_doc('Employee', exit_interview.employee) - context.update(employee.as_dict()) + email_success = [] + email_failure = [] - email = get_employee_email(employee) - template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') - template = frappe.get_doc('Email Template', template_name) + for exit_interview in interviews: + interview = frappe.get_doc('Exit Interview', exit_interview.get('name')) + if interview.get('questionnaire_email_sent'): + continue - if email: - frappe.sendmail( - recipients=email, - subject=template.subject, - message=frappe.render_template(template.response, context), - reference_doctype=exit_interview.doctype, - reference_name=exit_interview.name + employee = frappe.get_doc('Employee', interview.employee) + email = get_employee_email(employee) + + context = interview.as_dict() + context.update(employee.as_dict()) + template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') + template = frappe.get_doc('Email Template', template_name) + + if email: + frappe.sendmail( + recipients=email, + subject=template.subject, + message=frappe.render_template(template.response, context), + reference_doctype=interview.doctype, + reference_name=interview.name + ) + interview.db_set('questionnaire_email_sent', True) + interview.notify_update() + email_success.append(email) + else: + email_failure.append(get_link_to_form('Employee', employee.name)) + + show_email_summary(email_success, email_failure) + + +def get_interviews(interviews): + import json + + if isinstance(interviews, str): + interviews = json.loads(interviews) + + if not len(interviews): + frappe.throw(_('Atleast one interview has to be selected.')) + + return interviews + + +def validate_questionnaire_settings(): + settings = frappe.db.get_value('HR Settings', 'HR Settings', + ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True) + + if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template: + frappe.throw( + message=_('Please set {0} and {1} in {2}.').format( + frappe.bold('Exit Questionnaire Web Form'), + frappe.bold('Notification Template'), + get_link_to_form('HR Settings', 'HR Settings')), + title=_('Settings Missing') ) - frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email), - title='Success', indicator='green') - exit_interview.db_set('questionnaire_email_sent', True) - exit_interview.notify_update() - else: - frappe.msgprint(_('Email IDs for employee not found.')) \ No newline at end of file + + +def show_email_summary(email_success, email_failure): + message = '' + if email_success: + message += _('{0}: {1}').format( + frappe.bold('Sent Successfully'), ', '.join(email_success)) + if email_failure: + message += + '

' + _('{0} due to missing email information for employee(s): {1}').format( + frappe.bold('Sending Failed'), ', '.join(email_failure)) + + frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/exit_interview_list.js b/erpnext/hr/doctype/exit_interview/exit_interview_list.js new file mode 100644 index 0000000000..93d7b213f2 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview_list.js @@ -0,0 +1,27 @@ +frappe.listview_settings['Exit Interview'] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + let status_color = { + 'Pending': 'orange', + 'Scheduled': 'yellow', + 'Completed': 'green', + 'Cancelled': 'red', + }; + return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status]; + }, + + onload: function(listview) { + if (frappe.boot.user.can_write.includes('Exit Interview')) { + listview.page.add_action_item(__('Send Exit Questionnaires'), function() { + const interviews = listview.get_checked_items(); + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + freeze: true, + args: { + 'interviews': interviews + } + }); + }); + } + } +}; From d67536cc8d11ce7ec6e4fa0f46f633eba724ba33 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 19:55:25 +0530 Subject: [PATCH 125/247] feat: update Exit Interview date in employee master on submission --- .../hr/doctype/employee/employee_dashboard.py | 6 +++++- .../doctype/exit_interview/exit_interview.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index a4c0af0a4b..a1247d9eb1 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -21,7 +21,11 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance'] + }, + { + 'label': _('Exit'), + 'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index b2ada4d83c..59dd4631c7 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -22,7 +22,11 @@ class ExitInterview(Document): title=_('Relieving Date Missing')) def validate_duplicate_interview(self): - doc = frappe.db.exists('Exit Interview', {'employee': self.employee, 'name': ('!=', self.name)}) + doc = frappe.db.exists('Exit Interview', { + 'employee': self.employee, + 'name': ('!=', self.name), + 'docstatus': ('!=', 2) + }) if doc: frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), @@ -36,6 +40,18 @@ class ExitInterview(Document): if self.status != 'Completed': frappe.throw(_('Only Completed documents can be submitted')) + self.update_interview_date_in_employee() + + def on_cancel(self): + self.update_interview_date_in_employee() + self.db_set('status', 'Cancelled') + + def update_interview_date_in_employee(self): + if self.docstatus == 1: + frappe.db.set_value('Employee', self.employee, 'held_on', self.date) + elif self.docstatus == 2: + frappe.db.set_value('Employee', self.employee, 'held_on', None) + @frappe.whitelist() def send_exit_questionnaire(interviews): From 3437f568be7ceafa3dc00af163c185aeec865c77 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 21:55:13 +0530 Subject: [PATCH 126/247] feat: Employee Exits report --- erpnext/hr/report/employee_exits/__init__.py | 0 .../report/employee_exits/employee_exits.js | 77 ++++++ .../report/employee_exits/employee_exits.json | 33 +++ .../report/employee_exits/employee_exits.py | 231 ++++++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 erpnext/hr/report/employee_exits/__init__.py create mode 100644 erpnext/hr/report/employee_exits/employee_exits.js create mode 100644 erpnext/hr/report/employee_exits/employee_exits.json create mode 100644 erpnext/hr/report/employee_exits/employee_exits.py diff --git a/erpnext/hr/report/employee_exits/__init__.py b/erpnext/hr/report/employee_exits/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/report/employee_exits/employee_exits.js b/erpnext/hr/report/employee_exits/employee_exits.js new file mode 100644 index 0000000000..ac677d87e7 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.js @@ -0,0 +1,77 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Employee Exits"] = { + filters: [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.nowdate() + }, + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company" + }, + { + "fieldname": "department", + "label": __("Department"), + "fieldtype": "Link", + "options": "Department" + }, + { + "fieldname": "designation", + "label": __("Designation"), + "fieldtype": "Link", + "options": "Designation" + }, + { + "fieldname": "employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "reports_to", + "label": __("Reports To"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "interview_status", + "label": __("Interview Status"), + "fieldtype": "Select", + "options": ["", "Pending", "Scheduled", "Completed"] + }, + { + "fieldname": "final_decision", + "label": __("Final Decision"), + "fieldtype": "Select", + "options": ["", "Employee Retained", "Exit Confirmed"] + }, + { + "fieldname": "exit_interview_pending", + "label": __("Exit Interview Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "questionnaire_pending", + "label": __("Exit Questionnaire Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "fnf_pending", + "label": __("FnF Pending"), + "fieldtype": "Check" + } + ] +}; diff --git a/erpnext/hr/report/employee_exits/employee_exits.json b/erpnext/hr/report/employee_exits/employee_exits.json new file mode 100644 index 0000000000..4fe9a853c0 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-12-05 19:47:18.332319", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test", + "modified": "2021-12-05 19:47:18.332319", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Exits", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Exit Interview", + "report_name": "Employee Exits", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py new file mode 100644 index 0000000000..fd49543d08 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -0,0 +1,231 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _ +from frappe.utils import getdate + + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + chart = get_chart_data(data) + report_summary = get_report_summary(data) + + return columns, data, None, chart, report_summary + +def get_columns(): + return [ + { + 'label': _('Employee'), + 'fieldname': 'employee', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 150 + }, + { + 'label': _('Employee Name'), + 'fieldname': 'employee_name', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Date of Joining'), + 'fieldname': 'date_of_joining', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Relieving Date'), + 'fieldname': 'relieving_date', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Department'), + 'fieldname': 'department', + 'fieldtype': 'Link', + 'options': 'Department', + 'width': 120 + }, + { + 'label': _('Designation'), + 'fieldname': 'designation', + 'fieldtype': 'Link', + 'options': 'Designation', + 'width': 120 + }, + { + 'label': _('Reports To'), + 'fieldname': 'reports_to', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 98 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 150 + } + ] + +def get_data(filters): + data = [] + + employee = frappe.qb.DocType('Employee') + interview = frappe.qb.DocType('Exit Interview') + fnf = frappe.qb.DocType('Full and Final Statement') + + query = ( + frappe.qb.from_(employee) + .left_join(interview).on(interview.employee == employee.name) + .left_join(fnf).on(fnf.employee == employee.name) + .select( + employee.name.as_('employee'), employee.employee_name.as_('employee_name'), + employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'), + employee.department.as_('department'), employee.designation.as_('designation'), + employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), + interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') + ).distinct() + .where( + ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) + & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) + & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2))) + ) + ) + + query = get_conditions(filters, query, employee, interview, fnf) + result = query.run(as_dict=True) + + return result + + +def get_conditions(filters, query, employee, interview, fnf): + if filters.get('from_date') and filters.get('to_date'): + query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))]) + + elif filters.get('from_date'): + query = query.where(employee.relieving_date >= filters.get('from_date')) + + elif filters.get('to_date'): + query = query.where(employee.relieving_date <= filters.get('to_date')) + + if filters.get('company'): + query = query.where(employee.company == filters.get('company')) + + if filters.get('department'): + query = query.where(employee.department == filters.get('department')) + + if filters.get('designation'): + query = query.where(employee.designation == filters.get('designation')) + + if filters.get('employee'): + query = query.where(employee.name == filters.get('employee')) + + if filters.get('reports_to'): + query = query.where(employee.reports_to == filters.get('reports_to')) + + if filters.get('interview_status'): + query = query.where(interview.status == filters.get('interview_status')) + + if filters.get('final_decision'): + query = query.where(interview.employee_status == filters.get('final_decision')) + + if filters.get('exit_interview_pending'): + query = query.where((interview.name == '') | (interview.name.isnull())) + + if filters.get('questionnaire_pending'): + query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) + + if filters.get('fnf_pending'): + query = query.where((fnf.name == '') | (interview.name.isnull())) + + return query + + +def get_chart_data(data): + if not data: + return None + + retained = 0 + exit_confirmed = 0 + pending = 0 + + for entry in data: + if entry.employee_status == 'Employee Retained': + retained += 1 + elif entry.employee_status == 'Exit Confirmed': + exit_confirmed += 1 + else: + pending += 1 + + chart = { + 'data': { + 'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')], + 'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}] + }, + 'type': 'donut', + 'colors': ['green', 'red', 'blue'], + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_resignations = len(data) + interviews_pending = len([entry.name for entry in data if not entry.exit_interview]) + fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement]) + questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire]) + + return [ + { + 'value': total_resignations, + 'label': _('Total Resignations'), + 'indicator': 'Red' if total_resignations > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': interviews_pending, + 'label': _('Pending Interviews'), + 'indicator': 'Blue' if interviews_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': fnf_pending, + 'label': _('Pending FnF'), + 'indicator': 'Blue' if fnf_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': questionnaires_pending, + 'label': _('Pending Questionnaires'), + 'indicator': 'Blue' if questionnaires_pending > 0 else 'Green', + 'datatype': 'Int' + }, + ] + From 1c09439d037b2694c309f497a0b3d611e8241798 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 22:06:19 +0530 Subject: [PATCH 127/247] chore: update HR workspace --- erpnext/hr/workspace/hr/hr.json | 871 +++++++++++++++++++++++++++++--- 1 file changed, 789 insertions(+), 82 deletions(-) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index 7408d63eee..85e641c856 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -5,7 +5,7 @@ "label": "Outgoing Salary" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:58.322521", "docstatus": 0, "doctype": "Workspace", @@ -15,14 +15,6 @@ "idx": 0, "label": "HR", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Employee", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -111,14 +103,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Employee Lifecycle", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Job Applicant", "hidden": 0, @@ -227,14 +211,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Shift Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -268,14 +244,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Leaves", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -386,14 +354,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Attendance", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Employee", "hidden": 0, @@ -449,14 +409,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Expense Claims", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Employee", "hidden": 0, @@ -489,14 +441,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Settings", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -530,14 +474,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Fleet Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "hidden": 0, "is_query_report": 0, @@ -581,14 +517,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Recruitment", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -808,14 +736,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Key Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Attendance", "hidden": 0, @@ -933,9 +853,796 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Lifecycle", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Job Applicant", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Onboarding", + "link_count": 0, + "link_to": "Employee Onboarding", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Skill Map", + "link_count": 0, + "link_to": "Employee Skill Map", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Promotion", + "link_count": 0, + "link_to": "Employee Promotion", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Transfer", + "link_count": 0, + "link_to": "Employee Transfer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_count": 0, + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_count": 0, + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Onboarding Template", + "link_count": 0, + "link_to": "Employee Onboarding Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Exit", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Separation Template", + "link_count": 0, + "link_to": "Employee Separation Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Separation", + "link_count": 0, + "link_to": "Employee Separation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Full and Final Statement", + "link_count": 0, + "link_to": "Full and Final Statement", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Exit Interview", + "link_count": 0, + "link_to": "Exit Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee", + "link_count": 8, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee", + "link_count": 0, + "link_to": "Employee", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employment Type", + "link_count": 0, + "link_to": "Employment Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Branch", + "link_count": 0, + "link_to": "Branch", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Department", + "link_count": 0, + "link_to": "Department", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Designation", + "link_count": 0, + "link_to": "Designation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grade", + "link_count": 0, + "link_to": "Employee Grade", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Group", + "link_count": 0, + "link_to": "Employee Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Health Insurance", + "link_count": 0, + "link_to": "Employee Health Insurance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Key Reports", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Attendance", + "hidden": 0, + "is_query_report": 1, + "label": "Monthly Attendance Sheet", + "link_count": 0, + "link_to": "Monthly Attendance Sheet", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Staffing Plan", + "hidden": 0, + "is_query_report": 1, + "label": "Recruitment Analytics", + "link_count": 0, + "link_to": "Recruitment Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Analytics", + "link_count": 0, + "link_to": "Employee Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance", + "link_count": 0, + "link_to": "Employee Leave Balance", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance Summary", + "link_count": 0, + "link_to": "Employee Leave Balance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee Advance", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Advance Summary", + "link_count": 0, + "link_to": "Employee Advance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Exits", + "link_count": 0, + "link_to": "Employee Exits", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Recruitment", + "link_count": 11, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Opening", + "link_count": 0, + "link_to": "Job Opening", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Referral", + "link_count": 0, + "link_to": "Employee Referral", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Applicant", + "link_count": 0, + "link_to": "Job Applicant", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Offer", + "link_count": 0, + "link_to": "Job Offer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Staffing Plan", + "link_count": 0, + "link_to": "Staffing Plan", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter", + "link_count": 0, + "link_to": "Appointment Letter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter Template", + "link_count": 0, + "link_to": "Appointment Letter Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Type", + "link_count": 0, + "link_to": "Interview Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Round", + "link_count": 0, + "link_to": "Interview Round", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview", + "link_count": 0, + "link_to": "Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Feedback", + "link_count": 0, + "link_to": "Interview Feedback", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Fleet Management", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Driver", + "link_count": 0, + "link_to": "Driver", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Vehicle", + "link_count": 0, + "link_to": "Vehicle", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Vehicle Log", + "link_count": 0, + "link_to": "Vehicle Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Vehicle", + "hidden": 0, + "is_query_report": 1, + "label": "Vehicle Expenses", + "link_count": 0, + "link_to": "Vehicle Expenses", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "HR Settings", + "link_count": 0, + "link_to": "HR Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Daily Work Summary Group", + "link_count": 0, + "link_to": "Daily Work Summary Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Team Updates", + "link_count": 0, + "link_to": "team-updates", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Expense Claims", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Expense Claim", + "link_count": 0, + "link_to": "Expense Claim", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Advance", + "link_count": 0, + "link_to": "Employee Advance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Travel Request", + "link_count": 0, + "link_to": "Travel Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Attendance", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Attendance Tool", + "link_count": 0, + "link_to": "Employee Attendance Tool", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Attendance", + "link_count": 0, + "link_to": "Attendance", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Attendance Request", + "link_count": 0, + "link_to": "Attendance Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Upload Attendance", + "link_count": 0, + "link_to": "Upload Attendance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Checkin", + "link_count": 0, + "link_to": "Employee Checkin", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Leaves", + "link_count": 10, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Holiday List", + "link_count": 0, + "link_to": "Holiday List", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Type", + "link_count": 0, + "link_to": "Leave Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Period", + "link_count": 0, + "link_to": "Leave Period", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Type", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy", + "link_count": 0, + "link_to": "Leave Policy", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Policy", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy Assignment", + "link_count": 0, + "link_to": "Leave Policy Assignment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Application", + "link_count": 0, + "link_to": "Leave Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Allocation", + "link_count": 0, + "link_to": "Leave Allocation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Encashment", + "link_count": 0, + "link_to": "Leave Encashment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Block List", + "link_count": 0, + "link_to": "Leave Block List", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Compensatory Leave Request", + "link_count": 0, + "link_to": "Compensatory Leave Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Shift Management", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Type", + "link_count": 0, + "link_to": "Shift Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Request", + "link_count": 0, + "link_to": "Shift Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Assignment", + "link_count": 0, + "link_to": "Shift Assignment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2021-08-31 12:18:59.842919", + "modified": "2021-12-05 22:05:13.004462", "modified_by": "Administrator", "module": "HR", "name": "HR", From b69e0d2c6353c924591c79c320e17b71c32104b0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 22:28:22 +0530 Subject: [PATCH 128/247] feat: default Notification - a day before Exit Interview --- .../exit_interview_scheduled/__init__.py | 0 .../exit_interview_scheduled.json | 29 +++++++++++++++ .../exit_interview_scheduled.md | 37 +++++++++++++++++++ .../exit_interview_scheduled.py | 5 +++ 4 files changed, 71 insertions(+) create mode 100644 erpnext/hr/notification/exit_interview_scheduled/__init__.py create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py diff --git a/erpnext/hr/notification/exit_interview_scheduled/__init__.py b/erpnext/hr/notification/exit_interview_scheduled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json new file mode 100644 index 0000000000..8323ef0694 --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json @@ -0,0 +1,29 @@ +{ + "attach_print": 0, + "channel": "Email", + "condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'", + "creation": "2021-12-05 22:11:47.263933", + "date_changed": "date", + "days_in_advance": 1, + "docstatus": 0, + "doctype": "Notification", + "document_type": "Exit Interview", + "enabled": 1, + "event": "Days Before", + "idx": 0, + "is_standard": 1, + "message": "\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n
\n\t\t\t
\n\t\t\t\t{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}\n\t\t\t
\n\t\t
\n\n\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n
\n\t\t\t
\n\t\t\t\t
    \n\t\t\t\t\t
  • {{_(\"Employee\")}}: {{ doc.employee }} - {{ doc.employee_name }}
  • \n\t\t\t\t\t
  • {{_(\"Date\")}}: {{ doc.date }}
  • \n\t\t\t\t\t
  • {{_(\"Interviewers\")}}:
  • \n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t
    • {{ entry.user }}
    • \n\t\t\t\t\t\t
    \n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t
  • {{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n\t\t\t\t
\n\t\t\t
\n\t\t
\n", + "modified": "2021-12-05 22:26:57.096159", + "modified_by": "Administrator", + "module": "HR", + "name": "Exit Interview Scheduled", + "owner": "Administrator", + "recipients": [ + { + "receiver_by_document_field": "email" + } + ], + "send_system_notification": 0, + "send_to_all_assignees": 1, + "subject": "Exit Interview Scheduled: {{ doc.name }}" +} \ No newline at end of file diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md new file mode 100644 index 0000000000..6d6db4014b --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md @@ -0,0 +1,37 @@ + + + + + + + + +
+
+

{{_("Exit Interview Scheduled:")}} {{ doc.name }}

+
+
+ + + + + + + + + +
+
+
    +
  • {{_("Employee")}}: {{ doc.employee }} - {{ doc.employee_name }}
  • +
  • {{_("Date")}}: {{ frappe.utils.formatdate(doc.date) }}
  • +
  • {{_("Interviewers")}}:
  • + {% for entry in doc.interviewers %} +
      +
    • {{ entry.user }}
    • +
    + {% endfor %} +
  • {{ _("Interview Document") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py new file mode 100644 index 0000000000..e1ada61927 --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py @@ -0,0 +1,5 @@ +import frappe + +def get_context(context): + # do your magic here + pass From 3230741cdeca72daa018a3eaa903e4c59a24856c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Dec 2021 10:08:51 +0530 Subject: [PATCH 129/247] fix: email summary --- erpnext/hr/doctype/exit_interview/exit_interview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 59dd4631c7..ba75100a3b 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -123,7 +123,7 @@ def show_email_summary(email_success, email_failure): message += _('{0}: {1}').format( frappe.bold('Sent Successfully'), ', '.join(email_success)) if email_failure: - message += + '

' + _('{0} due to missing email information for employee(s): {1}').format( + message += '

' + _('{0} due to missing email information for employee(s): {1}').format( frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file From 105b6d498c11c2b2e37a377fa0fd1468edeb3eb2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 5 Dec 2021 21:30:03 +0530 Subject: [PATCH 130/247] fix: remove bad defaults from selling settings "All cusotmer groups" and "All territories" are pointless defaults, not sure why these are made default. They don't help you track anything. "All" might as well be `Null`. Even the filters for customer_group suggest it shouldn't be group then having the root as default makes no sense. --- .../selling/doctype/selling_settings/selling_settings.py | 7 ------- erpnext/setup/setup_wizard/operations/install_fixtures.py | 1 - 2 files changed, 8 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index e7c5e76996..fb86e614b6 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -8,7 +8,6 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document from frappe.utils import cint -from frappe.utils.nestedset import get_root_of class SellingSettings(Document): @@ -37,9 +36,3 @@ class SellingSettings(Document): editable_bundle_item_rates = cint(self.editable_bundle_item_rates) make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) - - def set_default_customer_group_and_territory(self): - if not self.customer_group: - self.customer_group = get_root_of('Customer Group') - if not self.territory: - self.territory = get_root_of('Territory') diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 503aeacd01..98f9119885 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -303,7 +303,6 @@ def set_more_defaults(): def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") - selling_settings.set_default_customer_group_and_territory() selling_settings.cust_master_name = "Customer Name" selling_settings.so_required = "No" selling_settings.dn_required = "No" From bdf7b8d3798a940f2d0ec3ab19ec402b70166f41 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 5 Dec 2021 22:00:52 +0530 Subject: [PATCH 131/247] fix: patch to remove default item group and territory --- erpnext/patches.txt | 1 + .../patches/v13_0/remove_bad_selling_defaults.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v13_0/remove_bad_selling_defaults.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce25..7a2cc7a897 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -278,6 +278,7 @@ erpnext.patches.v13_0.update_tds_check_field #3 erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.shopify_deprecation_warning +erpnext.patches.v13_0.remove_bad_selling_defaults erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.einvoicing_deprecation_warning diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py new file mode 100644 index 0000000000..5487a6c60c --- /dev/null +++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py @@ -0,0 +1,15 @@ +import frappe +from frappe import _ + + +def execute(): + selling_settings = frappe.get_single("Selling Settings") + + if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"): + selling_settings.customer_group = None + + if selling_settings.territory in (_("All Territories"), "All Territories"): + selling_settings.territory = None + + selling_settings.flags.ignore_mandatory=True + selling_settings.save(ignore_permissions=True) From a8375239eb407d434a72a4c8b4ca81556908fb02 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 5 Dec 2021 22:07:14 +0530 Subject: [PATCH 132/247] test: set customer group and territory defaults --- erpnext/setup/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 1478007da8..cad4c54d7d 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -53,6 +53,7 @@ def before_tests(): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) enable_all_roles_and_domains() + set_defaults_for_tests() frappe.db.commit() @@ -127,6 +128,14 @@ def enable_all_roles_and_domains(): [d.name for d in domains]) add_all_roles_to('Administrator') +def set_defaults_for_tests(): + from frappe.utils.nestedset import get_root_of + + selling_settings = frappe.get_single("Selling Settings") + selling_settings.customer_group = get_root_of("Customer Group") + selling_settings.territory = get_root_of("Territory") + selling_settings.save() + def insert_record(records): for r in records: From 56df646067af5c127ff133f54ee31e507fc8cd5e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 11:19:58 +0530 Subject: [PATCH 133/247] fix(test): test_depr_entry_posting_with_income_account --- erpnext/assets/doctype/asset/test_asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index b0549b1027..9b5dcc4fe4 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -903,6 +903,7 @@ class TestDepreciationBasics(AssetSetup): depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") depr_expense_account.root_type = "Income" depr_expense_account.parent_account = "Income - _TC" + depr_expense_account.save() asset = create_asset( item_code = "Macbook Pro", @@ -932,6 +933,7 @@ class TestDepreciationBasics(AssetSetup): # resetting depr_expense_account.root_type = "Expense" depr_expense_account.parent_account = "Expenses - _TC" + depr_expense_account.save() def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" From f9408d170a8f7e863144003e5966f7b5a3219d3d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 12:38:25 +0530 Subject: [PATCH 134/247] patch: split 'ongoing' sla status --- erpnext/patches.txt | 1 + .../rename_ongoing_status_in_sla_documents.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce25..d85f2339ae 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents \ No newline at end of file diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py new file mode 100644 index 0000000000..dddf4a3778 --- /dev/null +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -0,0 +1,27 @@ +import frappe + + +def execute(): + active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])] + + for doctype in active_sla_documents: + doctype = frappe.qb.DocType(doctype) + try: + query = ( + frappe.qb + .update(doctype) + .set(doctype.agreement_status, 'First Response Due') + .where( + (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + ) + ) + query.run() + query = ( + frappe.qb + .update(doctype) + .set(doctype.agreement_status, 'Resolution Due') + .where(doctype.agreement_status == 'Ongoing') + ) + query.run() + except Exception as e: + frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file From eb522a374644bdf533411acbbf64e7b6a2aaa229 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 6 Dec 2021 12:26:59 +0530 Subject: [PATCH 135/247] refactor: map serial from schedule if only one --- .../maintenance_schedule/maintenance_schedule.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index db126cde8e..2ffae1a4f2 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -323,10 +323,14 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No target.maintenance_schedule = source.name target.maintenance_schedule_detail = s_id - def update_sales(source, target, parent): + def update_sales_and_serial(source, target, parent): sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') target.service_person = sales_person - target.serial_no = '' + serial_nos = get_serial_nos(target.serial_no) + if len(serial_nos) == 1: + target.serial_no = serial_nos[0] + else: + target.serial_no = '' doclist = get_mapped_doc("Maintenance Schedule", source_name, { "Maintenance Schedule": { @@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", "condition": lambda doc: doc.item_name == item_name, - "postprocess": update_sales + "postprocess": update_sales_and_serial } }, target_doc) From 3928a402d4b9b3b0e8dc6a63d4a67f77ecfedbb9 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Mon, 6 Dec 2021 13:52:00 +0530 Subject: [PATCH 136/247] feat: CRM Settings (#27788) * feat: crm settings * feat: CRM Settings * feat: lead and opprtunity section * feat: added CRM Settings in ERPNext Settings workspace * fix: review chnages * added patch * fix: linter issues * fix: linter issues * fix: linter issues * fix: removed crm settings from selling module * fix: raw query to frappe.qb * fix: removed hardcoded value * fix: linter issue * fix: simplify CRM Settings migration patch Co-authored-by: Anupam Kumar Co-authored-by: Rucha Mahabal --- erpnext/crm/doctype/crm_settings/__init__.py | 0 .../crm/doctype/crm_settings/crm_settings.js | 8 ++ .../doctype/crm_settings/crm_settings.json | 114 ++++++++++++++++++ .../crm/doctype/crm_settings/crm_settings.py | 9 ++ .../doctype/crm_settings/test_crm_settings.py | 9 ++ erpnext/crm/doctype/lead/lead.py | 86 ++++++------- .../crm/doctype/opportunity/opportunity.py | 73 +++++------ erpnext/patches.txt | 1 + erpnext/patches/v14_0/migrate_crm_settings.py | 16 +++ .../selling_settings/selling_settings.json | 35 +----- .../erpnext_settings/erpnext_settings.json | 9 +- erpnext/startup/boot.py | 2 +- 12 files changed, 249 insertions(+), 113 deletions(-) create mode 100644 erpnext/crm/doctype/crm_settings/__init__.py create mode 100644 erpnext/crm/doctype/crm_settings/crm_settings.js create mode 100644 erpnext/crm/doctype/crm_settings/crm_settings.json create mode 100644 erpnext/crm/doctype/crm_settings/crm_settings.py create mode 100644 erpnext/crm/doctype/crm_settings/test_crm_settings.py create mode 100644 erpnext/patches/v14_0/migrate_crm_settings.py diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js new file mode 100644 index 0000000000..c6569d8122 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('CRM Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json new file mode 100644 index 0000000000..95b19fa982 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "creation": "2021-09-09 17:03:22.754446", + "description": "Settings for Selling Module", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "campaign_naming_by", + "allow_lead_duplication_based_on_emails", + "column_break_4", + "create_event_on_next_contact_date", + "auto_creation_of_contact", + "opportunity_section", + "close_opportunity_after_days", + "column_break_9", + "create_event_on_next_contact_date_opportunity", + "quotation_section", + "default_valid_till" + ], + "fields": [ + { + "fieldname": "campaign_naming_by", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Lead" + }, + { + "default": "0", + "fieldname": "allow_lead_duplication_based_on_emails", + "fieldtype": "Check", + "label": "Allow Lead Duplication based on Emails" + }, + { + "default": "1", + "fieldname": "auto_creation_of_contact", + "fieldtype": "Check", + "label": "Auto Creation of Contact" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "opportunity_section", + "fieldtype": "Section Break", + "label": "Opportunity" + }, + { + "default": "15", + "description": "Auto close Opportunity Replied after the no. of days mentioned above", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Replied Opportunity After Days" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date_opportunity", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "quotation_section", + "fieldtype": "Section Break", + "label": "Quotation" + } + ], + "icon": "fa fa-cog", + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "migration_hash": "3ae78b12dd1c64d551736c6e82092f90", + "modified": "2021-11-03 09:00:36.883496", + "modified_by": "Administrator", + "module": "CRM", + "name": "CRM Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py new file mode 100644 index 0000000000..bde52547c9 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CRMSettings(Document): + pass diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py new file mode 100644 index 0000000000..3372c5deb4 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestCRMSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c590523a4f..9adbe8b6f1 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -11,6 +11,7 @@ from frappe.utils import ( cint, comma_and, cstr, + get_link_to_form, getdate, has_gravatar, nowdate, @@ -91,13 +92,14 @@ class Lead(SellingController): self.contact_doc.save() def add_calendar_event(self, opts=None, force=False): - super(Lead, self).add_calendar_event({ - "owner": self.lead_owner, - "starts_on": self.contact_date, - "ends_on": self.ends_on or "", - "subject": ('Contact ' + cstr(self.lead_name)), - "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') - }, force) + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'): + super(Lead, self).add_calendar_event({ + "owner": self.lead_owner, + "starts_on": self.contact_date, + "ends_on": self.ends_on or "", + "subject": ('Contact ' + cstr(self.lead_name)), + "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') + }, force) def update_prospects(self): prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent']) @@ -108,12 +110,13 @@ class Lead(SellingController): def check_email_id_is_unique(self): if self.email_id: # validate email is unique - duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) - duplicate_leads = [lead.name for lead in duplicate_leads] + if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'): + duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) + duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads] - if duplicate_leads: - frappe.throw(_("Email Address must be unique, already exists for {0}") - .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) + if duplicate_leads: + frappe.throw(_("Email Address must be unique, already exists for {0}") + .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) def on_trash(self): frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) @@ -172,41 +175,42 @@ class Lead(SellingController): self.title = self.company_name or self.lead_name def create_contact(self): - if not self.lead_name: - self.set_full_name() - self.set_lead_name() + if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'): + if not self.lead_name: + self.set_full_name() + self.set_lead_name() - contact = frappe.new_doc("Contact") - contact.update({ - "first_name": self.first_name or self.lead_name, - "last_name": self.last_name, - "salutation": self.salutation, - "gender": self.gender, - "designation": self.designation, - "company_name": self.company_name, - }) - - if self.email_id: - contact.append("email_ids", { - "email_id": self.email_id, - "is_primary": 1 + contact = frappe.new_doc("Contact") + contact.update({ + "first_name": self.first_name or self.lead_name, + "last_name": self.last_name, + "salutation": self.salutation, + "gender": self.gender, + "designation": self.designation, + "company_name": self.company_name, }) - if self.phone: - contact.append("phone_nos", { - "phone": self.phone, - "is_primary_phone": 1 - }) + if self.email_id: + contact.append("email_ids", { + "email_id": self.email_id, + "is_primary": 1 + }) - if self.mobile_no: - contact.append("phone_nos", { - "phone": self.mobile_no, - "is_primary_mobile_no":1 - }) + if self.phone: + contact.append("phone_nos", { + "phone": self.phone, + "is_primary_phone": 1 + }) - contact.insert(ignore_permissions=True) + if self.mobile_no: + contact.append("phone_nos", { + "phone": self.mobile_no, + "is_primary_mobile_no":1 + }) - return contact + contact.insert(ignore_permissions=True) + + return contact @frappe.whitelist() def make_customer(source_name, target_doc=None): diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 0bef80a749..fcbd4ded39 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -8,6 +8,7 @@ import frappe from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import DocType from frappe.utils import cint, cstr, flt, get_fullname from erpnext.setup.utils import get_exchange_rate @@ -28,7 +29,6 @@ class Opportunity(TransactionBase): }) self.make_new_lead_if_required() - self.validate_item_details() self.validate_uom_is_integer("uom", "qty") self.validate_cust_name() @@ -70,21 +70,21 @@ class Opportunity(TransactionBase): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: # check if customer is already created agains the self.contact_email - customer = frappe.db.sql("""select - distinct `tabDynamic Link`.link_name as customer - from - `tabContact`, - `tabDynamic Link` - where `tabContact`.email_id='{0}' - and - `tabContact`.name=`tabDynamic Link`.parent - and - ifnull(`tabDynamic Link`.link_name, '')<>'' - and - `tabDynamic Link`.link_doctype='Customer' - """.format(self.contact_email), as_dict=True) - if customer and customer[0].customer: - self.party_name = customer[0].customer + dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact") + customer = frappe.qb.from_( + dynamic_link + ).join( + contact + ).on( + (contact.name == dynamic_link.parent) + & (dynamic_link.link_doctype == "Customer") + & (contact.email_id == self.contact_email) + ).select( + dynamic_link.link_name + ).distinct().run(as_dict=True) + + if customer and customer[0].link_name: + self.party_name = customer[0].link_name self.opportunity_from = "Customer" return @@ -191,30 +191,31 @@ class Opportunity(TransactionBase): self.add_calendar_event() def add_calendar_event(self, opts=None, force=False): - if not opts: - opts = frappe._dict() + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'): + if not opts: + opts = frappe._dict() - opts.description = "" - opts.contact_date = self.contact_date + opts.description = "" + opts.contact_date = self.contact_date - if self.party_name and self.opportunity_from == 'Customer': - if self.contact_person: - opts.description = 'Contact '+cstr(self.contact_person) - else: - opts.description = 'Contact customer '+cstr(self.party_name) - elif self.party_name and self.opportunity_from == 'Lead': - if self.contact_display: - opts.description = 'Contact '+cstr(self.contact_display) - else: - opts.description = 'Contact lead '+cstr(self.party_name) + if self.party_name and self.opportunity_from == 'Customer': + if self.contact_person: + opts.description = 'Contact '+cstr(self.contact_person) + else: + opts.description = 'Contact customer '+cstr(self.party_name) + elif self.party_name and self.opportunity_from == 'Lead': + if self.contact_display: + opts.description = 'Contact '+cstr(self.contact_display) + else: + opts.description = 'Contact lead '+cstr(self.party_name) - opts.subject = opts.description - opts.description += '. By : ' + cstr(self.contact_by) + opts.subject = opts.description + opts.description += '. By : ' + cstr(self.contact_by) - if self.to_discuss: - opts.description += ' To Discuss : ' + cstr(self.to_discuss) + if self.to_discuss: + opts.description += ' To Discuss : ' + cstr(self.to_discuss) - super(Opportunity, self).add_calendar_event(opts, force) + super(Opportunity, self).add_calendar_event(opts, force) def validate_item_details(self): if not self.get('items'): @@ -363,7 +364,7 @@ def set_multiple_status(names, status): def auto_close_opportunity(): """ auto close the `Replied` Opportunities after 7 days """ - auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15 + auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and modified Date: Mon, 6 Dec 2021 14:34:59 +0530 Subject: [PATCH 137/247] refactor: handle special cases on communication creation --- erpnext/hooks.py | 2 +- .../service_level_agreement.py | 34 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 546166fe19..09037f0f6c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -234,7 +234,7 @@ doc_events = { }, "Communication": { "on_update": [ - "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time", + "erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update", "erpnext.support.doctype.issue.issue.set_first_response_time" ] }, diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 662e42cc9f..9ae2d64d6f 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -376,14 +376,14 @@ def process_sla(doc, sla): doc.service_level_agreement = sla.name doc.priority = doc.get("priority") or sla.default_priority - prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') - handle_status_change(doc, prev_status, sla.apply_sla_for_resolution) + handle_status_change(doc, sla.apply_sla_for_resolution) update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution) update_agreement_status(doc, sla.apply_sla_for_resolution) -def handle_status_change(doc, prev_status, apply_sla_for_resolution): +def handle_status_change(doc, apply_sla_for_resolution): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') hold_statuses = get_hold_statuses(doc.service_level_agreement) fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) @@ -407,7 +407,7 @@ def handle_status_change(doc, prev_status, apply_sla_for_resolution): doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours doc.on_hold_since = None - if is_open_status(prev_status) and not is_open_status(doc.status): + if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): # status changed from Open to something else if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.first_responded_on = now_time @@ -625,8 +625,8 @@ def reset_resolution_metrics(doc): # called via hooks on communication update -def update_hold_time(doc, status): - if doc.communication_type == "Comment" or doc.sent_or_received != "Received": +def on_communication_update(doc, status): + if doc.communication_type == "Comment": return parent = get_parent_doc(doc) @@ -638,7 +638,27 @@ def update_hold_time(doc, status): for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - handle_status_change(parent, 'Replied', for_resolution) + if ( + doc.sent_or_received == "Received" # a reply is received + and parent.get('status') == 'Open' # issue status is set as open from communication.py + and parent._doc_before_save + and parent.get('status') != parent._doc_before_save.get('status') # status changed + ): + # undo the status change in db + # since prev status is fetched from db + frappe.db.set_value(parent.doctype, parent.name, 'status', parent._doc_before_save.get('status')) + + elif ( + doc.sent_or_received == "Sent" # a reply is sent + and parent.get('first_responded_on') # first_responded_on is set from communication.py + and parent._doc_before_save + and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set + ): + # reset first_responded_on since it will be handled/set later on + parent.first_responded_on = None + parent.flags.on_first_reply = True + + handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) From 812572d250e5dd18ebdd9e043826ba7616e504fe Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 14:35:35 +0530 Subject: [PATCH 138/247] feat: record assignment on first response failure --- erpnext/support/doctype/issue/test_issue.py | 38 +++++++++++++++++++ .../service_level_agreement.py | 22 +++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index d566f33f24..477ac7260c 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -211,6 +211,43 @@ class TestIssue(TestSetUp): self.assertEquals(issue.agreement_status, 'Fulfilled') self.assertEquals(issue.resolution_date, frappe.flags.current_time) + def test_recording_of_assignment_on_first_reponse_failure(self): + from frappe.desk.form.assign_to import add as add_assignment + + frappe.flags.current_time = get_datetime("2021-11-01 19:00") + + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + add_assignment({ + 'doctype': issue.doctype, + 'name': issue.name, + 'assign_to': ['test@admin.com'] + }) + issue.reload() + + # send a reply failing response SLA + frappe.flags.current_time = get_datetime("2021-11-02 15:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + + # assert if a new timeline item has been added + # to record the assignment + comment = frappe.get_last_doc('Comment') + self.assertTrue('First Response SLA Failed' in comment.content) + + def test_agreement_status_on_response(self): + frappe.flags.current_time = get_datetime("2021-11-01 19:00") + + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + self.assertTrue(issue.status == 'Open') + + # send a reply within response SLA + frappe.flags.current_time = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + + issue.reload() + self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + self.assertEquals(issue.agreement_status, 'Resolution Due') class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm @@ -425,6 +462,7 @@ class TestFirstResponseTime(TestSetUp): def create_issue_and_communication(issue_creation, first_responded_on): issue = make_issue(issue_creation, index=1) sender = create_user("test@admin.com") + frappe.flags.current_time = first_responded_on create_communication(issue.name, sender.email, "Sent", first_responded_on) issue.reload() diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9ae2d64d6f..19aa5783f7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -397,6 +397,12 @@ def handle_status_change(doc, apply_sla_for_resolution): def is_open_status(status): return status not in hold_statuses and status not in fulfillment_statuses + def set_first_response(): + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + doc.first_responded_on = now_time + if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')): + record_assigned_users_on_failure(doc) + def calculate_hold_hours(): # In case issue was closed and after few days it has been opened # The hold time should be calculated from resolution_date @@ -408,9 +414,7 @@ def handle_status_change(doc, apply_sla_for_resolution): doc.on_hold_since = None if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): - # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): - doc.first_responded_on = now_time + set_first_response() # Open to Replied if is_open_status(prev_status) and is_hold_status(doc.status): @@ -688,6 +692,18 @@ def set_resolution_by(doc, start_date_time, priority): doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) +def record_assigned_users_on_failure(doc): + assigned_users = doc.get_assigned_users() + if assigned_users: + from frappe.utils import get_fullname + assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) + message = _(f'First Response SLA Failed by {assigned_users}') + doc.add_comment( + comment_type='Assigned', + text=message + ) + + def get_service_level_agreement_fields(): return [ { From 3dabac15edf4baedfc3da288400e16933d497e70 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Dec 2021 15:05:20 +0530 Subject: [PATCH 139/247] fix: Ageing in AR/AP report for advances --- .../report/accounts_receivable/accounts_receivable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 353f9087f1..a990f23cd6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -545,7 +545,9 @@ class ReceivablePayableReport(object): def set_ageing(self, row): if self.filters.ageing_based_on == "Due Date": - entry_date = row.due_date + # use posting date as a fallback for advances posted via journal and payment entry + # when ageing viewed by due date + entry_date = row.due_date or row.posting_date elif self.filters.ageing_based_on == "Supplier Invoice Date": entry_date = row.bill_date else: From 32c81818f619c25878ab5620b3f5e505ef43c1e7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 15:38:01 +0530 Subject: [PATCH 140/247] fix: transalations --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 19aa5783f7..7527adf2bf 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -697,7 +697,7 @@ def record_assigned_users_on_failure(doc): if assigned_users: from frappe.utils import get_fullname assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) - message = _(f'First Response SLA Failed by {assigned_users}') + message = _('First Response SLA Failed by {}').format(assigned_users) doc.add_comment( comment_type='Assigned', text=message From 10b87e081cd0523fe63f7c6254d683ea4fb48439 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 15:38:19 +0530 Subject: [PATCH 141/247] fix: sider issues --- .../rename_ongoing_status_in_sla_documents.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py index dddf4a3778..b5296fbdb0 100644 --- a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -7,21 +7,21 @@ def execute(): for doctype in active_sla_documents: doctype = frappe.qb.DocType(doctype) try: - query = ( - frappe.qb - .update(doctype) - .set(doctype.agreement_status, 'First Response Due') - .where( - (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') - ) - ) - query.run() - query = ( - frappe.qb - .update(doctype) - .set(doctype.agreement_status, 'Resolution Due') - .where(doctype.agreement_status == 'Ongoing') - ) - query.run() - except Exception as e: + frappe.qb.update( + doctype + ).set( + doctype.agreement_status, 'First Response Due' + ).where( + (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + ).run() + + frappe.qb.update( + doctype + ).set( + doctype.agreement_status, 'Resolution Due' + ).where( + doctype.agreement_status == 'Ongoing' + ).run() + + except Exception: frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file From 1a76b3801ac1e933087b2cf14a1a3e74c6be8383 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 16:16:13 +0530 Subject: [PATCH 142/247] fix: test --- erpnext/support/doctype/issue/test_issue.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 477ac7260c..14cec46ad4 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -1,10 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt -import datetime import unittest import frappe +from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.utils import flt, get_datetime @@ -231,8 +231,13 @@ class TestIssue(TestSetUp): # assert if a new timeline item has been added # to record the assignment - comment = frappe.get_last_doc('Comment') - self.assertTrue('First Response SLA Failed' in comment.content) + comment = frappe.db.exists('Comment', { + 'reference_doctype': 'Issue', + 'reference_name': issue.name, + 'comment_type': 'Assigned', + 'content': _('First Response SLA Failed by {}').format('test') + }) + self.assertTrue(comment) def test_agreement_status_on_response(self): frappe.flags.current_time = get_datetime("2021-11-01 19:00") From 476e81a6312f9d38ae8b525b52f47317cbceefe5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 18:55:30 +0530 Subject: [PATCH 143/247] fix: time to respond & resolve indicators --- .../rename_ongoing_status_in_sla_documents.py | 4 ++-- erpnext/public/js/utils.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py index b5296fbdb0..1cc5f38f42 100644 --- a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -12,7 +12,7 @@ def execute(): ).set( doctype.agreement_status, 'First Response Due' ).where( - (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + doctype.first_responded_on.isnull() ).run() frappe.qb.update( @@ -24,4 +24,4 @@ def execute(): ).run() except Exception: - frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file + frappe.log_error(title='Failed to Patch SLA Status') \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f0facdd3a1..2d8b1be93f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -831,7 +831,7 @@ $(document).on('app_ready', function() { refresh: function(frm) { if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement - && frm.doc.agreement_status === 'Ongoing') { + && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { frappe.call({ 'method': 'frappe.client.get', args: { @@ -884,8 +884,8 @@ $(document).on('app_ready', function() { function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { frm.dashboard.clear_headline(); - let time_to_respond = get_status(frm.doc.response_by_variance); - if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') { + let time_to_respond = get_status(frm.doc.response_by); + if (!frm.doc.first_responded_on) { time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); } @@ -899,8 +899,8 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { if (apply_sla_for_resolution) { - let time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') { + let time_to_resolve = get_status(frm.doc.resolution_by); + if (!frm.doc.resolution_date) { time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); } @@ -924,8 +924,9 @@ function get_time_left(timestamp, agreement_status) { return {'diff_display': diff_display, 'indicator': indicator}; } -function get_status(variance) { - if (variance > 0) { +function get_status(timestamp) { + const time_left = moment(timestamp).diff(moment()); + if (time_left >= 0) { return {'diff_display': 'Fulfilled', 'indicator': 'green'}; } else { return {'diff_display': 'Failed', 'indicator': 'red'}; From 91aa78707c471f688acb1147956f5aac3b84af99 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:13:31 +0530 Subject: [PATCH 144/247] fix: remove missed 'ongoing' references --- erpnext/support/doctype/issue/issue.json | 2 +- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 38b5395adf..30aa7319df 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -372,7 +372,7 @@ "read_only": 1 }, { - "default": "Ongoing", + "default": "First Response Due", "depends_on": "eval: doc.service_level_agreement", "fieldname": "agreement_status", "fieldtype": "Select", diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 7527adf2bf..50f31fde2d 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -625,7 +625,7 @@ def reset_resolution_metrics(doc): doc.user_resolution_time = None if doc.meta.has_field("agreement_status"): - doc.agreement_status = "Ongoing" + doc.agreement_status = "First Response Due" # called via hooks on communication update From 3446f7545ff8178999a93fd2042c188905de1db2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:17:38 +0530 Subject: [PATCH 145/247] fix: remove 'ongoing' status from issue summary report --- erpnext/support/report/issue_summary/issue_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index 39a5c407cd..67fe345d5f 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -82,7 +82,8 @@ class IssueSummary(object): self.sla_status_map = { 'SLA Failed': 'failed', 'SLA Fulfilled': 'fulfilled', - 'SLA Ongoing': 'ongoing' + 'First Response Due': 'first_response_due', + 'Resolution Due': 'resolution_due' } for label, fieldname in self.sla_status_map.items(): From 2f7d8ac29e58898e7fe7bd05b460abdcc1ea7a5a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:19:34 +0530 Subject: [PATCH 146/247] fix: indentation --- erpnext/support/doctype/issue/issue.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 30aa7319df..3ff7d02f1a 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -347,16 +347,16 @@ "read_only": 1 }, { - "fieldname": "resolution_time", - "fieldtype": "Duration", - "label": "Resolution Time", - "read_only": 1 + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 }, { - "fieldname": "user_resolution_time", - "fieldtype": "Duration", - "label": "User Resolution Time", - "read_only": 1 + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 }, { "fieldname": "on_hold_since", From d106d59c3f750a2545cb0c82f86c75fd88ab8276 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Dec 2021 20:36:48 +0530 Subject: [PATCH 147/247] fix: TDS Monthly payable report --- .../report/tds_payable_monthly/tds_payable_monthly.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index a3a45d1e79..caee1a10bb 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): posting_date = entry.posting_date voucher_type = entry.voucher_type + if not tax_withholding_category: + tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category') + rate = tax_rate_map.get(tax_withholding_category) + if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit) - if rate and tds_deducted: + if tds_deducted: row = { 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'supplier': supplier_map.get(supplier, {}).get('name') @@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_supplier_pan_map(): supplier_map = frappe._dict() - suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name']) + suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category']) for d in suppliers: supplier_map[d.name] = d From 276bf739746bee40a8c8ad01502ecdb3fa3486e0 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:11:43 +0530 Subject: [PATCH 148/247] feat: create QR Code field if not existing --- erpnext/regional/saudi_arabia/utils.py | 189 +++++++++++++------------ 1 file changed, 96 insertions(+), 93 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 7d00d8b392..0668f98083 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,140 +1,142 @@ import io import os from base64 import b64encode +from pyqrcode import create as qr_create import frappe from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.utils.data import add_to_date, get_time, getdate -from pyqrcode import create as qr_create from erpnext import get_region def create_qr_code(doc, method): - """Create QR Code after inserting Sales Inv - """ - region = get_region(doc.company) if region not in ['Saudi Arabia']: return - # if QR Code field not present, do nothing - if not hasattr(doc, 'qr_code'): - return + # if QR Code field not present, create it. Invoices without QR are invalid as per law. + if not hasattr(doc, 'ksa_einv_qr'): + create_custom_fields({ + 'Sales Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='QR Code', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ] + }) # Don't create QR Code if it already exists - qr_code = doc.get("qr_code") + qr_code = doc.get("ksa_einv_qr") if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}): return - meta = frappe.get_meta('Sales Invoice') + meta = frappe.get_meta(doc.doctype) - for field in meta.get_image_fields(): - if field.fieldname == 'qr_code': - ''' TLV conversion for - 1. Seller's Name - 2. VAT Number - 3. Time Stamp - 4. Invoice Amount - 5. VAT Amount - ''' - tlv_array = [] - # Sellers Name + if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: + ''' TLV conversion for + 1. Seller's Name + 2. VAT Number + 3. Time Stamp + 4. Invoice Amount + 5. VAT Amount + ''' + tlv_array = [] + # Sellers Name - seller_name = frappe.db.get_value( - 'Company', - doc.company, - 'company_name_in_arabic') + seller_name = frappe.db.get_value( + 'Company', + doc.company, + 'company_name_in_arabic') - if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) + if not seller_name: + frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) - tag = bytes([1]).hex() - length = bytes([len(seller_name.encode('utf-8'))]).hex() - value = seller_name.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([1]).hex() + length = bytes([len(seller_name.encode('utf-8'))]).hex() + value = seller_name.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # VAT Number - tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') - if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) + # VAT Number + tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + if not tax_id: + frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) - tag = bytes([2]).hex() - length = bytes([len(tax_id)]).hex() - value = tax_id.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([2]).hex() + length = bytes([len(tax_id)]).hex() + value = tax_id.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Time Stamp - posting_date = getdate(doc.posting_date) - time = get_time(doc.posting_time) - seconds = time.hour * 60 * 60 + time.minute * 60 + time.second - time_stamp = add_to_date(posting_date, seconds=seconds) - time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + # Time Stamp + posting_date = getdate(doc.posting_date) + time = get_time(doc.posting_time) + seconds = time.hour * 60 * 60 + time.minute * 60 + time.second + time_stamp = add_to_date(posting_date, seconds=seconds) + time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') - tag = bytes([3]).hex() - length = bytes([len(time_stamp)]).hex() - value = time_stamp.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([3]).hex() + length = bytes([len(time_stamp)]).hex() + value = time_stamp.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Invoice Amount - invoice_amount = str(doc.grand_total) - tag = bytes([4]).hex() - length = bytes([len(invoice_amount)]).hex() - value = invoice_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + # Invoice Amount + invoice_amount = str(doc.grand_total) + tag = bytes([4]).hex() + length = bytes([len(invoice_amount)]).hex() + value = invoice_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # VAT Amount - vat_amount = str(doc.total_taxes_and_charges) + # VAT Amount + vat_amount = str(doc.total_taxes_and_charges) - tag = bytes([5]).hex() - length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([5]).hex() + length = bytes([len(vat_amount)]).hex() + value = vat_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Joining bytes into one - tlv_buff = ''.join(tlv_array) + # Joining bytes into one + tlv_buff = ''.join(tlv_array) - # base64 conversion for QR Code - base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() + # base64 conversion for QR Code + base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - qr_image = io.BytesIO() - url = qr_create(base64_string, error='L') - url.png(qr_image, scale=2, quiet_zone=1) + qr_image = io.BytesIO() + url = qr_create(base64_string, error='L') + url.png(qr_image, scale=2, quiet_zone=1) - name = frappe.generate_hash(doc.name, 5) + name = frappe.generate_hash(doc.name, 5) - # making file - filename = f"QRCode-{name}.png".replace(os.path.sep, "__") - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "is_private": 0, - "content": qr_image.getvalue(), - "attached_to_doctype": doc.get("doctype"), - "attached_to_name": doc.get("name"), - "attached_to_field": "qr_code" - }) + # making file + filename = f"QRCode-{name}.png".replace(os.path.sep, "__") + _file = frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "is_private": 0, + "content": qr_image.getvalue(), + "attached_to_doctype": doc.get("doctype"), + "attached_to_name": doc.get("name"), + "attached_to_field": "ksa_einv_qr" + }) - _file.save() + _file.save() - # assigning to document - doc.db_set('qr_code', _file.file_url) - doc.notify_update() - - break + # assigning to document + doc.db_set('ksa_einv_qr', _file.file_url) + doc.notify_update() -def delete_qr_code_file(doc, method): - """Delete QR Code on deleted sales invoice""" - +def delete_qr_code_file(doc, method=None): region = get_region(doc.company) if region not in ['Saudi Arabia']: return - if hasattr(doc, 'qr_code'): - if doc.get('qr_code'): + if hasattr(doc, 'ksa_einv_qr'): + if doc.get('ksa_einv_qr'): file_doc = frappe.get_list('File', { - 'file_url': doc.get('qr_code') + 'file_url': doc.get('ksa_einv_qr') }) if len(file_doc): frappe.delete_doc('File', file_doc[0].name) @@ -143,5 +145,6 @@ def delete_vat_settings_for_company(doc, method): if doc.country != 'Saudi Arabia': return - settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name}) - settings_doc.delete() \ No newline at end of file + settings_doc = frappe.get_all('KSA VAT Setting', filters={'company': doc.name}, pluck='name') + for settings in settings_doc: + frappe.delete_doc('KSA VAT Setting', settings) From ebd4179295d5809afac79077f59edfe2f46f1617 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:15:58 +0530 Subject: [PATCH 149/247] fix: get doctype from doc instead of hardcoding SI --- erpnext/regional/saudi_arabia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 0668f98083..8209115f84 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -11,7 +11,7 @@ from frappe.utils.data import add_to_date, get_time, getdate from erpnext import get_region -def create_qr_code(doc, method): +def create_qr_code(doc, method=None): region = get_region(doc.company) if region not in ['Saudi Arabia']: return @@ -19,7 +19,7 @@ def create_qr_code(doc, method): # if QR Code field not present, create it. Invoices without QR are invalid as per law. if not hasattr(doc, 'ksa_einv_qr'): create_custom_fields({ - 'Sales Invoice': [ + doc.doctype: [ dict( fieldname='ksa_einv_qr', label='QR Code', From 6a6d6f7f8bde1e4fc85c9292a26e4703655177d7 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:17:45 +0530 Subject: [PATCH 150/247] fix: localize QR fieldname --- erpnext/regional/saudi_arabia/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 38a089c632..3afcf53883 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -33,8 +33,8 @@ def make_custom_fields(): custom_fields = { 'Sales Invoice': [ dict( - fieldname='qr_code', - label='QR Code', + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1 ) From f0fee5672f222c1531dd8d8bf26706173fb4720c Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:18:32 +0530 Subject: [PATCH 151/247] chore: change QR field label --- erpnext/regional/saudi_arabia/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 8209115f84..9e77b62399 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -22,7 +22,7 @@ def create_qr_code(doc, method=None): doc.doctype: [ dict( fieldname='ksa_einv_qr', - label='QR Code', + label='KSA E-Invoicing QR', fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1 ) From d06c4b50cd2203ad71525ac424672398deb47b3a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:29:59 +0530 Subject: [PATCH 152/247] feat: create QR field in POS Invoice --- erpnext/regional/saudi_arabia/setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 3afcf53883..850d87441b 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -39,6 +39,14 @@ def make_custom_fields(): read_only=1, no_copy=1, hidden=1 ) ], + 'POS Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ], 'Address': [ dict( fieldname='address_in_arabic', From 0a937dc0509e26d7f88e205902212c3b8987e202 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 13:04:23 +0530 Subject: [PATCH 153/247] fix: show Exit Questionnaire button only to the users with write access - fix linter issues --- erpnext/hr/doctype/exit_interview/exit_interview.js | 2 +- erpnext/hr/doctype/exit_interview/exit_interview.py | 4 ++-- erpnext/hr/doctype/exit_interview/test_exit_interview.py | 1 + .../exit_interview_scheduled/exit_interview_scheduled.py | 3 ++- erpnext/hr/report/employee_exits/employee_exits.py | 2 -- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index 849e8542d2..502af423a2 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Exit Interview', { refresh: function(frm) { - if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent) { + if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) { frm.add_custom_button(__('Send Exit Questionnaire'), function () { frm.trigger('send_exit_questionnaire'); }); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index ba75100a3b..e72c47e8a7 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -28,9 +28,9 @@ class ExitInterview(Document): 'docstatus': ('!=', 2) }) if doc: - frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( + frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format( get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), - title=_('Duplicate Document')) + frappe.DuplicateEntryError) def set_employee_email(self): employee = frappe.get_doc('Employee', self.employee) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index daf3d66290..3a6316c200 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestExitInterview(unittest.TestCase): pass diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py index e1ada61927..5f697c9613 100644 --- a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py @@ -1,4 +1,5 @@ -import frappe +# import frappe + def get_context(context): # do your magic here diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index fd49543d08..93252295f3 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -91,8 +91,6 @@ def get_columns(): ] def get_data(filters): - data = [] - employee = frappe.qb.DocType('Employee') interview = frappe.qb.DocType('Exit Interview') fnf = frappe.qb.DocType('Full and Final Statement') From 848b641d7b2e539c0db06c48fc7a2a083e4c3c3e Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 14:51:05 +0530 Subject: [PATCH 154/247] fix: rename KSA QR field to match localisation --- erpnext/patches/v13_0/rename_ksa_qr_field.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/patches/v13_0/rename_ksa_qr_field.py diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py new file mode 100644 index 0000000000..4946b0d1db --- /dev/null +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, Wahni Green Technologies and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if not company: + return + + if frappe.db.exists('DocType', 'Sales Invoice') and frappe.get_meta('Sales Invoice').has_field('qr_code'): + rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') From 8db21e065c0fceed7a63459ddbc8ee970ecb393b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 14:55:17 +0530 Subject: [PATCH 155/247] test: Exit Interview --- .../exit_interview/exit_interview.json | 14 ++-- .../doctype/exit_interview/exit_interview.py | 6 +- .../exit_interview/test_exit_interview.py | 84 ++++++++++++++++++- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 4b396402db..86720a105b 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -30,7 +30,7 @@ "reference_document_name", "interview_summary_section", "interviewers", - "text_editor_12", + "interview_summary", "employee_status_section", "employee_status", "amended_from" @@ -112,11 +112,6 @@ "fieldtype": "Section Break", "label": "Interview Details" }, - { - "fieldname": "text_editor_12", - "fieldtype": "Text Editor", - "label": "Interview Summary" - }, { "fieldname": "column_break_10", "fieldtype": "Column Break" @@ -213,12 +208,17 @@ "options": "Exit Interview", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "interview_summary", + "fieldtype": "Text Editor", + "label": "Interview Summary" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-05 18:56:34.856854", + "modified": "2021-12-07 14:08:29.355390", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index e72c47e8a7..30e19f1c9b 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -109,7 +109,7 @@ def validate_questionnaire_settings(): if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template: frappe.throw( - message=_('Please set {0} and {1} in {2}.').format( + _('Please set {0} and {1} in {2}.').format( frappe.bold('Exit Questionnaire Web Form'), frappe.bold('Notification Template'), get_link_to_form('HR Settings', 'HR Settings')), @@ -122,8 +122,10 @@ def show_email_summary(email_success, email_failure): if email_success: message += _('{0}: {1}').format( frappe.bold('Sent Successfully'), ', '.join(email_success)) + if message and email_failure: + message += '

' if email_failure: - message += '

' + _('{0} due to missing email information for employee(s): {1}').format( + message += _('{0} due to missing email information for employee(s): {1}').format( frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index 3a6316c200..8eeb4a1b95 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -1,9 +1,89 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.test_webform import create_custom_doctype, create_webform +from frappe.utils import getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire + class TestExitInterview(unittest.TestCase): - pass + def test_duplicate_interview(self): + employee = make_employee('employeeexit1@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + interview = create_exit_interview(employee) + + doc = frappe.copy_doc(interview) + self.assertRaises(frappe.DuplicateEntryError, doc.save) + + def test_relieving_date_validation(self): + employee = make_employee('employeeexit2@example.com') + + interview = create_exit_interview(employee, save=False) + self.assertRaises(frappe.ValidationError, interview.save) + + # set relieving date + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + interview = create_exit_interview(employee) + self.assertTrue(interview.name) + + def test_interview_date_updated_in_employee_master(self): + employee = make_employee('employeeexit3@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + + interview = create_exit_interview(employee) + interview.status = 'Completed' + interview.employee_status = 'Exit Confirmed' + + # exit interview date updated on submit + interview.submit() + self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date) + + # exit interview reset on cancel + interview.reload() + interview.cancel() + self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None) + + def test_send_exit_questionnaire(self): + create_custom_doctype() + create_webform() + + webform = frappe.db.get_all('Web Form', limit=1) + frappe.db.set_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form', webform[0].name) + + employee = make_employee('employeeexit3@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + + interview = create_exit_interview(employee) + send_exit_questionnaire([interview]) + + email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1) + self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message) + + def tearDown(self): + frappe.db.rollback() + + +def create_exit_interview(employee, save=True): + interviewer = create_user('test_interviewer1@example.com') + + doc = frappe.get_doc({ + 'doctype': 'Exit Interview', + 'employee': employee, + 'company': '_Test Company', + 'status': 'Pending', + 'date': getdate(), + 'interviewers': [{ + 'interviewer': interviewer.name + }], + 'interview_summary': 'Test' + }) + + if save: + return doc.insert() + return doc From ef38b127ae0f592a17070a55087804cad8a44ec0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 15:02:00 +0530 Subject: [PATCH 156/247] chore: fix report column widths --- .../report/employee_exits/employee_exits.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 93252295f3..56cea4917b 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -33,13 +33,39 @@ def get_columns(): 'label': _('Date of Joining'), 'fieldname': 'date_of_joining', 'fieldtype': 'Date', - 'width': 100 + 'width': 120 }, { 'label': _('Relieving Date'), 'fieldname': 'relieving_date', 'fieldtype': 'Date', - 'width': 100 + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 150 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 130 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 180 }, { 'label': _('Department'), @@ -61,32 +87,6 @@ def get_columns(): 'fieldtype': 'Link', 'options': 'Employee', 'width': 120 - }, - { - 'label': _('Exit Interview'), - 'fieldname': 'exit_interview', - 'fieldtype': 'Link', - 'options': 'Exit Interview', - 'width': 98 - }, - { - 'label': _('Interview Status'), - 'fieldname': 'interview_status', - 'fieldtype': 'Data', - 'width': 98 - }, - { - 'label': _('Final Decision'), - 'fieldname': 'employee_status', - 'fieldtype': 'Data', - 'width': 98 - }, - { - 'label': _('Full and Final Statement'), - 'fieldname': 'full_and_final_statement', - 'fieldtype': 'Link', - 'options': 'Full and Final Statement', - 'width': 150 } ] From ca230e426761fd80b6528f38b004ebea8260d980 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 15:11:31 +0530 Subject: [PATCH 157/247] test: index test should pass without calling function #28771 test: index test should pass without calling function [skip ci] --- erpnext/stock/doctype/item/test_item.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 8b1224bd3e..4028d93334 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -534,8 +534,6 @@ class TestItem(ERPNextTestCase): def test_index_creation(self): "check if index is getting created in db" - from erpnext.stock.doctype.item.item import on_doctype_update - on_doctype_update() indices = frappe.db.sql("show index from tabItem", as_dict=1) expected_columns = {"item_code", "item_name", "item_group", "route"} From 811e51fb50f013492c14a1a0c7f6e8ca3cab0410 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 15:21:51 +0530 Subject: [PATCH 158/247] fix: reload SI meta before checking --- erpnext/patches/v13_0/rename_ksa_qr_field.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index 4946b0d1db..0bb86e0450 100644 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -10,5 +10,7 @@ def execute(): if not company: return - if frappe.db.exists('DocType', 'Sales Invoice') and frappe.get_meta('Sales Invoice').has_field('qr_code'): - rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') + if frappe.db.exists('DocType', 'Sales Invoice'): + frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) + if frappe.db.has_column('Sales Invoice', 'qr_code'): + rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') From c305ff911f2db58b40463ad069a8e99fcc1b2cd9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 16:22:17 +0530 Subject: [PATCH 159/247] test: Employee Exits Report --- .../report/employee_exits/employee_exits.py | 7 +- .../employee_exits/test_employee_exits.py | 221 ++++++++++++++++++ 2 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/report/employee_exits/test_employee_exits.py diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 56cea4917b..d0e1ef9a32 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -105,8 +105,9 @@ def get_data(filters): employee.department.as_('department'), employee.designation.as_('designation'), employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), - interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') - ).distinct() + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement')) + .distinct() + .orderby(employee.relieving_date) .where( ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) @@ -158,7 +159,7 @@ def get_conditions(filters, query, employee, interview, fnf): query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) if filters.get('fnf_pending'): - query = query.where((fnf.name == '') | (interview.name.isnull())) + query = query.where((fnf.name == '') | (fnf.name.isnull())) return query diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py new file mode 100644 index 0000000000..1c64b46773 --- /dev/null +++ b/erpnext/hr/report/employee_exits/test_employee_exits.py @@ -0,0 +1,221 @@ +import unittest + +import frappe +from frappe.utils import add_days, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview +from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import create_full_and_final_statement +from erpnext.hr.report.employee_exits.employee_exits import execute + + +class TestEmployeeExits(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabEmployee` where company='Test Company'") + frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'") + frappe.db.sql("delete from `tabExit Interview` where company='Test Company'") + + cls.create_records() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + @classmethod + def create_records(cls): + cls.emp1 = make_employee('employeeexit1@example.com', + company='Test Company', + date_of_joining=getdate('01-10-2021'), + relieving_date=add_days(getdate(), 14), + designation='Accountant' + ) + cls.emp2 = make_employee('employeeexit2@example.com', + company='Test Company', + date_of_joining=getdate('01-12-2021'), + relieving_date=add_days(getdate(), 15), + designation='Accountant' + ) + + cls.emp3 = make_employee('employeeexit3@example.com', + company='Test Company', + date_of_joining=getdate('02-12-2021'), + relieving_date=add_days(getdate(), 29), + designation='Engineer' + ) + cls.emp4 = make_employee('employeeexit4@example.com', + company='Test Company', + date_of_joining=getdate('01-12-2021'), + relieving_date=add_days(getdate(), 30), + designation='Engineer' + ) + + # exit interview for 3 employees only + cls.interview1 = create_exit_interview(cls.emp1) + cls.interview2 = create_exit_interview(cls.emp2) + cls.interview3 = create_exit_interview(cls.emp3) + + # create fnf for some records + cls.fnf1 = create_full_and_final_statement(cls.emp1) + cls.fnf2 = create_full_and_final_statement(cls.emp2) + + # link questionnaire for a few records + # setting employee doctype as reference instead of creating a questionnaire + # since this is just for a test + frappe.db.set_value('Exit Interview', cls.interview1.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp1 + }) + + frappe.db.set_value('Exit Interview', cls.interview2.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp2 + }) + + frappe.db.set_value('Exit Interview', cls.interview3.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp3 + }) + + + def test_employee_exits_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 15), + 'designation': 'Accountant' + } + + report = execute(filters) + + employee1 = frappe.get_doc('Employee', self.emp1) + employee2 = frappe.get_doc('Employee', self.emp2) + expected_data = [{ + 'employee': employee1.name, + 'employee_name': employee1.employee_name, + 'date_of_joining': employee1.date_of_joining, + 'relieving_date': employee1.relieving_date, + 'department': employee1.department, + 'designation': employee1.designation, + 'reports_to': None, + 'exit_interview': self.interview1.name, + 'interview_status': self.interview1.status, + 'employee_status': '', + 'questionnaire': employee1.name, + 'full_and_final_statement': self.fnf1.name + }, + { + 'employee': employee2.name, + 'employee_name': employee2.employee_name, + 'date_of_joining': employee2.date_of_joining, + 'relieving_date': employee2.relieving_date, + 'department': employee2.department, + 'designation': employee2.designation, + 'reports_to': None, + 'exit_interview': self.interview2.name, + 'interview_status': self.interview2.status, + 'employee_status': '', + 'questionnaire': employee2.name, + 'full_and_final_statement': self.fnf2.name + }] + + self.assertEqual(expected_data, report[1]) # rows + + + def test_pending_exit_interviews_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 30), + 'exit_interview_pending': 1 + } + + report = execute(filters) + + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows + + def test_pending_exit_questionnaire_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 30), + 'questionnaire_pending': 1 + } + + report = execute(filters) + + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows + + + def test_pending_fnf_summary(self): + filters = { + 'company': 'Test Company', + 'fnf_pending': 1 + } + + report = execute(filters) + + employee3 = frappe.get_doc('Employee', self.emp3) + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee3.name, + 'employee_name': employee3.employee_name, + 'date_of_joining': employee3.date_of_joining, + 'relieving_date': employee3.relieving_date, + 'department': employee3.department, + 'designation': employee3.designation, + 'reports_to': None, + 'exit_interview': self.interview3.name, + 'interview_status': self.interview3.status, + 'employee_status': '', + 'questionnaire': employee3.name, + 'full_and_final_statement': None + }, + { + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows \ No newline at end of file From 6efbbb1058e60bfae96564e2908c441d824a5220 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 16:26:14 +0530 Subject: [PATCH 160/247] fix: ignore mandatory fields while creating WO from SO (#28772) If fields are made mandatory from customizations the WO creation simply fails. --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 1c2482568f..e69e28da92 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -978,6 +978,7 @@ def make_work_orders(items, sales_order, company, project=None): description=i['description'] )).insert() work_order.set_work_order_operations() + work_order.flags.ignore_mandatory = True work_order.save() out.append(work_order) From c5c47e9fadf70b7f9e7072841b5517c886682f20 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:01:02 +0000 Subject: [PATCH 161/247] feat: generate QR Code for POS transactions --- erpnext/hooks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 05c46c50e9..63530ea29f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -265,6 +265,9 @@ doc_events = { "erpnext.regional.india.utils.update_taxable_values" ] }, + "POS Invoice": { + "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"] + }, "Purchase Invoice": { "validate": [ "erpnext.regional.india.utils.validate_reverse_charge_transaction", From 09f178ceeee33390762f8b04c1871914926eb135 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:01:37 +0000 Subject: [PATCH 162/247] fix: disable ksa vat invoice by default --- .../print_format/ksa_vat_invoice/ksa_vat_invoice.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 8e9a72897d..6b64d47453 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -5,19 +5,19 @@ "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}", "custom_format": 1, "default_print_language": "en", - "disabled": 0, + "disabled": 1, "doc_type": "Sales Invoice", "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-29 13:47:37.870818", + "modified": "2021-12-07 13:43:38.018593", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", From 2256155e806e3e37f0e6d6ba3ff85546101d4d27 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:02:28 +0000 Subject: [PATCH 163/247] feat(Regional): patch to disable ksa print formats for other countries --- .../v13_0/disable_ksa_print_format_for_others.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/patches/v13_0/disable_ksa_print_format_for_others.py diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py new file mode 100644 index 0000000000..c7b184dba5 --- /dev/null +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, Wahni Green Technologies and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if company: + return + + if frappe.db.exists('DocType', 'Print Format'): + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 WHERE + name IN ('KSA VAT Invoice', 'KSA POS Invoice')""") From 293d981af988593b4c634c1affda4178ab5211d0 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:03:22 +0000 Subject: [PATCH 164/247] feat(Regional): POS Invoice format for KSA --- .../print_format/ksa_pos_invoice/__init__.py | 0 .../ksa_pos_invoice/ksa_pos_invoice.json | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 erpnext/regional/print_format/ksa_pos_invoice/__init__.py create mode 100644 erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json new file mode 100644 index 0000000000..ab2743232d --- /dev/null +++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json @@ -0,0 +1,32 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-12-07 13:25:05.424827", + "css": "", + "custom_format": 1, + "default_print_language": "en", + "disabled": 1, + "doc_type": "POS Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 0, + "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}
@ {{ item.get_formatted(\"rate\") }}
{{ item.get_formatted(\"amount\") }}
\n\n\t\n\t\t\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% endif %}\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t{%- for row in doc.payments -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t {{ row.mode_of_payment }}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 0.0, + "margin_left": 0.0, + "margin_right": 0.0, + "margin_top": 0.0, + "modified": "2021-12-07 13:43:30.387257", + "modified_by": "Administrator", + "module": "Regional", + "name": "KSA POS Invoice", + "owner": "Administrator", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file From bb3119cd1ff4f9ee34180dfe61835338f0aeb991 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 11:05:41 +0000 Subject: [PATCH 165/247] feat(Regional): enable KSA print formats on setup --- erpnext/regional/saudi_arabia/setup.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 850d87441b..803c79883f 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -3,7 +3,7 @@ import frappe from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats +from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -13,6 +13,18 @@ def setup(company=None, patch=True): add_permissions() make_custom_fields() +def add_print_formats(): + frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") + frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") + frappe.reload_doc("regional", "print_format", "tax_invoice") + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice") + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice") + + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 0 WHERE + name IN ('Simplified Tax Invoice', 'Detailed Tax Invoice', + 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice') + """) + def add_permissions(): """Add Permissions for KSA VAT Setting.""" add_permission('KSA VAT Setting', 'All', 0) From a0aeacee8e03702bc2fdebed61184c91e78dfee9 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 12:39:39 +0000 Subject: [PATCH 166/247] fix: reload doc --- .../v13_0/disable_ksa_print_format_for_others.py | 7 +++++-- erpnext/regional/saudi_arabia/setup.py | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index c7b184dba5..b4080c5577 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -10,5 +10,8 @@ def execute(): return if frappe.db.exists('DocType', 'Print Format'): - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 WHERE - name IN ('KSA VAT Invoice', 'KSA POS Invoice')""") + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 + WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') + """) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 803c79883f..2131edad96 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -14,11 +14,11 @@ def setup(company=None, patch=True): make_custom_fields() def add_print_formats(): - frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") - frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") - frappe.reload_doc("regional", "print_format", "tax_invoice") - frappe.reload_doc("regional", "print_format", "ksa_vat_invoice") - frappe.reload_doc("regional", "print_format", "ksa_pos_invoice") + frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 0 WHERE name IN ('Simplified Tax Invoice', 'Detailed Tax Invoice', From 2df641eb54aeded545eba0e2bd6789685b3fe37f Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 18:13:35 +0530 Subject: [PATCH 167/247] feat(Regional): rename qr field and disable print formats --- erpnext/patches.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ee9060b37d..26fb859e63 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -314,3 +314,5 @@ erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v14_0.migrate_crm_settings +erpnext.patches.v13_0.rename_ksa_qr_field +erpnext.patches.v13_0.disable_ksa_print_format_for_others From 45fce5a64bc8bb56791c238a4638300da0a40656 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 18:29:16 +0530 Subject: [PATCH 168/247] chore: linters --- erpnext/regional/saudi_arabia/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 9e77b62399..c712809cf5 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,12 +1,12 @@ import io import os from base64 import b64encode -from pyqrcode import create as qr_create import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.utils.data import add_to_date, get_time, getdate +from pyqrcode import create as qr_create from erpnext import get_region From 15373f1b2dee16c79a44c269836d553a44316946 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 7 Dec 2021 18:34:34 +0530 Subject: [PATCH 169/247] fix: intendation error --- .../v13_0/disable_ksa_print_format_for_others.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index b4080c5577..4e0c8ec4f1 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -10,8 +10,8 @@ def execute(): return if frappe.db.exists('DocType', 'Print Format'): - frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) - frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 - WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') - """) + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) + frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 + WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') + """) From cb21dff882eb68edfe8667de69feddeea5e001e7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Dec 2021 18:44:05 +0530 Subject: [PATCH 170/247] fix: Error on creating invoice (cherry picked from commit e11515a3561eac6d33d21190cac2db112ae301fb) --- erpnext/regional/india/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index bbca77273f..9743c3b547 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -217,6 +217,7 @@ def get_regional_address_details(party_details, doctype, company): return if not party_details.place_of_supply: return party_details + if not party_details.company_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", From dcbf0c9eca1ca811a69eb318a0365a38054ac2cc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 23:40:10 +0530 Subject: [PATCH 171/247] fix: tests and sider issues --- .../exit_interview/exit_interview.json | 6 +- .../exit_interview/test_exit_interview.py | 30 +++- .../employee_exits/test_employee_exits.py | 145 ++++++++++-------- 3 files changed, 115 insertions(+), 66 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 86720a105b..989a1b8118 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -120,8 +120,8 @@ "fieldname": "interviewers", "fieldtype": "Table MultiSelect", "label": "Interviewers", - "options": "Interviewer", - "reqd": 1 + "mandatory_depends_on": "eval:doc.status==='Scheduled';", + "options": "Interviewer" }, { "fetch_from": "employee.date_of_joining", @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-07 14:08:29.355390", + "modified": "2021-12-07 23:39:22.645401", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index 8eeb4a1b95..b31d593a2d 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.tests.test_webform import create_custom_doctype, create_webform from frappe.utils import getdate @@ -13,6 +14,9 @@ from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionn class TestExitInterview(unittest.TestCase): + def setUp(self): + frappe.db.sql('delete from `tabExit Interview`') + def test_duplicate_interview(self): employee = make_employee('employeeexit1@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) @@ -23,6 +27,8 @@ class TestExitInterview(unittest.TestCase): def test_relieving_date_validation(self): employee = make_employee('employeeexit2@example.com') + # unset relieving date + frappe.db.set_value('Employee', employee, 'relieving_date', None) interview = create_exit_interview(employee, save=False) self.assertRaises(frappe.ValidationError, interview.save) @@ -52,9 +58,13 @@ class TestExitInterview(unittest.TestCase): def test_send_exit_questionnaire(self): create_custom_doctype() create_webform() + template = create_notification_template() webform = frappe.db.get_all('Web Form', limit=1) - frappe.db.set_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form', webform[0].name) + frappe.db.set_value('HR Settings', 'HR Settings', { + 'exit_questionnaire_web_form': webform[0].name, + 'exit_questionnaire_notification_template': template + }) employee = make_employee('employeeexit3@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) @@ -87,3 +97,21 @@ def create_exit_interview(employee, save=True): if save: return doc.insert() return doc + + +def create_notification_template(): + template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification')) + if not template: + base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') + response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + + template = frappe.get_doc({ + 'doctype': 'Email Template', + 'name': _('Exit Questionnaire Notification'), + 'response': response, + 'subject': _('Exit Questionnaire Notification'), + 'owner': frappe.session.user, + }).insert(ignore_permissions=True) + template = template.name + + return template \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py index 1c64b46773..d7e95a60d0 100644 --- a/erpnext/hr/report/employee_exits/test_employee_exits.py +++ b/erpnext/hr/report/employee_exits/test_employee_exits.py @@ -5,13 +5,16 @@ from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview -from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import create_full_and_final_statement +from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import ( + create_full_and_final_statement, +) from erpnext.hr.report.employee_exits.employee_exits import execute class TestEmployeeExits(unittest.TestCase): @classmethod def setUpClass(cls): + create_company() frappe.db.sql("delete from `tabEmployee` where company='Test Company'") frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'") frappe.db.sql("delete from `tabExit Interview` where company='Test Company'") @@ -24,26 +27,30 @@ class TestEmployeeExits(unittest.TestCase): @classmethod def create_records(cls): - cls.emp1 = make_employee('employeeexit1@example.com', + cls.emp1 = make_employee( + 'employeeexit1@example.com', company='Test Company', date_of_joining=getdate('01-10-2021'), relieving_date=add_days(getdate(), 14), designation='Accountant' ) - cls.emp2 = make_employee('employeeexit2@example.com', + cls.emp2 = make_employee( + 'employeeexit2@example.com', company='Test Company', date_of_joining=getdate('01-12-2021'), relieving_date=add_days(getdate(), 15), designation='Accountant' ) - cls.emp3 = make_employee('employeeexit3@example.com', + cls.emp3 = make_employee( + 'employeeexit3@example.com', company='Test Company', date_of_joining=getdate('02-12-2021'), relieving_date=add_days(getdate(), 29), designation='Engineer' ) - cls.emp4 = make_employee('employeeexit4@example.com', + cls.emp4 = make_employee( + 'employeeexit4@example.com', company='Test Company', date_of_joining=getdate('01-12-2021'), relieving_date=add_days(getdate(), 30), @@ -90,34 +97,36 @@ class TestEmployeeExits(unittest.TestCase): employee1 = frappe.get_doc('Employee', self.emp1) employee2 = frappe.get_doc('Employee', self.emp2) - expected_data = [{ - 'employee': employee1.name, - 'employee_name': employee1.employee_name, - 'date_of_joining': employee1.date_of_joining, - 'relieving_date': employee1.relieving_date, - 'department': employee1.department, - 'designation': employee1.designation, - 'reports_to': None, - 'exit_interview': self.interview1.name, - 'interview_status': self.interview1.status, - 'employee_status': '', - 'questionnaire': employee1.name, - 'full_and_final_statement': self.fnf1.name - }, - { - 'employee': employee2.name, - 'employee_name': employee2.employee_name, - 'date_of_joining': employee2.date_of_joining, - 'relieving_date': employee2.relieving_date, - 'department': employee2.department, - 'designation': employee2.designation, - 'reports_to': None, - 'exit_interview': self.interview2.name, - 'interview_status': self.interview2.status, - 'employee_status': '', - 'questionnaire': employee2.name, - 'full_and_final_statement': self.fnf2.name - }] + expected_data = [ + { + 'employee': employee1.name, + 'employee_name': employee1.employee_name, + 'date_of_joining': employee1.date_of_joining, + 'relieving_date': employee1.relieving_date, + 'department': employee1.department, + 'designation': employee1.designation, + 'reports_to': None, + 'exit_interview': self.interview1.name, + 'interview_status': self.interview1.status, + 'employee_status': '', + 'questionnaire': employee1.name, + 'full_and_final_statement': self.fnf1.name + }, + { + 'employee': employee2.name, + 'employee_name': employee2.employee_name, + 'date_of_joining': employee2.date_of_joining, + 'relieving_date': employee2.relieving_date, + 'department': employee2.department, + 'designation': employee2.designation, + 'reports_to': None, + 'exit_interview': self.interview2.name, + 'interview_status': self.interview2.status, + 'employee_status': '', + 'questionnaire': employee2.name, + 'full_and_final_statement': self.fnf2.name + } + ] self.assertEqual(expected_data, report[1]) # rows @@ -189,33 +198,45 @@ class TestEmployeeExits(unittest.TestCase): employee3 = frappe.get_doc('Employee', self.emp3) employee4 = frappe.get_doc('Employee', self.emp4) - expected_data = [{ - 'employee': employee3.name, - 'employee_name': employee3.employee_name, - 'date_of_joining': employee3.date_of_joining, - 'relieving_date': employee3.relieving_date, - 'department': employee3.department, - 'designation': employee3.designation, - 'reports_to': None, - 'exit_interview': self.interview3.name, - 'interview_status': self.interview3.status, - 'employee_status': '', - 'questionnaire': employee3.name, - 'full_and_final_statement': None - }, - { - 'employee': employee4.name, - 'employee_name': employee4.employee_name, - 'date_of_joining': employee4.date_of_joining, - 'relieving_date': employee4.relieving_date, - 'department': employee4.department, - 'designation': employee4.designation, - 'reports_to': None, - 'exit_interview': None, - 'interview_status': None, - 'employee_status': None, - 'questionnaire': None, - 'full_and_final_statement': None - }] + expected_data = [ + { + 'employee': employee3.name, + 'employee_name': employee3.employee_name, + 'date_of_joining': employee3.date_of_joining, + 'relieving_date': employee3.relieving_date, + 'department': employee3.department, + 'designation': employee3.designation, + 'reports_to': None, + 'exit_interview': self.interview3.name, + 'interview_status': self.interview3.status, + 'employee_status': '', + 'questionnaire': employee3.name, + 'full_and_final_statement': None + }, + { + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + } + ] - self.assertEqual(expected_data, report[1]) # rows \ No newline at end of file + self.assertEqual(expected_data, report[1]) # rows + + +def create_company(): + if not frappe.db.exists('Company', 'Test Company'): + frappe.get_doc({ + 'doctype': 'Company', + 'company_name': 'Test Company', + 'default_currency': 'INR', + 'country': 'India' + }).insert() \ No newline at end of file From ca8509472859fd06a4742c465c23212836046354 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 07:52:06 +0530 Subject: [PATCH 172/247] chore: switch to frappe.db.set_value --- erpnext/regional/saudi_arabia/setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 2131edad96..2e31c03d5c 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -20,10 +20,8 @@ def add_print_formats(): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 0 WHERE - name IN ('Simplified Tax Invoice', 'Detailed Tax Invoice', - 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice') - """) + for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'): + frappe.db.set_value("Print Format", d, "disabled", 0) def add_permissions(): """Add Permissions for KSA VAT Setting.""" From 5b20746311f97bd3872d7314c10108634f76d90d Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 07:53:45 +0530 Subject: [PATCH 173/247] chore: switch to ORM method --- erpnext/patches/v13_0/disable_ksa_print_format_for_others.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index 4e0c8ec4f1..c815b3bb3c 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -12,6 +12,5 @@ def execute(): if frappe.db.exists('DocType', 'Print Format'): frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - frappe.db.sql("""UPDATE`tabPrint Format` SET disabled = 1 - WHERE name IN ('KSA VAT Invoice', 'KSA POS Invoice') - """) + for d in ('KSA VAT Invoice', 'KSA POS Invoice'): + frappe.db.set_value("Print Format", d, "disabled", 1) From 3e97492da49a4e51e93db58a81b6ecdf48d61220 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 08:03:30 +0530 Subject: [PATCH 174/247] fix: replace frappe.get_all --- erpnext/regional/saudi_arabia/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index c712809cf5..a03c3f0994 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -141,10 +141,9 @@ def delete_qr_code_file(doc, method=None): if len(file_doc): frappe.delete_doc('File', file_doc[0].name) -def delete_vat_settings_for_company(doc, method): +def delete_vat_settings_for_company(doc, method=None): if doc.country != 'Saudi Arabia': return - settings_doc = frappe.get_all('KSA VAT Setting', filters={'company': doc.name}, pluck='name') - for settings in settings_doc: - frappe.delete_doc('KSA VAT Setting', settings) + if frappe.db.exists('KSA VAT Setting', doc.name): + frappe.delete_doc('KSA VAT Setting', doc.name) From 4a316550f4493adf7f5166d2f6717f5d2d9f31b3 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 8 Dec 2021 07:25:41 +0000 Subject: [PATCH 175/247] chore: POS invoice format changes --- .../print_format/ksa_pos_invoice/ksa_pos_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json index ab2743232d..c2a309231d 100644 --- a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json +++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json @@ -10,14 +10,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 0, - "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}
@ {{ item.get_formatted(\"rate\") }}
{{ item.get_formatted(\"amount\") }}
\n\n\t\n\t\t\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% endif %}\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t{%- for row in doc.payments -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t {{ row.mode_of_payment }}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", + "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}{{ item.get_formatted(\"net_amount\") }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", "idx": 0, "line_breaks": 0, "margin_bottom": 0.0, "margin_left": 0.0, "margin_right": 0.0, "margin_top": 0.0, - "modified": "2021-12-07 13:43:30.387257", + "modified": "2021-12-08 10:25:01.930885", "modified_by": "Administrator", "module": "Regional", "name": "KSA POS Invoice", From d5380dd716f07390aefb13fadf83bc172a989efa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 8 Dec 2021 14:06:34 +0530 Subject: [PATCH 176/247] fix: Error on Invoice generation (cherry picked from commit 82255293c4eb8a9f586db083d19d63c1ea435fb9) --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 9743c3b547..fd3ec3c08c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -213,7 +213,7 @@ def get_regional_address_details(party_details, doctype, company): tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) if tax_template_by_category: - party_details.get['taxes_and_charges'] = tax_template_by_category + party_details['taxes_and_charges'] = tax_template_by_category return if not party_details.place_of_supply: return party_details From 90b98440e241fe03270bc000ac1c910ea67261a9 Mon Sep 17 00:00:00 2001 From: aaronmenezes Date: Wed, 8 Dec 2021 14:48:19 +0530 Subject: [PATCH 177/247] fix: Maintenence Visit -Purpose (item ) tables is not visible on submitted or saved entries (#28792) --- .../doctype/maintenance_visit/maintenance_visit.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 7f983541f6..6f6ca61ebc 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', { } }); } - else { - frm.clear_table("purposes"); - } - if (!frm.doc.status) { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { + frm.clear_table("purposes"); frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, From 1cbeba5f1de245fca7e01b73875b4f0b61bcf773 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 2 Jul 2021 15:28:41 +0530 Subject: [PATCH 178/247] test: check execution of illegal stock entry seq --- .../doctype/stock_entry/test_stock_entry.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5ef07705d8..c46f052ac6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -928,6 +928,50 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + def test_future_negative_sle(self): + # Initialize item, batch, warehouse, opening qty + is_allow_neg = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') + frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', 0) + + item_code = '_Test Future Neg Item' + batch_no = '_Test Future Neg Batch' + warehouses = [ + '_Test Future Neg Warehouse Source', + '_Test Future Neg Warehouse Destination' + ] + warehouse_names = initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, + opening_qty=2, posting_date='2021-07-01' + ) + + # Executing an illegal sequence should raise an error + sequence_of_entries = [ + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-03', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[1], + to_warehouse=warehouse_names[0], + batch_no=batch_no, + posting_date='2021-07-04', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-02', # Illegal SE + purpose='Material Transfer') + ] + + self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) + frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', is_allow_neg) + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -998,3 +1042,31 @@ def get_multiple_items(): ] test_records = frappe.get_test_records('Stock Entry') + +def initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, opening_qty, posting_date): + from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + TestBatch.make_batch_item(item_code) + make_new_batch(item_code=item_code, batch_id=batch_no) + warehouse_names = [create_warehouse(w) for w in warehouses] + create_stock_reconciliation( + purpose='Opening Stock', + posting_date=posting_date, + posting_time='20:00:20', + item_code=item_code, + warehouse=warehouse_names[0], + valuation_rate=100, + qty=opening_qty, + batch_no=batch_no, + ) + return warehouse_names + + +def create_stock_entries(sequence_of_entries): + for entry_detail in sequence_of_entries: + make_stock_entry(**entry_detail) From 5eba57528ce0792f382ac30af99cbbb63b07c77e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 23:03:52 +0530 Subject: [PATCH 179/247] fix: check future negative stock for batches batch's ledger is only maintained in form of `actual_qty` on batch's SLEs. To validate if batch has any negative qty in future, cumulative total of `actual_qty` is required to ensure it never goes negative. --- erpnext/stock/stock_ledger.py | 62 +++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d78632a0f3..e95c0fcd23 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1089,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): allow_negative_stock = cint(allow_negative_stock) \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: - sle = get_future_sle_with_negative_qty(args) - if sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - sle[0]["posting_date"], sle[0]["posting_time"], - frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) + if allow_negative_stock: + return + if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): + return + + neg_sle = get_future_sle_with_negative_qty(args) + if neg_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_sle[0]["qty_after_transaction"]), + frappe.get_desk_link('Item', args.item_code), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) + + frappe.throw(message, NegativeStockError, title='Insufficient Stock') + + + if not args.batch_no: + return + + neg_batch_sle = get_future_sle_with_negative_batch_qty(args) + if neg_batch_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_batch_sle[0]["cumulative_total"]), + frappe.get_desk_link('Batch', args.batch_no), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) + frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") - frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): return frappe.db.sql(""" @@ -1118,6 +1137,29 @@ def get_future_sle_with_negative_qty(args): limit 1 """, args, as_dict=1) + +def get_future_sle_with_negative_batch_qty(args): + return frappe.db.sql(""" + with batch_ledger as ( + select + posting_date, posting_time, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no=%(batch_no)s + and is_cancelled = 0 + order by posting_date, posting_time, creation + ) + select * from batch_ledger + where + cumulative_total < 0.0 + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + limit 1 + """, args, as_dict=1) + + def _round_off_if_near_zero(number: float, precision: int = 6) -> float: """ Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 6. From 9c90b7a40da25962a8c2eabe94c5e46ed5522a6f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Dec 2021 23:06:36 +0530 Subject: [PATCH 180/247] refactor: remove redundant batch qty validation This check was only checking total sum, which is problamatic when making backdated entries that can cause intermediate values to go negative while overall values stay positive. --- .../stock_ledger_entry/stock_ledger_entry.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 93bca7a694..c53830799d 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -8,7 +8,7 @@ import frappe from frappe import _ from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock @@ -43,7 +43,6 @@ class StockLedgerEntry(Document): def on_submit(self): self.check_stock_frozen_date() - self.actual_amt_check() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): @@ -57,18 +56,6 @@ class StockLedgerEntry(Document): "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) - def actual_amt_check(self): - """Validate that qty at warehouse for selected batch is >=0""" - if self.batch_no and not self.get("allow_negative_stock"): - batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", - (self.warehouse, self.item_code, self.batch_no))[0][0]) - - if batch_bal_after_transaction < 0: - frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}") - .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse)) - def validate_mandatory(self): mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] for k in mandatory: From f0152d03a4cdc7635271f617efdc271864f0fad7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 8 Dec 2021 10:40:39 +0530 Subject: [PATCH 181/247] test: simplfy test and expect specific exception --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c46f052ac6..654fbabeb6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -24,7 +24,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle +from erpnext.tests.utils import change_settings def get_sle(**args): @@ -928,11 +929,9 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty - is_allow_neg = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') - frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', 0) - item_code = '_Test Future Neg Item' batch_no = '_Test Future Neg Batch' warehouses = [ @@ -969,8 +968,7 @@ class TestStockEntry(unittest.TestCase): purpose='Material Transfer') ] - self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries) - frappe.db.set_value('Stock Settings', 'Stock Settings', 'allow_negative_stock', is_allow_neg) + self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) def make_serialized_item(**args): args = frappe._dict(args) From 96a019ec490968694af4b63d44f32be7605118ef Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 8 Dec 2021 13:06:33 +0530 Subject: [PATCH 182/247] test: add multi-batch negative qty test --- .../doctype/stock_entry/test_stock_entry.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 654fbabeb6..5a9e77e325 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -25,7 +25,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle -from erpnext.tests.utils import change_settings +from erpnext.tests.utils import ERPNextTestCase, change_settings def get_sle(**args): @@ -39,7 +39,7 @@ def get_sle(**args): order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, values, as_dict=1) -class TestStockEntry(unittest.TestCase): +class TestStockEntry(ERPNextTestCase): def tearDown(self): frappe.set_user("Administrator") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") @@ -970,6 +970,42 @@ class TestStockEntry(unittest.TestCase): self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_future_negative_sle_batch(self): + from erpnext.stock.doctype.batch.test_batch import TestBatch + + # Initialize item, batch, warehouse, opening qty + item_code = '_Test MultiBatch Item' + TestBatch.make_batch_item(item_code) + + batch_nos = [] # store generate batches + warehouse = '_Test Warehouse - _TC' + + se1 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-01', + purpose='Material Receipt' + ) + batch_nos.append(se1.items[0].batch_no) + se2 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-03', + purpose='Material Receipt' + ) + batch_nos.append(se2.items[0].batch_no) + + with self.assertRaises(NegativeStockError) as nse: + make_stock_entry(item_code=item_code, + qty=1, + from_warehouse=warehouse, + batch_no=batch_nos[1], + posting_date='2021-09-02', # backdated consumption of 2nd batch + purpose='Material Issue') + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From f043f59324f666571b544d4b4eb8291b1ce6b3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=B6ller?= Date: Thu, 9 Dec 2021 07:01:25 +0100 Subject: [PATCH 183/247] fix: wrong german translation of abbreviation PAN Wrong german translation of abbreviation: PAN --- erpnext/translations/de.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index ca03a787cd..d46ffb5609 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1847,7 +1847,7 @@ Overdue,Überfällig, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, Owner,Besitzer, -PAN,PFANNE, +PAN,PAN, POS,Verkaufsstelle, POS Profile,Verkaufsstellen-Profil, POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden", From c64d5028b41d3aa0d1bbbf49eb6311735a386325 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 9 Dec 2021 13:45:44 +0530 Subject: [PATCH 184/247] fix: deduplicate after finishing the repost (#28803) Not really a bug but avoids potential of prematurely skipping something if failure occurs and failure isn't resolved. --- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 01cceb176b..b2ad07f9c3 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -168,8 +168,8 @@ def repost_entries(): for row in riv_entries: doc = frappe.get_doc('Repost Item Valuation', row.name) if doc.status in ('Queued', 'In Progress'): - doc.deduplicate_similar_repost() repost(doc) + doc.deduplicate_similar_repost() riv_entries = get_repost_item_valuation_entries() if riv_entries: From 6485ac4e596dced1a0c5bfc2fc7f9fb920d3b6d8 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 9 Dec 2021 17:04:00 +0530 Subject: [PATCH 185/247] fix: misleading "Set Default X" fields after saving (#28798) * fix: misleading "Set Default X" fields after saving * refactor: remove unncessary code and minor formatting * fix: extend to more doctypes and correct fieldnames Co-authored-by: Ankush Menat --- .../purchase_invoice/purchase_invoice.py | 3 +++ .../doctype/sales_invoice/sales_invoice.py | 2 ++ .../doctype/purchase_order/purchase_order.py | 1 + .../tests/test_transaction_base.py | 22 +++++++++++++++++++ .../doctype/sales_order/sales_order.py | 2 ++ .../doctype/delivery_note/delivery_note.py | 1 + .../material_request/material_request.py | 3 +++ .../purchase_receipt/purchase_receipt.py | 4 ++++ .../stock/doctype/stock_entry/stock_entry.py | 2 ++ erpnext/utilities/transaction_base.py | 22 +++++++++++++++++++ 10 files changed, 62 insertions(+) create mode 100644 erpnext/controllers/tests/test_transaction_base.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 48b5cb9459..f4fd1bf16e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController): self.set_status() self.validate_purchase_receipt_if_update_stock() validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c4d59f1ef7..64712b550f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -155,6 +155,8 @@ class SalesInvoice(SellingController): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: validate_loyalty_points(self, self.loyalty_points) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5eab21bd9d..1b5f35efbb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController): self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference) + self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): super(PurchaseOrder, self).validate_with_previous_doc({ diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py new file mode 100644 index 0000000000..13aa697610 --- /dev/null +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -0,0 +1,22 @@ +import unittest + +import frappe + + +class TestUtils(unittest.TestCase): + def test_reset_default_field_value(self): + doc = frappe.get_doc({ + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + }) + + # Same values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, "Warehouse 1") + + # Mixed values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, None) + diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e69e28da92..cc951850a4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -63,6 +63,8 @@ class SalesOrder(SellingController): if not self.billing_status: self.billing_status = 'Not Billed' if not self.delivery_status: self.delivery_status = 'Not Delivered' + self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_po(self): # validate p.o date v/s delivery date if self.po_date and not self.skip_delivery_note: diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 52684607b4..70d48a42d7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,6 +138,7 @@ class DeliveryNote(SellingController): self.update_current_stock() if not self.installation_status: self.installation_status = 'Not Installed' + self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): super(DeliveryNote, self).validate_with_previous_doc({ diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index d717c50919..103e8d6a88 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -80,6 +80,9 @@ class MaterialRequest(BuyingController): # NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated # Though the creation of Material Request from a Production Plan can be rethought to fix this + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def set_title(self): '''Set title as comma separated list of items''' if not self.title: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 762f45f75f..c97b306c4e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -118,6 +118,10 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_cwip_accounts(self): for item in self.get('items'): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a38dfa5062..a00d63e6f2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -103,6 +103,8 @@ class StockEntry(StockController): self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() + self.reset_default_field_value("from_warehouse", "items", "s_warehouse") + self.reset_default_field_value("to_warehouse", "items", "t_warehouse") def on_submit(self): self.update_stock_ledger() diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 14b3afa5a7..1d8b3a8db6 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -162,6 +162,28 @@ class TransactionBase(StatusUpdater): return ret + def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str): + """ Reset "Set default X" fields on forms to avoid confusion. + + example: + doc = { + "set_from_warehouse": "Warehouse A", + "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}], + } + Since this has dissimilar values in child table, the default field will be erased. + + doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + """ + child_table_values = set() + + for row in self.get(child_table): + child_table_values.add(row.get(child_table_field)) + + if len(child_table_values) > 1: + self.set(default_field, None) + else: + self.set(default_field, list(child_table_values)[0]) + def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT distinct `tabEvent`.name From f1c0190f02858b3c1368e1a084a5fbf8c889beb8 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 9 Dec 2021 20:21:17 +0530 Subject: [PATCH 186/247] feat: added QI link in Job Card Dashboard (#28643) --- .../manufacturing/doctype/job_card/job_card_dashboard.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py index acaa895c1a..2c488721b0 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py +++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py @@ -4,10 +4,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'job_card', + 'non_standard_fieldnames': { + 'Quality Inspection': 'reference_name' + }, 'transactions': [ { 'label': _('Transactions'), 'items': ['Material Request', 'Stock Entry'] + }, + { + 'label': _('Reference'), + 'items': ['Quality Inspection'] } ] } From d37541d3fb57923b681753871bfd975bf53b630f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 12:04:10 +0530 Subject: [PATCH 187/247] fix: ensure that reposting is finished before freezing stock/account --- .../accounts_settings/accounts_settings.py | 8 ++++++ erpnext/public/js/utils.js | 4 +++ .../doctype/stock_settings/stock_settings.py | 8 ++++++ erpnext/stock/utils.py | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 745191712b..4839207410 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ from frappe.model.document import Document from frappe.utils import cint +from erpnext.stock.utils import check_pending_reposting + class AccountsSettings(Document): def on_update(self): @@ -25,6 +27,7 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() self.toggle_discount_accounting_fields() + self.validate_pending_reposts() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -56,3 +59,8 @@ class AccountsSettings(Document): make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + + + def validate_pending_reposts(self): + if self.acc_frozen_upto: + check_pending_reposting(self.acc_frozen_upto) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f0facdd3a1..cad1659561 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -84,6 +84,10 @@ $.extend(erpnext, { }); }, + route_to_pending_reposts: (args) => { + frappe.set_route('List', 'Repost Item Valuation', args); + }, + proceed_save_with_reminders_frequency_change: () => { frappe.ui.hide_open_dialog(); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 1de48b6f1f..c1293cbf0f 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -11,6 +11,8 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.utils.html_utils import clean_html +from erpnext.stock.utils import check_pending_reposting + class StockSettings(Document): def validate(self): @@ -36,6 +38,7 @@ class StockSettings(Document): self.validate_warehouses() self.cant_change_valuation_method() self.validate_clean_description_html() + self.validate_pending_reposts() def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] @@ -64,6 +67,11 @@ class StockSettings(Document): # changed to text frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + def validate_pending_reposts(self): + if self.stock_frozen_upto: + check_pending_reposting(self.stock_frozen_upto) + + def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 72d8098d44..b00dbad476 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -417,3 +417,28 @@ def is_reposting_item_valuation_in_progress(): {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) if reposting_in_progress: frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) + +def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: + """Check if there are pending reposting job till the specified posting date.""" + + filters = { + "docstatus": 1, + "status": ["in", ["Queued","In Progress", "Failed"]], + "posting_date": ["<=", posting_date], + } + + reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + if reposting_pending and throw_error: + msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") + frappe.msgprint(msg, + raise_exception=frappe.ValidationError, + title="Stock Reposting Ongoing", + indicator="red", + primary_action={ + "label": _("Show pending entries"), + "client_action": "erpnext.route_to_pending_reposts", + "args": filters, + } + ) + + return bool(reposting_pending) From 75bc404cbea8ab0713a4f64e382d35558c453eed Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 12:39:38 +0530 Subject: [PATCH 188/247] test: stock frozen validation --- .../test_repost_item_valuation.py | 24 +++++++++++++++++++ erpnext/stock/utils.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index de793163fd..78b432d564 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -4,12 +4,14 @@ import unittest import frappe +from frappe.utils import nowdate from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) +from erpnext.stock.utils import PendingRepostingError class TestRepostItemValuation(unittest.TestCase): @@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase): # to avoid breaking other tests accidentaly riv4.set_status("Skipped") riv3.set_status("Skipped") + + def test_stock_freeze_validation(self): + + today = nowdate() + + riv = frappe.get_doc( + doctype="Repost Item Valuation", + item_code="_Test Item", + warehouse="_Test Warehouse - _TC", + based_on="Item and Warehouse", + posting_date=today, + posting_time="00:01:00", + ) + riv.flags.dont_run_in_test = True # keep it queued + riv.submit() + + stock_settings = frappe.get_doc("Stock Settings") + stock_settings.stock_frozen_upto = today + + self.assertRaises(PendingRepostingError, stock_settings.save) + + riv.set_status("Skipped") diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index b00dbad476..3b1ae3b43c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,6 +12,7 @@ import erpnext class InvalidWarehouseCompany(frappe.ValidationError): pass +class PendingRepostingError(frappe.ValidationError): pass def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -431,7 +432,7 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool if reposting_pending and throw_error: msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") frappe.msgprint(msg, - raise_exception=frappe.ValidationError, + raise_exception=PendingRepostingError, title="Stock Reposting Ongoing", indicator="red", primary_action={ From ffa3d45c42649c7cf4db4c12f67b2181901f0f05 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 14:42:38 +0530 Subject: [PATCH 189/247] chore: update stale rules and apply on issues [skip ci] --- .github/stale.yml | 56 ++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 9322ae87bf..8b7cb9be3e 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,34 +1,36 @@ # Configuration for probot-stale - https://github.com/probot/stale -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 15 - -# Number of days of inactivity before a stale Issue or Pull Request is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 3 - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - hotfix - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: true - # Label to use when marking as stale staleLabel: inactive -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This pull request has been automatically marked as stale because it has not had - recent activity. It will be closed within a week if no further activity occurs, but it - only takes a comment to keep a contribution alive :) Also, even if it is closed, - you can always reopen the PR when you're ready. Thank you for contributing. - # Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 +limitPerRun: 10 -# Limit to only `issues` or `pulls` -only: pulls +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +pulls: + daysUntilStale: 15 + daysUntilClose: 3 + exemptLabels: + - hotfix + markComment: > + This pull request has been automatically marked as inactive because it has + not had recent activity. It will be closed within 3 days if no further + activity occurs, but it only takes a comment to keep a contribution alive + :) Also, even if it is closed, you can always reopen the PR when you're + ready. Thank you for contributing. + +issues: + daysUntilStale: 60 + daysUntilClose: 7 + exemptLabels: + - valid + - to-validate + markComment: > + This issue has been automatically marked as inactive because it has not had + recent activity and it wasn't validated by maintainer team. It will be + closed within a week if no further activity occurs. From bf48f176003e2ab6d43de450d5831d941be44caf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 15:58:56 +0530 Subject: [PATCH 190/247] chore: new and improved bug report form [skip ci] --- .github/ISSUE_TEMPLATE/bug_report.md | 47 ----------- .github/ISSUE_TEMPLATE/bug_report.yaml | 106 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 47 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c145291b57..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: Bug report -about: Report a bug encountered while using ERPNext -labels: bug ---- - - - -## Description of the issue - -## Context information (for bug reports) - -**Output of `bench version`** -``` -(paste here) -``` - -## Steps to reproduce the issue - -1. -2. -3. - -### Observed result - -### Expected result - -### Stacktrace / full error message - -``` -(paste here) -``` - -## Additional information - -OS version / distribution, `ERPNext` install method, etc. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000..df8fcc2989 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,106 @@ +name: Bug Report +description: Report a bug encountered while using ERPNext +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: + + 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext + - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com) + - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly. + 2. When making a bug report, make sure you provide all required information. The easier it is for + maintainers to reproduce, the faster it'll be fixed. + 3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉 + + - type: textarea + id: bug-info + attributes: + label: Information about bug + description: Also tell us, what did you expect to happen? + placeholder: Please provide as much information as possible. + validations: + required: true + + - type: dropdown + id: version + attributes: + label: Version + description: Affected versions. + multiple: true + options: + - v12 + - v13 + - v14 + - develop + validations: + required: true + + - type: dropdown + id: module + attributes: + label: Module + description: Select affected module of ERPNext. + multiple: true + options: + - accounts + - stock + - buying + - selling + - ecommerce + - manufacturing + - HR + - projects + - support + - assets + - integrations + - quality + - regional + - portal + - agriculture + - education + - non-profit + validations: + required: true + + - type: textarea + id: exact-version + attributes: + label: Version + description: Share exact version number of Frappe and ERPNext you are using. + placeholder: | + Frappe version - + ERPNext Verion - + validations: + required: true + + - type: Installation method + id: install-method + attributes: + label: Module + options: + - docker + - easy-install + - manual install + - FrappeCloud + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output / Stack trace / Full Error Message. + description: Please copy and paste any relevant log output. This will be automatically formatted. + render: shell + + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true From fcb614d1b763bb950b9c133d0c05f392c3f11b45 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 18:22:55 +0530 Subject: [PATCH 191/247] chore: correct form format for issues --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index df8fcc2989..a6e16a03d8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -76,10 +76,10 @@ body: validations: required: true - - type: Installation method + - type: dropdown id: install-method attributes: - label: Module + label: Installation method options: - docker - easy-install From fe597cdd3bb663e81e81be2ba9c4fb708294a0d4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 18:33:21 +0530 Subject: [PATCH 192/247] chore: remove unused issue tempaltes [skip ci] --- .github/ISSUE_TEMPLATE/feature_request.md | 3 +++ .../question-about-using-erpnext.md | 17 ----------------- 2 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/question-about-using-erpnext.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6cdad356cd..418bf3c941 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,10 @@ --- name: Feature request about: Suggest an idea to improve ERPNext +title: '' labels: feature-request +assignees: '' + ---