From ab4a96dadd530211c253ad99425dc0a86f5f4cc2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Feb 2022 13:07:51 +0530 Subject: [PATCH 001/136] fix: Multiple fixes in Gross Profit report (cherry picked from commit da73685f7172290151a279f8cf796628dbf6617e) --- .../report/gross_profit/gross_profit.js | 10 +++-- .../report/gross_profit/gross_profit.py | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 2ba649da07f..158ff4d3437 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 }, { "fieldname":"from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_start_date") + "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_end_date") + "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname":"sales_invoice", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index a8b5a0e28bd..0af7ca9c8bd 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -371,20 +371,37 @@ class GrossProfitGenerator(object): return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' - if row.project: - condition += " AND a.project=%s" % (frappe.db.escape(row.project)) - elif row.cost_center: - condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) - if self.filters.to_date: - condition += " AND modified='%s'" % (self.filters.to_date) + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") - last_purchase_rate = frappe.db.sql(""" - select (a.base_rate / a.conversion_factor) - from `tabPurchase Invoice Item` a - where a.item_code = %s and a.docstatus=1 - {0} - order by a.modified desc limit 1""".format(condition), item_code) + query = (frappe.qb.from_(purchase_invoice_item) + .inner_join( + purchase_invoice + ).on( + purchase_invoice.name == purchase_invoice_item.parent + ).select( + purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor + ).where( + purchase_invoice.docstatus == 1 + ).where( + purchase_invoice.posting_date <= self.filters.to_date + ).where( + purchase_invoice_item.item_code == item_code + )) + + if row.project: + query.where( + purchase_invoice_item.item_code == row.project + ) + + if row.cost_center: + query.where( + purchase_invoice_item.cost_center == row.cost_center + ) + + query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc) + query.limit(1) + last_purchase_rate = query.run() return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 From b0a9ff0b04b905ea75c2d9422ab4d6a91aa0dfae Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 11 Feb 2022 14:48:39 +0530 Subject: [PATCH 002/136] fix: Update columns in new format (cherry picked from commit 2172ab2d37d8be0c43d1f885a40657d352d255b4) --- .../report/gross_profit/gross_profit.json | 4 +- .../report/gross_profit/gross_profit.py | 80 ++++++------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 76c560ad247..0730ffd77e5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,5 +1,5 @@ { - "add_total_row": 0, + "add_total_row": 1, "columns": [], "creation": "2013-02-25 17:03:34", "disable_prepared_report": 0, @@ -9,7 +9,7 @@ "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2021-11-13 19:14:23.730198", + "modified": "2022-02-11 10:18:36.956558", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 0af7ca9c8bd..3c8e3eb898e 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -70,43 +70,42 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ data.append(row) def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): - for idx, src in enumerate(gross_profit_data.grouped_data): + for src in gross_profit_data.grouped_data: row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) - if idx == len(gross_profit_data.grouped_data)-1: - row[0] = "Total" data.append(row) def get_columns(group_wise_columns, filters): columns = [] column_map = frappe._dict({ - "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date:100", - "posting_time": _("Posting Time") + ":Data:100", - "item_code": _("Item Code") + ":Link/Item:100", - "item_name": _("Item Name") + ":Data:100", - "item_group": _("Item Group") + ":Link/Item Group:100", - "brand": _("Brand") + ":Link/Brand:100", - "description": _("Description") +":Data:100", - "warehouse": _("Warehouse") + ":Link/Warehouse:100", - "qty": _("Qty") + ":Float:80", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", - "buying_rate": _("Valuation Rate") + ":Currency/currency:100", - "base_amount": _("Selling Amount") + ":Currency/currency:100", - "buying_amount": _("Buying Amount") + ":Currency/currency:100", - "gross_profit": _("Gross Profit") + ":Currency/currency:100", - "gross_profit_percent": _("Gross Profit %") + ":Percent:100", - "project": _("Project") + ":Link/Project:100", - "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", - "customer": _("Customer") + ":Link/Customer:100", - "customer_group": _("Customer Group") + ":Link/Customer Group:100", - "territory": _("Territory") + ":Link/Territory:100" + "parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120}, + "invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120}, + "posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + "posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100}, + "item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + "item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + "item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, + "brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100}, + "description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100}, + "warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100}, + "qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80}, + "base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100}, + "buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100}, + "base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100}, + "buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100}, + "gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100}, + "gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%", + "fieldtype": "Percent", "width": 100}, + "project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, + "sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100}, + "allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100}, + "customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100}, + "customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100}, + "territory": {"label": _('Territory'), "fieldname": "territory", "fieldtype": "Link", "options": "territory", "width": 100}, }) for col in group_wise_columns.get(scrub(filters.group_by)): @@ -223,16 +222,6 @@ class GrossProfitGenerator(object): self.get_average_rate_based_on_group_by() def get_average_rate_based_on_group_by(self): - # sum buying / selling totals for group - self.totals = frappe._dict( - qty=0, - base_amount=0, - buying_amount=0, - gross_profit=0, - gross_profit_percent=0, - base_rate=0, - buying_rate=0 - ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): @@ -244,7 +233,6 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.indent == 1.0: @@ -258,17 +246,6 @@ class GrossProfitGenerator(object): if (flt(row.qty) or row.base_amount): row = self.set_average_rate(row) self.grouped_data.append(row) - self.add_to_totals(row) - - self.set_average_gross_profit(self.totals) - - if self.filters.get("group_by") == "Invoice": - self.totals.indent = 0.0 - self.totals.parent_invoice = "" - self.totals.invoice_or_item = "Total" - self.si_list.append(self.totals) - else: - self.grouped_data.append(self.totals) def is_not_invoice_row(self, row): return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" @@ -286,11 +263,6 @@ class GrossProfitGenerator(object): new_row.buying_rate = flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 new_row.base_rate = flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - def add_to_totals(self, new_row): - for key in self.totals: - if new_row.get(key): - self.totals[key] += new_row[key] - def get_returned_invoice_items(self): returned_invoices = frappe.db.sql(""" select @@ -391,7 +363,7 @@ class GrossProfitGenerator(object): if row.project: query.where( - purchase_invoice_item.item_code == row.project + purchase_invoice_item.project == row.project ) if row.cost_center: From 9a0b83027a91da3f1f62312e1875b69f8197e865 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 12 Feb 2022 19:05:03 +0530 Subject: [PATCH 003/136] fix: Remove unused param (cherry picked from commit 07bcbc6c7e10f977bc5a6ff8f5b48d91ec9b2b70) --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 3c8e3eb898e..2ec64ed5759 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -172,7 +172,7 @@ class GrossProfitGenerator(object): buying_amount = 0 for row in reversed(self.si_list): - if self.skip_row(row, self.product_bundles): + if self.skip_row(row): continue row.base_amount = flt(row.base_net_amount, self.currency_precision) @@ -280,7 +280,7 @@ class GrossProfitGenerator(object): self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ .setdefault(inv.item_code, []).append(inv) - def skip_row(self, row, product_bundles): + def skip_row(self, row): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True From a63373ce982802ecee61185c36c216740833809b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Feb 2022 22:14:17 +0530 Subject: [PATCH 004/136] fix: Gross profit for credit notes (cherry picked from commit 973f6b1bbd53594e5b2a51a1dcdf7d9e38dd46a8) --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2ec64ed5759..f6c8c63d8a5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -284,8 +284,8 @@ class GrossProfitGenerator(object): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True - elif row.get("is_return") == 1: - return True + + return False def get_buying_amount_from_product_bundle(self, row, product_bundle): buying_amount = 0.0 From 93033fb87f2b961d7bcfff5f1ee217251f4a279b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 19:15:30 +0530 Subject: [PATCH 005/136] feat: Bank Reconciliation for loan documents (cherry picked from commit 555b1335f65cca4f77c28294e153002a39e114a4) --- .../bank_reconciliation_tool.py | 73 ++++++++++++++++++- .../loan_disbursement/loan_disbursement.json | 47 ++++++++++-- .../loan_disbursement/loan_disbursement.py | 12 +-- .../loan_repayment/loan_repayment.json | 52 ++++++++++++- .../doctype/loan_repayment/loan_repayment.py | 24 +++--- .../dialog_manager.js | 17 ++++- 6 files changed, 190 insertions(+), 35 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 4211bd0169d..26078d63298 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -275,6 +275,10 @@ def check_matching(bank_account, company, transaction, document_types): } matching_vouchers = [] + + matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, + document_types, filters)) + for query in subquery: matching_vouchers.extend( frappe.db.sql(query, filters,) @@ -311,6 +315,74 @@ def get_queries(bank_account, company, transaction, document_types): return queries +def get_loan_vouchers(bank_account, transaction, document_types, filters): + vouchers = [] + amount_condition = True if "exact_match" in document_types else False + + if transaction.withdrawal > 0 and "loan_disbursement" in document_types: + vouchers.append(get_ld_matching_query(bank_account, amount_condition, filters)) + + if transaction.deposit > 0 and "loan_repayment" in document_types: + vouchers.append(get_lr_matching_query(bank_account, amount_condition, filters)) + +def get_ld_matching_query(bank_account, amount_condition, filters): + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + query = frappe.qb.from_(loan_disbursement).select( + loan_disbursement.name, + loan_disbursement.disbursed_amount, + loan_disbursement.reference_number, + loan_disbursement.reference_date, + loan_disbursement.applicant_type, + loan_disbursement.disbursement_date + ).where( + loan_disbursement.docstatus == 1 + ).where( + loan_disbursement.clearance_date.isnull() + ).where( + loan_disbursement.disbursement_account == bank_account + ) + + if amount_condition: + query.where( + loan_disbursement.disbursed_amount == filters.get('amount') + ) + else: + query.where( + loan_disbursement.disbursed_amount <= filters.get('amount') + ) + + vouchers = query.run(as_dict=1) + return vouchers + +def get_lr_matching_query(bank_account, amount_condition, filters): + loan_repayment = frappe.qb.DocType("Loan Repayment") + query = frappe.qb.from_(loan_repayment).select( + loan_repayment.name, + loan_repayment.paid_amount, + loan_repayment.reference_number, + loan_repayment.reference_date, + loan_repayment.applicant_type, + loan_repayment.posting_date + ).where( + loan_repayment.docstatus == 1 + ).where( + loan_repayment.clearance_date.isnull() + ).where( + loan_repayment.disbursement_account == bank_account + ) + + if amount_condition: + query.where( + loan_repayment.paid_amount == filters.get('amount') + ) + else: + query.where( + loan_repayment.paid_amount <= filters.get('amount') + ) + + vouchers = query.run(as_dict=1) + return vouchers + def get_pe_matching_query(amount_condition, account_from_to, transaction): # get matching payment entries query if transaction.deposit > 0: @@ -348,7 +420,6 @@ def get_je_matching_query(amount_condition, transaction): # We have mapping at the bank level # So one bank could have both types of bank accounts like asset and liability # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type - company_account = frappe.get_value("Bank Account", transaction.bank_account, "account") cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" return f""" diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index 7811d56a758..50926d77268 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -14,11 +14,15 @@ "applicant", "section_break_7", "disbursement_date", + "clearance_date", "column_break_8", "disbursed_amount", "accounting_dimensions_section", "cost_center", - "customer_details_section", + "accounting_details", + "disbursement_account", + "column_break_16", + "loan_account", "bank_account", "disbursement_references_section", "reference_date", @@ -106,11 +110,6 @@ "fieldtype": "Section Break", "label": "Disbursement Details" }, - { - "fieldname": "customer_details_section", - "fieldtype": "Section Break", - "label": "Customer Details" - }, { "fetch_from": "against_loan.applicant_type", "fieldname": "applicant_type", @@ -149,15 +148,48 @@ "fieldname": "reference_number", "fieldtype": "Data", "label": "Reference Number" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "against_loan.disbursement_account", + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:09:32.175355", + "modified": "2022-02-17 18:23:44.157598", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -194,5 +226,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index df3aadfb18d..54a03b92b5e 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController): if not self.posting_date: self.posting_date = self.disbursement_date or nowdate() - if not self.bank_account and self.applicant_type == "Customer": - self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") - def validate_disbursal_amount(self): possible_disbursal_amount = get_disbursal_amount(self.against_loan) @@ -117,12 +114,11 @@ class LoanDisbursement(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - loan_details = frappe.get_doc("Loan", self.against_loan) gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.disbursement_account, + "account": self.loan_account, + "against": self.disbursement_account, "debit": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", @@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ - "account": loan_details.disbursement_account, - "against": loan_details.loan_account, + "account": self.disbursement_account, + "against": self.loan_account, "credit": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 93ef2170420..766602de866 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "LM-REP-.####", - "creation": "2019-09-03 14:44:39.977266", + "creation": "2022-01-25 10:30:02.767941", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -13,6 +13,7 @@ "column_break_3", "company", "posting_date", + "clearance_date", "rate_of_interest", "payroll_payable_account", "is_term_loan", @@ -37,7 +38,12 @@ "total_penalty_paid", "total_interest_paid", "repayment_details", - "amended_from" + "amended_from", + "accounting_details_section", + "repayment_account", + "penalty_income_account", + "column_break_36", + "loan_account" ], "fields": [ { @@ -260,12 +266,52 @@ "fieldname": "repay_from_salary", "fieldtype": "Check", "label": "Repay From Salary" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fetch_from": "against_loan.payment_account", + "fieldname": "repayment_account", + "fieldtype": "Link", + "label": "Repayment Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "read_only": 1 + }, + { + "fetch_from": "against_loan.penalty_income_account", + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Penalty Income Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-06 01:51:06.707782", + "modified": "2022-02-17 19:10:07.742298", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index a6e526a0490..0610da14d4c 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -311,7 +311,6 @@ class LoanRepayment(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - loan_details = frappe.get_doc("Loan", self.against_loan) if self.shortfall_amount and self.amount_paid > self.shortfall_amount: remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, @@ -324,13 +323,13 @@ class LoanRepayment(AccountsController): if self.repay_from_salary: payment_account = self.payroll_payable_account else: - payment_account = loan_details.payment_account + payment_account = self.payment_account if self.total_penalty_paid: gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, + "account": self.loan_account, + "against": payment_account, "debit": self.total_penalty_paid, "debit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", @@ -345,8 +344,8 @@ class LoanRepayment(AccountsController): gle_map.append( self.get_gl_dict({ - "account": loan_details.penalty_income_account, - "against": loan_details.loan_account, + "account": self.penalty_income_account, + "against": self.loan_account, "credit": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", @@ -360,8 +359,7 @@ class LoanRepayment(AccountsController): gle_map.append( self.get_gl_dict({ "account": payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, + "against": self.loan_account + ", " + self.penalty_income_account, "debit": self.amount_paid, "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", @@ -369,16 +367,16 @@ class LoanRepayment(AccountsController): "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date), - "party_type": loan_details.applicant_type if self.repay_from_salary else '', - "party": loan_details.applicant if self.repay_from_salary else '' + "party_type": self.applicant_type if self.repay_from_salary else '', + "party": self.applicant if self.repay_from_salary else '' }) ) gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, "against": payment_account, "credit": self.amount_paid, "credit_in_account_currency": self.amount_paid, diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 75ed332f4b6..bb799af36ea 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "journal_entry", onchange: () => this.update_options(), }, + { + fieldtype: "Check", + label: "Loan Repayment", + fieldname: "loan_repayment", + onchange: () => this.update_options(), + }, { fieldname: "column_break_5", fieldtype: "Column Break", @@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "sales_invoice", onchange: () => this.update_options(), }, - { fieldtype: "Check", label: "Purchase Invoice", fieldname: "purchase_invoice", onchange: () => this.update_options(), }, + { + fieldtype: "Check", + label: "Show Only Exact Amount", + fieldname: "exact_match", + onchange: () => this.update_options(), + }, { fieldname: "column_break_5", fieldtype: "Column Break", @@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { }, { fieldtype: "Check", - label: "Show Only Exact Amount", - fieldname: "exact_match", + label: "Loan Disbursement", + fieldname: "loan_disbursement", onchange: () => this.update_options(), }, { From c2129b762a29e285b440c9ffa9e234657eec4731 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 11:44:00 +0530 Subject: [PATCH 006/136] fix: Add patch for account fields (cherry picked from commit a0bdcbd0cd551895af63955343f517051917c8eb) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 5 +++ .../v13_0/update_accounts_in_loan_docs.py | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 erpnext/patches/v13_0/update_accounts_in_loan_docs.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6aaf9aa33aa..f7bc17cb3a8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -349,5 +349,10 @@ erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo +<<<<<<< HEAD erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr +======= +erpnext.patches.v14_0.delete_amazon_mws_doctype +erpnext.patches.v13_0.update_accounts_in_loan_docs +>>>>>>> a0bdcbd0cd (fix: Add patch for account fields) diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py new file mode 100644 index 00000000000..440f912be21 --- /dev/null +++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py @@ -0,0 +1,37 @@ +import frappe + + +def execute(): + ld = frappe.qb.DocType("Loan Disbursement").as_("ld") + lr = frappe.qb.DocType("Loan Repayment").as_("lr") + loan = frappe.qb.DocType("Loan") + + frappe.qb.update( + ld + ).inner_join( + loan + ).on( + loan.name == ld.against_loan + ).set( + ld.disbursement_account, loan.disbursement_account + ).set( + ld.loan_account, loan.loan_account + ).where( + ld.docstatus < 2 + ).run() + + frappe.qb.update( + lr + ).inner_join( + loan + ).on( + loan.name == lr.against_loan + ).set( + lr.payment_account, loan.payment_account + ).set( + lr.loan_account, loan.loan_account + ).set( + lr.penalty_income_account, loan.penalty_income_account + ).where( + lr.docstatus < 2 + ).run() From e998521ca6fc10af42aa85fb338472634f4b0345 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 11:45:23 +0530 Subject: [PATCH 007/136] fix: Update queries in Bank Reconciliation Tool (cherry picked from commit 295cbb0ff22b04c705148d727d96f70b836fee93) --- .../bank_reconciliation_tool.py | 57 ++++++++++++++++--- .../bank_transaction/bank_transaction.py | 13 ++++- .../loan_repayment/loan_repayment.json | 6 +- 3 files changed, 63 insertions(+), 13 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 26078d63298..f3351ddcba4 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt from erpnext import get_company_currency @@ -320,14 +321,34 @@ def get_loan_vouchers(bank_account, transaction, document_types, filters): amount_condition = True if "exact_match" in document_types else False if transaction.withdrawal > 0 and "loan_disbursement" in document_types: - vouchers.append(get_ld_matching_query(bank_account, amount_condition, filters)) + vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters)) if transaction.deposit > 0 and "loan_repayment" in document_types: - vouchers.append(get_lr_matching_query(bank_account, amount_condition, filters)) + vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters)) + + return vouchers def get_ld_matching_query(bank_account, amount_condition, filters): loan_disbursement = frappe.qb.DocType("Loan Disbursement") + matching_reference = loan_disbursement.reference_number == filters.get("reference_number") + matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \ + loan_disbursement.applicant == filters.get("party") + + rank = ( + frappe.qb.terms.Case() + .when(matching_reference, 1) + .else_(0) + ) + + rank1 = ( + frappe.qb.terms.Case() + .when(matching_party, 1) + .else_(0) + ) + query = frappe.qb.from_(loan_disbursement).select( + rank + rank1 + 1, + ConstantColumn("Loan Disbursement").as_("doctype"), loan_disbursement.name, loan_disbursement.disbursed_amount, loan_disbursement.reference_number, @@ -351,14 +372,33 @@ def get_ld_matching_query(bank_account, amount_condition, filters): loan_disbursement.disbursed_amount <= filters.get('amount') ) - vouchers = query.run(as_dict=1) + vouchers = query.run(as_list=True) + return vouchers def get_lr_matching_query(bank_account, amount_condition, filters): loan_repayment = frappe.qb.DocType("Loan Repayment") + matching_reference = loan_repayment.reference_number == filters.get("reference_number") + matching_party = loan_repayment.applicant_type == filters.get("party_type") and \ + loan_repayment.applicant == filters.get("party") + + rank = ( + frappe.qb.terms.Case() + .when(matching_reference, 1) + .else_(0) + ) + + rank1 = ( + frappe.qb.terms.Case() + .when(matching_party, 1) + .else_(0) + ) + query = frappe.qb.from_(loan_repayment).select( + rank + rank1 + 1, + ConstantColumn("Loan Repayment").as_("doctype"), loan_repayment.name, - loan_repayment.paid_amount, + loan_repayment.amount_paid, loan_repayment.reference_number, loan_repayment.reference_date, loan_repayment.applicant_type, @@ -368,19 +408,20 @@ def get_lr_matching_query(bank_account, amount_condition, filters): ).where( loan_repayment.clearance_date.isnull() ).where( - loan_repayment.disbursement_account == bank_account + loan_repayment.payment_account == bank_account ) if amount_condition: query.where( - loan_repayment.paid_amount == filters.get('amount') + loan_repayment.amount_paid == filters.get('amount') ) else: query.where( - loan_repayment.paid_amount <= filters.get('amount') + loan_repayment.amount_paid <= filters.get('amount') ) - vouchers = query.run(as_dict=1) + vouchers = query.run() + return vouchers def get_pe_matching_query(amount_condition, account_from_to, transaction): diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 44cea31ed38..464d645a994 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -48,7 +48,8 @@ class BankTransaction(StatusUpdater): def clear_linked_payment_entries(self, for_cancel=False): for payment_entry in self.payment_entries: - if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: + if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment", + "Loan Disbursement"]: self.clear_simple_entry(payment_entry, for_cancel=for_cancel) elif payment_entry.payment_document == "Sales Invoice": @@ -103,6 +104,7 @@ def get_total_allocated_amount(payment_entry): bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) def get_paid_amount(payment_entry, currency, bank_account): + print(payment_entry.payment_document, "#@#@#@") if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" @@ -115,11 +117,18 @@ def get_paid_amount(payment_entry, currency, bank_account): payment_entry.payment_entry, paid_amount_field) elif payment_entry.payment_document == "Journal Entry": - return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)") + return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, + "sum(credit_in_account_currency)") elif payment_entry.payment_document == "Expense Claim": return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") + elif payment_entry.payment_document == "Loan Disbursement": + return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount") + + elif payment_entry.payment_document == "Loan Repayment": + return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid") + else: frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 766602de866..480e010b49a 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -40,7 +40,7 @@ "repayment_details", "amended_from", "accounting_details_section", - "repayment_account", + "payment_account", "penalty_income_account", "column_break_36", "loan_account" @@ -281,7 +281,7 @@ }, { "fetch_from": "against_loan.payment_account", - "fieldname": "repayment_account", + "fieldname": "payment_account", "fieldtype": "Link", "label": "Repayment Account", "options": "Account", @@ -311,7 +311,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-17 19:10:07.742298", + "modified": "2022-02-18 19:10:07.742298", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", From 247325e4f5fa8b1eb5f251a45a0be52d21677eef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 11:46:44 +0530 Subject: [PATCH 008/136] fix: Update bank reconciliation statement (cherry picked from commit 0b5e618e3ab206f7ae080f570a736a87fcbccf2d) --- .../bank_reconciliation_statement.py | 105 ++++++++++++++++-- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 6c401fb8f3b..b72d2669775 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -4,7 +4,12 @@ import frappe from frappe import _ -from frappe.utils import flt, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Sum +from frappe.utils import flt, getdate +from pypika import CustomFunction + +from erpnext.accounts.utils import get_balance_on def execute(filters=None): @@ -18,7 +23,6 @@ def execute(filters=None): data = get_entries(filters) - from erpnext.accounts.utils import get_balance_on balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) total_debit, total_credit = 0,0 @@ -118,7 +122,21 @@ def get_columns(): ] def get_entries(filters): - journal_entries = frappe.db.sql(""" + journal_entries = get_journal_entries(filters) + + payment_entries = get_payment_entries(filters) + + loan_entries = get_loan_entries(filters) + + pos_entries = [] + if filters.include_pos_transactions: + pos_entries = get_pos_entries(filters) + + return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)), + key=lambda k: getdate(k['posting_date'])) + +def get_journal_entries(filters): + return frappe.db.sql(""" select "Journal Entry" as payment_document, jv.posting_date, jv.name as payment_entry, jvd.debit_in_account_currency as debit, jvd.credit_in_account_currency as credit, jvd.against_account, @@ -130,7 +148,8 @@ def get_entries(filters): and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) - payment_entries = frappe.db.sql(""" +def get_payment_entries(filters): + return frappe.db.sql(""" select "Payment Entry" as payment_document, name as payment_entry, reference_no, reference_date as ref_date, @@ -145,9 +164,8 @@ def get_entries(filters): and ifnull(clearance_date, '4000-01-01') > %(report_date)s """, filters, as_dict=1) - pos_entries = [] - if filters.include_pos_transactions: - pos_entries = frappe.db.sql(""" +def get_pos_entries(filters): + return frappe.db.sql(""" select "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, si.posting_date, si.debit_to as against_account, sip.clearance_date, @@ -161,8 +179,42 @@ def get_entries(filters): si.posting_date ASC, si.name DESC """, filters, as_dict=1) - return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), - key=lambda k: k['posting_date'] or getdate(nowdate())) +def get_loan_entries(filters): + loan_docs = [] + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction('IFNULL', ['value', 'default']) + + if doctype == "Loan Disbursement": + amount_field = (loan_doc.disbursed_amount).as_("credit") + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + else: + amount_field = (loan_doc.amount_paid).as_("debit") + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + + entries = frappe.qb.from_(loan_doc).select( + ConstantColumn(doctype).as_("payment_document"), + (loan_doc.name).as_("payment_entry"), + (loan_doc.reference_number).as_("reference_no"), + (loan_doc.reference_date).as_("ref_date"), + amount_field, + posting_date, + ).where( + loan_doc.docstatus == 1 + ).where( + account == filters.get('account') + ).where( + posting_date <= getdate(filters.get('report_date')) + ).where( + ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date')) + ).run(as_dict=1) + + loan_docs.extend(entries) + + return loan_docs + def get_amounts_not_reflected_in_system(filters): je_amount = frappe.db.sql(""" @@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters): pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 - return je_amount + pe_amount + loan_amount = get_loan_amount(filters) + + return je_amount + pe_amount + loan_amount + +def get_loan_amount(filters): + total_amount = 0 + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction('IFNULL', ['value', 'default']) + + if doctype == "Loan Disbursement": + amount_field = Sum(loan_doc.disbursed_amount) + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + else: + amount_field = Sum(loan_doc.amount_paid) + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + + amount = frappe.qb.from_(loan_doc).select( + amount_field + ).where( + loan_doc.docstatus == 1 + ).where( + account == filters.get('account') + ).where( + posting_date > getdate(filters.get('report_date')) + ).where( + ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date')) + ).run()[0][0] + + total_amount += flt(amount) + + return amount def get_balance_row(label, amount, account_currency): if amount > 0: From 259a091a379524ec2f4732f7586ed89c0cebc30b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 17:08:25 +0530 Subject: [PATCH 009/136] fix: Remove print statements (cherry picked from commit a4c6cb9f12f0ff931909a15b657b62a4bc85a20b) --- erpnext/accounts/doctype/bank_transaction/bank_transaction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 464d645a994..b8e8d970220 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -104,7 +104,6 @@ def get_total_allocated_amount(payment_entry): bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) def get_paid_amount(payment_entry, currency, bank_account): - print(payment_entry.payment_document, "#@#@#@") if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" From d1049b0d008f7ca838d4d620b8078ff3409d8303 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 23 Feb 2022 11:10:07 +0530 Subject: [PATCH 010/136] fix: Resolve conflicts --- erpnext/patches.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f7bc17cb3a8..d53df7e4f63 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -349,10 +349,6 @@ erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo -<<<<<<< HEAD erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr -======= -erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v13_0.update_accounts_in_loan_docs ->>>>>>> a0bdcbd0cd (fix: Add patch for account fields) From a8ae86e23f1316efbcc8df177400426dcece4085 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 20:29:52 +0530 Subject: [PATCH 011/136] fix: wrong payment days in salary slip for employees joining/leaving during mid payroll dates (backport #29082) (#30099) Co-authored-by: Rucha Mahabal Co-authored-by: Dany Robert --- erpnext/hr/doctype/attendance/attendance.py | 18 ++- .../hr/doctype/attendance/test_attendance.py | 93 +++++++++++++- .../doctype/salary_slip/salary_slip.py | 55 ++++++-- .../doctype/salary_slip/test_salary_slip.py | 120 ++++++++++++++---- 4 files changed, 239 insertions(+), 47 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index b1eaaf8b587..b1e373e2181 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -174,16 +174,22 @@ def get_month_map(): def get_unmarked_days(employee, month, exclude_holidays=0): import calendar month_map = get_month_map() - today = get_datetime() - dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)] + joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"]) + start_day = 1 + end_day = calendar.monthrange(today.year, month_map[month])[1] + 1 - length = len(dates_of_month) - month_start, month_end = dates_of_month[0], dates_of_month[length-1] + if joining_date and joining_date.month == month_map[month]: + start_day = joining_date.day + if relieving_date and relieving_date.month == month_map[month]: + end_day = relieving_date.day + 1 - records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [ + dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)] + month_start, month_end = dates_of_month[0], dates_of_month[-1] + + records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[ ["attendance_date", ">=", month_start], ["attendance_date", "<=", month_end], ["employee", "=", employee], @@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0): for date in dates_of_month: date_time = get_datetime(date) - if today.day == date_time.day and today.month == date_time.month: + if today.day <= date_time.day and today.month <= date_time.month: break if date_time not in marked_days: unmarked_days.append(date) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index a770d70ffa9..118cc987efb 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -4,17 +4,104 @@ import unittest import frappe -from frappe.utils import nowdate +from frappe.utils import add_days, get_first_day, getdate, nowdate + +from erpnext.hr.doctype.attendance.attendance import ( + get_month_map, + get_unmarked_days, + mark_attendance, +) +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday test_records = frappe.get_test_records('Attendance') class TestAttendance(unittest.TestCase): def test_mark_absent(self): - from erpnext.hr.doctype.employee.test_employee import make_employee employee = make_employee("test_mark_absent@example.com") date = nowdate() frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date}) - from erpnext.hr.doctype.attendance.attendance import mark_attendance attendance = mark_attendance(employee, date, 'Absent') fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'}) self.assertEqual(attendance, fetch_attendance) + + def test_unmarked_days(self): + first_day = get_first_day(getdate()) + + employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) + frappe.db.delete('Attendance', {'employee': employee}) + + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + holiday_list = make_holiday_list() + frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) + + first_sunday = get_first_sunday(holiday_list) + mark_attendance(employee, first_day, 'Present') + month_name = get_month_name(first_day) + + unmarked_days = get_unmarked_days(employee, month_name) + unmarked_days = [getdate(date) for date in unmarked_days] + + # attendance already marked for the day + self.assertNotIn(first_day, unmarked_days) + # attendance unmarked + self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + # holiday considered in unmarked days + self.assertIn(first_sunday, unmarked_days) + + def test_unmarked_days_excluding_holidays(self): + first_day = get_first_day(getdate()) + + employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) + frappe.db.delete('Attendance', {'employee': employee}) + + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + holiday_list = make_holiday_list() + frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) + + first_sunday = get_first_sunday(holiday_list) + mark_attendance(employee, first_day, 'Present') + month_name = get_month_name(first_day) + + unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) + unmarked_days = [getdate(date) for date in unmarked_days] + + # attendance already marked for the day + self.assertNotIn(first_day, unmarked_days) + # attendance unmarked + self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + # holidays not considered in unmarked days + self.assertNotIn(first_sunday, unmarked_days) + + def test_unmarked_days_as_per_joining_and_relieving_dates(self): + first_day = get_first_day(getdate()) + + doj = add_days(first_day, 1) + relieving_date = add_days(first_day, 5) + employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj, + date_of_relieving=relieving_date) + frappe.db.delete('Attendance', {'employee': employee}) + + attendance_date = add_days(first_day, 2) + mark_attendance(employee, attendance_date, 'Present') + month_name = get_month_name(first_day) + + unmarked_days = get_unmarked_days(employee, month_name) + unmarked_days = [getdate(date) for date in unmarked_days] + + # attendance already marked for the day + self.assertNotIn(attendance_date, unmarked_days) + # date before doj not in unmarked days + self.assertNotIn(add_days(doj, -1), unmarked_days) + # date after relieving not in unmarked days + self.assertNotIn(add_days(relieving_date, 1), unmarked_days) + + def tearDown(self): + frappe.db.rollback() + + +def get_month_name(date): + month_number = date.month + for month, number in get_month_map().items(): + if number == month_number: + return month \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index e70c5116bed..422bb0e1bbc 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -308,28 +308,59 @@ class SalarySlip(TransactionBase): if payroll_based_on == "Attendance": self.payment_days -= flt(absent) - unmarked_days = self.get_unmarked_days() consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent": + unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days) self.absent_days += unmarked_days #will be treated as absent self.payment_days -= unmarked_days - if include_holidays_in_total_working_days: - for holiday in holidays: - if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }): - self.payment_days += 1 else: self.payment_days = 0 - def get_unmarked_days(self): - marked_days = frappe.get_all("Attendance", filters = { - "attendance_date": ["between", [self.start_date, self.end_date]], - "employee": self.employee, - "docstatus": 1 - }, fields = ["COUNT(*) as marked_days"])[0].marked_days + def get_unmarked_days(self, include_holidays_in_total_working_days): + unmarked_days = self.total_working_days + joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, + ["date_of_joining", "relieving_date"]) + start_date = self.start_date + end_date = self.end_date - return self.total_working_days - marked_days + if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)): + start_date = joining_date + unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days, + include_holidays_in_total_working_days, self.start_date, joining_date) + if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)): + end_date = relieving_date + unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days, + include_holidays_in_total_working_days, relieving_date, self.end_date) + + # exclude days for which attendance has been marked + unmarked_days -= frappe.get_all("Attendance", filters = { + "attendance_date": ["between", [start_date, end_date]], + "employee": self.employee, + "docstatus": 1 + }, fields = ["COUNT(*) as marked_days"])[0].marked_days + + return unmarked_days + + def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days, + include_holidays_in_total_working_days, start_date, end_date): + """ + Exclude days before DOJ or after + Relieving Date from unmarked days + """ + from erpnext.hr.doctype.employee.employee import is_holiday + + if include_holidays_in_total_working_days: + unmarked_days -= date_diff(end_date, start_date) + else: + # exclude only if not holidays + for days in range(date_diff(end_date, start_date)): + date = add_days(end_date, -days) + if not is_holiday(self.employee, date): + unmarked_days -= 1 + + return unmarked_days def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): if not joining_date: diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 4249fa76c71..20060f479ac 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -7,10 +7,12 @@ import unittest import frappe from frappe.model.document import Document +from frappe.tests.utils import change_settings from frappe.utils import ( add_days, add_months, cstr, + date_diff, flt, get_first_day, get_last_day, @@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random import erpnext from erpnext.accounts.utils import get_fiscal_year +from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type @@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase): setup_test() def tearDown(self): + frappe.db.rollback() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") + @change_settings("Payroll Settings", { + "payroll_based_on": "Attendance", + "daily_wages_fraction_for_half_day": 0.75 + }) def test_payment_days_based_on_attendance(self): - from erpnext.hr.doctype.attendance.attendance import mark_attendance no_of_days = self.get_no_of_days() - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75) - emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -85,14 +88,78 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.gross_pay, gross_pay) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + @change_settings("Payroll Settings", { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": True + }) + def test_payment_days_for_mid_joinee_including_holidays(self): + from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday + no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + + new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") + joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) + frappe.db.set_value("Employee", new_emp_id, { + "date_of_joining": joining_date, + "relieving_date": relieving_date, + "status": "Left" + }) + + holidays = 0 + + for days in range(date_diff(relieving_date, joining_date) + 1): + date = add_days(joining_date, days) + if not is_holiday("Salary Slip Test Holiday List", date): + mark_attendance(new_emp_id, date, 'Present', ignore_validate=True) + else: + holidays += 1 + + new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence") + + self.assertEqual(new_ss.total_working_days, no_of_days[0]) + self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) + + @change_settings("Payroll Settings", { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": False + }) + def test_payment_days_for_mid_joinee_excluding_holidays(self): + from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday + + no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + + new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") + joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) + frappe.db.set_value("Employee", new_emp_id, { + "date_of_joining": joining_date, + "relieving_date": relieving_date, + "status": "Left" + }) + + holidays = 0 + + for days in range(date_diff(relieving_date, joining_date) + 1): + date = add_days(joining_date, days) + if not is_holiday("Salary Slip Test Holiday List", date): + mark_attendance(new_emp_id, date, 'Present', ignore_validate=True) + else: + holidays += 1 + + new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence") + + self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1]) + self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) + + @change_settings("Payroll Settings", { + "payroll_based_on": "Leave" + }) def test_payment_days_based_on_leave_application(self): no_of_days = self.get_no_of_days() - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -133,8 +200,9 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - + @change_settings("Payroll Settings", { + "payroll_based_on": "Attendance" + }) def test_payment_days_in_salary_slip_based_on_timesheet(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.projects.doctype.timesheet.test_timesheet import ( @@ -145,9 +213,6 @@ class TestSalarySlip(unittest.TestCase): make_salary_slip as make_salary_slip_for_timesheet, ) - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List") frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) @@ -185,8 +250,9 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") - + @change_settings("Payroll Settings", { + "payroll_based_on": "Attendance" + }) def test_component_amount_dependent_on_another_payment_days_based_component(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( @@ -194,9 +260,6 @@ class TestSalarySlip(unittest.TestCase): ) no_of_days = self.get_no_of_days() - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") - salary_structure = make_salary_structure_for_payment_days_based_component_dependency() employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") @@ -242,11 +305,12 @@ class TestSalarySlip(unittest.TestCase): expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision) self.assertEqual(actual_amount, expected_amount) - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + @change_settings("Payroll Settings", { + "include_holidays_in_total_working_days": 1 + }) def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() - frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) make_employee("test_salary_slip_with_holidays_included@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) @@ -260,9 +324,11 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.gross_pay, 78000) + @change_settings("Payroll Settings", { + "include_holidays_in_total_working_days": 0 + }) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() - frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) make_employee("test_salary_slip_with_holidays_excluded@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) @@ -277,14 +343,15 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.gross_pay, 78000) + @change_settings("Payroll Settings", { + "include_holidays_in_total_working_days": 1 + }) def test_payment_days(self): from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( create_salary_structure_assignment, ) no_of_days = self.get_no_of_days() - # Holidays not included in working days - frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) # set joinng date in the same month employee = make_employee("test_payment_days@salary.com") @@ -342,11 +409,12 @@ class TestSalarySlip(unittest.TestCase): frappe.set_user("test_employee_salary_slip_read_permission@salary.com") self.assertTrue(salary_slip_test_employee.has_permission("read")) + @change_settings("Payroll Settings", { + "email_salary_slip_to_employee": 1 + }) def test_email_salary_slip(self): frappe.db.sql("delete from `tabEmail Queue`") - frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) - make_employee("test_email_salary_slip@salary.com") ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" From fd3adbb6fe4a567fe3176bb27c8ed5c23de5ca66 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Feb 2022 16:25:32 +0530 Subject: [PATCH 012/136] fix: Item discounts for quotation (cherry picked from commit 3a547cb0d965b8012136d06adc9d7c7b94700660) # Conflicts: # erpnext/selling/doctype/quotation/quotation.js --- erpnext/controllers/taxes_and_totals.py | 2 +- erpnext/selling/doctype/quotation/quotation.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 08d1dcea7dc..d1904747918 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -116,7 +116,7 @@ class calculate_taxes_and_totals(object): if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: - if not item.rate or (item.pricing_rules and item.discount_percentage > 0): + if item.pricing_rules or item.discount_percentage > 0: item.rate = flt(item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 474cf56fc1b..0ff00c09762 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -38,6 +38,7 @@ frappe.ui.form.on('Quotation', { } }); +<<<<<<< HEAD erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ onload: function(doc, dt, dn) { var me = this; @@ -45,6 +46,13 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ }, party_name: function() { +======= +erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { + onload(doc, dt, dn) { + super.onload(doc, dt, dn); + } + party_name() { +>>>>>>> 3a547cb0d9 (fix: Item discounts for quotation) var me = this; erpnext.utils.get_party_details(this.frm, null, null, function() { me.apply_price_list(); From dfbd0ebf910e65eeb8a94043ebcd9ebe5a96d46a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 11:48:28 +0530 Subject: [PATCH 013/136] fix: apply margin on duplicated doc too (cherry picked from commit bbc4710fa31357cad038f2b515ae07ed09bd2c5e) --- erpnext/controllers/taxes_and_totals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index d1904747918..692af71a57a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -116,11 +116,11 @@ class calculate_taxes_and_totals(object): if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: - if item.pricing_rules or item.discount_percentage > 0: + if item.pricing_rules or abs(item.discount_percentage) > 0: item.rate = flt(item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) - elif item.discount_amount and item.pricing_rules: + elif item.discount_amount or item.pricing_rules: item.rate = item.price_list_rate - item.discount_amount if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: From 9af14ec0d0de06b0df422ca482428c430b4771ed Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Mar 2022 23:09:59 +0530 Subject: [PATCH 014/136] fix: Test cases with discount (cherry picked from commit d95f8934aa5cafdddd02568841786081f90c214a) --- erpnext/controllers/taxes_and_totals.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 692af71a57a..36e4f0a759d 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -113,17 +113,24 @@ class calculate_taxes_and_totals(object): for item in self.doc.get("items"): self.doc.round_floats_in(item) + if not item.rate: + item.rate = item.price_list_rate + if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: if item.pricing_rules or abs(item.discount_percentage) > 0: item.rate = flt(item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) - item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) + + if abs(item.discount_percentage) > 0: + item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) + elif item.discount_amount or item.pricing_rules: item.rate = item.price_list_rate - item.discount_amount - if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: + if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', + 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) if flt(item.rate_with_margin) > 0: item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) From 456ebc32f00be56cc9972cc76ac412776581060a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BCrker=20Tunal=C4=B1?= Date: Tue, 8 Mar 2022 07:24:33 +0300 Subject: [PATCH 015/136] fix: translate error message titles --- erpnext/stock/stock_ledger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 47a97c47fe5..dd7fb66f983 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -834,7 +834,7 @@ class update_entries_after(object): if msg_list: message = "\n\n".join(msg_list) if self.verbose: - frappe.throw(message, NegativeStockError, title='Insufficient Stock') + frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) else: raise NegativeStockError(message) @@ -1115,7 +1115,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): 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') + frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) if not args.batch_no: @@ -1129,7 +1129,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): 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 for Batch")) def get_future_sle_with_negative_qty(args): From ba4d49640467b148a804a4788766048e08992cdb Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 8 Mar 2022 09:55:36 +0530 Subject: [PATCH 016/136] fix: Resolve conflicts --- erpnext/selling/doctype/quotation/quotation.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 0ff00c09762..474cf56fc1b 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -38,7 +38,6 @@ frappe.ui.form.on('Quotation', { } }); -<<<<<<< HEAD erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ onload: function(doc, dt, dn) { var me = this; @@ -46,13 +45,6 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ }, party_name: function() { -======= -erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { - onload(doc, dt, dn) { - super.onload(doc, dt, dn); - } - party_name() { ->>>>>>> 3a547cb0d9 (fix: Item discounts for quotation) var me = this; erpnext.utils.get_party_details(this.frm, null, null, function() { me.apply_price_list(); From 31f20cf14441299007b2ace2b66b6d92d1ff0818 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 7 Mar 2022 18:01:07 +0530 Subject: [PATCH 017/136] fix(pos): multiple pos round off cases (cherry picked from commit 17445c7e04ff88cc5db727cb9f769647bcbebfdf) --- .../pos_invoice_merge_log.py | 9 -- .../test_pos_invoice_merge_log.py | 98 +++++++++++++++++++ .../doctype/sales_invoice/sales_invoice.py | 3 + erpnext/controllers/taxes_and_totals.py | 29 +++--- 4 files changed, 117 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 40ab0c50deb..41dfa226a56 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -85,20 +85,12 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() - self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name - def write_off_fractional_amount(self, invoice, data): - pos_invoice_grand_total = sum(d.grand_total for d in data) - - if abs(pos_invoice_grand_total - invoice.grand_total) < 1: - invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) - invoice.save() - def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -111,7 +103,6 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() - self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 5930aa097f7..89f7f18b42c 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -5,6 +5,7 @@ import json import unittest import frappe +from frappe.tests.utils import change_settings from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return @@ -280,3 +281,100 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + @change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3}) + def test_consolidation_round_off_error_3(self): + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + init_user_and_profile() + + item_rates = [69, 59, 29] + for i in [1, 2]: + inv = create_pos_invoice(is_return=1, do_not_save=1) + inv.items = [] + for rate in item_rates: + inv.append("items", { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": -1, + "rate": rate, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 15, + "included_in_print_rate": 1 + }) + inv.payments = [] + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157 + }) + inv.paid_amount = -157 + inv.save() + inv.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.status, 'Return') + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_rounding_adjustment(self): + ''' + Test if the rounding adjustment is calculated correctly + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + + init_user_and_profile() + + inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60 + }) + inv2.insert() + inv2.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.rounding_adjustment, 1) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 42da6b7708f..409677f3c26 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -272,6 +272,9 @@ class SalesInvoice(SellingController): self.process_common_party_accounting() def validate_pos_return(self): + if self.is_consolidated: + # pos return is already validated in pos invoice + return if self.is_pos and self.is_return: total_amount_in_payments = 0 diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 08d1dcea7dc..bcaf7a145a2 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -270,7 +270,8 @@ class calculate_taxes_and_totals(object): shipping_rule.apply(self.doc) def calculate_taxes(self): - if not self.doc.get('is_consolidated'): + rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') + if not rounding_adjustment_computed: self.doc.rounding_adjustment = 0 # maintain actual tax rate based on idx @@ -326,7 +327,7 @@ class calculate_taxes_and_totals(object): if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ and self.doc.discount_amount \ and self.doc.apply_discount_on == "Grand Total" \ - and not self.doc.get('is_consolidated'): + and not rounding_adjustment_computed: self.doc.rounding_adjustment = flt(self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, self.doc.precision("rounding_adjustment")) @@ -465,20 +466,22 @@ class calculate_taxes_and_totals(object): self.doc.total_net_weight += d.total_weight def set_rounded_total(self): - if not self.doc.get('is_consolidated'): - if self.doc.meta.get_field("rounded_total"): - if self.doc.is_rounded_total_disabled(): - self.doc.rounded_total = self.doc.base_rounded_total = 0 - return + if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'): + return - self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, - self.doc.currency, self.doc.precision("rounded_total")) + if self.doc.meta.get_field("rounded_total"): + if self.doc.is_rounded_total_disabled(): + self.doc.rounded_total = self.doc.base_rounded_total = 0 + return - #if print_in_rate is set, we would have already calculated rounding adjustment - self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, - self.doc.precision("rounding_adjustment")) + self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, + self.doc.currency, self.doc.precision("rounded_total")) - self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) + #if print_in_rate is set, we would have already calculated rounding adjustment + self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, + self.doc.precision("rounding_adjustment")) + + self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) def _cleanup(self): if not self.doc.get('is_consolidated'): From e5c82d018f62a45a0c6cc89d6695cbd04f9f4757 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 18:00:19 +0530 Subject: [PATCH 018/136] fix: Get MRs that are yet to be received but fully ordered in Report `Requested Items to Order and Receive` (backport #29987) (#30083) * fix: Get MRs that are yet to be received but fully ordered in Report - Remove incorrect query clause that only check if ordered qty < 100 - MR should be visible in report until fully received (cycle complete) (cherry picked from commit d3b0ca30c6ae0e979b7bdddbe67018941be8d59b) * refactor: Convert to QB, added test file, removed white space - Converted mysql raw query to qb - Test file for Report Requested Items to Order and Receive - Removed white space and edited copyright year (cherry picked from commit e6952cb7f993c37d4f71be4ba6779c94257656f6) * fix: Sider and Linter (cherry picked from commit ac425722e206465c34d4029b3e959ac726ebd0ef) * fix: linter (imports alphabetical) (cherry picked from commit 54b3676f35579840c35fce5690f0202e590dd424) Co-authored-by: marination --- .../requested_items_to_order_and_receive.py | 103 ++++++++++-------- ...st_requested_items_to_order_and_receive.py | 69 ++++++++++++ .../material_request/test_material_request.py | 14 +-- .../report/stock_ageing/test_stock_ageing.py | 2 +- 4 files changed, 136 insertions(+), 52 deletions(-) create mode 100644 erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index f98e5f12c2d..60a8f92cc34 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -6,6 +6,7 @@ import copy import frappe from frappe import _ +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import date_diff, flt, getdate @@ -16,12 +17,9 @@ def execute(filters=None): validate_filters(filters) columns = get_columns(filters) - conditions = get_conditions(filters) + data = get_data(filters) - #get queried data - data = get_data(filters, conditions) - - #prepare data for report and chart views + # prepare data for report and chart views data, chart_data = prepare_data(data, filters) return columns, data, None, chart_data @@ -34,53 +32,70 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) -def get_conditions(filters): - conditions = '' +def get_data(filters): + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + query = ( + frappe.qb.from_(mr) + .join(mr_item).on(mr_item.parent == mr.name) + .select( + mr.name.as_("material_request"), + mr.transaction_date.as_("date"), + mr_item.schedule_date.as_("required_date"), + mr_item.item_code.as_("item_code"), + Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"), + Coalesce(mr_item.stock_uom, '').as_("uom"), + Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), + Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), + ( + Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0)) + ).as_("qty_to_receive"), + Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), + ( + Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0)) + ).as_("qty_to_order"), + mr_item.item_name, + mr_item.description, + mr.company + ).where( + (mr.material_request_type == "Purchase") + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.per_received < 100) + ) + ) + + query = get_conditions(filters, query, mr, mr_item) # add conditional conditions + + query = ( + query.groupby( + mr.name, mr_item.item_code + ).orderby( + mr.transaction_date, mr.schedule_date + ) + ) + data = query.run(as_dict=True) + return data + +def get_conditions(filters, query, mr, mr_item): if filters.get("from_date") and filters.get("to_date"): - conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date")) - + query = ( + query.where( + (mr.transaction_date >= filters.get("from_date")) + & (mr.transaction_date <= filters.get("to_date")) + ) + ) if filters.get("company"): - conditions += " and mr.company = '{0}'".format(filters.get("company")) + query = query.where(mr.company == filters.get("company")) if filters.get("material_request"): - conditions += " and mr.name = '{0}'".format(filters.get("material_request")) + query = query.where(mr.name == filters.get("material_request")) if filters.get("item_code"): - conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code")) + query = query.where(mr_item.item_code == filters.get("item_code")) - return conditions - -def get_data(filters, conditions): - data = frappe.db.sql(""" - select - mr.name as material_request, - mr.transaction_date as date, - mr_item.schedule_date as required_date, - mr_item.item_code as item_code, - sum(ifnull(mr_item.stock_qty, 0)) as qty, - ifnull(mr_item.stock_uom, '') as uom, - sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty, - sum(ifnull(mr_item.received_qty, 0)) as received_qty, - (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive, - (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order, - mr_item.item_name as item_name, - mr_item.description as "description", - mr.company as company - from - `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where - mr_item.parent = mr.name - and mr.material_request_type = "Purchase" - and mr.docstatus = 1 - and mr.status != "Stopped" - {conditions} - group by mr.name, mr_item.item_code - having - sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0)) - order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1) - - return data + return query def update_qty_columns(row_to_update, data_row): fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py new file mode 100644 index 00000000000..f3c751c5c3c --- /dev/null +++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py @@ -0,0 +1,69 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, today + +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import ( + get_data, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.material_request.material_request import make_purchase_order + + +class TestRequestedItemsToOrderAndReceive(FrappeTestCase): + def setUp(self) -> None: + create_item("Test MR Report Item") + self.setup_material_request() # to order and receive + self.setup_material_request(order=True) # to receive (ordered) + self.setup_material_request(order=True, receive=True) # complete (ordered & received) + + self.filters = frappe._dict( + company="_Test Company", from_date=today(), to_date=add_days(today(), 30), + item_code="Test MR Report Item" + ) + + def tearDown(self) -> None: + frappe.db.rollback() + + def test_date_range(self): + data = get_data(self.filters) + self.assertEqual(len(data), 2) # MRs today should be fetched + + self.filters.from_date = add_days(today(), 1) + data = get_data(self.filters) + self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow + + def test_ordered_received_material_requests(self): + data = get_data(self.filters) + + # from the 3 MRs made, only 2 (to receive) should be fetched + self.assertEqual(len(data), 2) + self.assertEqual(data[0].ordered_qty, 0.0) + self.assertEqual(data[1].ordered_qty, 57.0) + + def setup_material_request(self, order=False, receive=False): + po = None + test_records = frappe.get_test_records('Material Request') + + mr = frappe.copy_doc(test_records[0]) + mr.transaction_date = today() + mr.schedule_date = add_days(today(), 1) + for row in mr.items: + row.item_code = "Test MR Report Item" + row.item_name = "Test MR Report Item" + row.description = "Test MR Report Item" + row.uom = "Nos" + row.schedule_date = add_days(today(), 1) + mr.submit() + + if order or receive: + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.submit() + if receive: + pr = make_purchase_receipt(po.name) + pr.submit() + diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 383b0ae806e..705ef27b37a 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -626,13 +626,13 @@ class TestMaterialRequest(ERPNextTestCase): mr.schedule_date = today() if not frappe.db.get_value('UOM Conversion Detail', - {'parent': item.item_code, 'uom': 'Kg'}): - item_doc = frappe.get_doc('Item', item.item_code) - item_doc.append('uoms', { - 'uom': 'Kg', - 'conversion_factor': 5 - }) - item_doc.save(ignore_permissions=True) + {'parent': item.item_code, 'uom': 'Kg'}): + item_doc = frappe.get_doc('Item', item.item_code) + item_doc.append('uoms', { + 'uom': 'Kg', + 'conversion_factor': 5 + }) + item_doc.save(ignore_permissions=True) item.uom = 'Kg' for item in mr.items: diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 3fc357e8d4f..562d178c329 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe From e8f17e582bf11d45cd3da4e197af88337a124834 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 23:25:59 +0530 Subject: [PATCH 019/136] test: Fix flaky tests (backport #30107) (#30119) Co-authored-by: Chillar Anand Co-authored-by: Rucha Mahabal --- erpnext/hr/doctype/attendance/test_attendance.py | 15 ++++++++------- .../leave_application/test_leave_application.py | 2 +- .../test_project_profitability.py | 6 ++---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index 118cc987efb..c74967d213e 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -1,10 +1,9 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt -import unittest - import frappe -from frappe.utils import add_days, get_first_day, getdate, nowdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate from erpnext.hr.doctype.attendance.attendance import ( get_month_map, @@ -16,7 +15,7 @@ from erpnext.hr.doctype.leave_application.test_leave_application import get_firs test_records = frappe.get_test_records('Attendance') -class TestAttendance(unittest.TestCase): +class TestAttendance(FrappeTestCase): def test_mark_absent(self): employee = make_employee("test_mark_absent@example.com") date = nowdate() @@ -74,12 +73,14 @@ class TestAttendance(unittest.TestCase): self.assertNotIn(first_sunday, unmarked_days) def test_unmarked_days_as_per_joining_and_relieving_dates(self): - first_day = get_first_day(getdate()) + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() doj = add_days(first_day, 1) relieving_date = add_days(first_day, 5) employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj, - date_of_relieving=relieving_date) + relieving_date=relieving_date) frappe.db.delete('Attendance', {'employee': employee}) attendance_date = add_days(first_day, 2) @@ -104,4 +105,4 @@ def get_month_name(date): month_number = date.month for month, number in get_month_map().items(): if number == month_number: - return month \ No newline at end of file + return month diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 39356bdcf18..38b8eef0c9b 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -791,4 +791,4 @@ def get_first_sunday(holiday_list): order by holiday_date """, (holiday_list, month_start_date, month_end_date))[0][0] - return first_sunday \ No newline at end of file + return first_sunday diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 3396a2193cf..c80f301962c 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -2,6 +2,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee @@ -13,7 +14,7 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_ from erpnext.projects.report.project_profitability.project_profitability import execute -class TestProjectProfitability(unittest.TestCase): +class TestProjectProfitability(FrappeTestCase): def setUp(self): frappe.db.sql('delete from `tabTimesheet`') emp = make_employee('test_employee_9@salary.com', company='_Test Company') @@ -68,6 +69,3 @@ class TestProjectProfitability(unittest.TestCase): fractional_cost = self.salary_slip.base_gross_pay * utilization self.assertEqual(fractional_cost, row.fractional_cost) - - def tearDown(self): - frappe.db.rollback() From 07b8dd030071fe197936ba34c375d9b86bfd618c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 23:48:44 +0530 Subject: [PATCH 020/136] fix: leave allocation records query (backport #30118) (#30120) Co-authored-by: Rucha Mahabal --- .../leave_application/leave_application.py | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 70250f5bcf8..ef5f4bcb0ff 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Max, Min, Sum from frappe.utils import ( add_days, cint, @@ -567,28 +568,39 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ return get_remaining_leaves(allocation, leaves_taken, date, expiry) def get_leave_allocation_records(employee, date, leave_type=None): - ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries ''' + """Returns the total allocated leaves and carry forwarded leaves based on ledger entries""" + Ledger = frappe.qb.DocType("Leave Ledger Entry") - conditions = ("and leave_type='%s'" % leave_type) if leave_type else "" - allocation_details = frappe.db.sql(""" - SELECT - SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves, - SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves, - MIN(from_date) as from_date, - MAX(to_date) as to_date, - leave_type - FROM `tabLeave Ledger Entry` - WHERE - from_date <= %(date)s - AND to_date >= %(date)s - AND docstatus=1 - AND transaction_type="Leave Allocation" - AND employee=%(employee)s - AND is_expired=0 - AND is_lwp=0 - {0} - GROUP BY employee, leave_type - """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec + cf_leave_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0) + sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves") + + new_leaves_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0) + sum_new_leaves = Sum(new_leaves_case).as_("new_leaves") + + query = ( + frappe.qb.from_(Ledger) + .select( + sum_cf_leaves, + sum_new_leaves, + Min(Ledger.from_date).as_("from_date"), + Max(Ledger.to_date).as_("to_date"), + Ledger.leave_type + ).where( + (Ledger.from_date <= date) + & (Ledger.to_date >= date) + & (Ledger.docstatus == 1) + & (Ledger.transaction_type == "Leave Allocation") + & (Ledger.employee == employee) + & (Ledger.is_expired == 0) + & (Ledger.is_lwp == 0) + ) + ) + + if leave_type: + query = query.where((Ledger.leave_type == leave_type)) + query = query.groupby(Ledger.employee, Ledger.leave_type) + + allocation_details = query.run(as_dict=True) allocated_leaves = frappe._dict() for d in allocation_details: From 3aace2f420e9cdf069dc1c1a4eb112779f7b276c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Mar 2022 10:11:01 +0530 Subject: [PATCH 021/136] fix: Reload doctypes --- erpnext/patches/v13_0/update_accounts_in_loan_docs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py index 440f912be21..0b26f55e002 100644 --- a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py +++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py @@ -2,6 +2,11 @@ import frappe def execute(): + + frappe.reload_doc('loan_management', 'doctype', 'loan') + frappe.reload_doc('loan_management', 'doctype', 'loan_disbursement') + frappe.reload_doc('loan_management', 'doctype', 'loan_repayment') + ld = frappe.qb.DocType("Loan Disbursement").as_("ld") lr = frappe.qb.DocType("Loan Repayment").as_("lr") loan = frappe.qb.DocType("Loan") From b9ccbd50b4427600f42ed9d471b00ff28fdac8d9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Mar 2022 22:31:42 +0530 Subject: [PATCH 022/136] fix: Ambigous column in picklist query (cherry picked from commit 517fbf1d1f0a7d44e817b3f22ae30142e7bdf4c8) --- erpnext/stock/doctype/pick_list/pick_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index b7987543f2b..86f24c2427c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -273,9 +273,9 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY - `warehouse`, - `batch_no`, - `item_code` + sle.`warehouse`, + sle.`batch_no`, + sle.`item_code` HAVING `qty` > 0 ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` """.format(warehouse_condition=warehouse_condition), { #nosec From 95792d8f32fbed533e7603b02968b356aff70120 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Mar 2022 07:56:20 +0530 Subject: [PATCH 023/136] fix: Remove unintentional changes (cherry picked from commit d9d4c2ce792304a528a7b8a27d50259a04bdd7ca) --- erpnext/stock/doctype/pick_list/pick_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 86f24c2427c..b2eaecb5868 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -9,7 +9,6 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.utils import cint, floor, flt, today -from six import iteritems from erpnext.selling.doctype.sales_order.sales_order import ( make_delivery_note as create_delivery_note_from_sales_order, @@ -246,7 +245,7 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses, warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no) locations = [] - for warehouse, serial_nos in iteritems(warehouse_serial_nos_map): + for warehouse, serial_nos in warehouse_serial_nos_map.items(): locations.append({ 'qty': len(serial_nos), 'warehouse': warehouse, From 8fc105a18686dee0b3391bd29e28d942674dc4a7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 6 Mar 2022 19:05:02 +0530 Subject: [PATCH 024/136] fix: Item-wise sales history report (cherry picked from commit e2c144e9e3e6bd71f6040dc81e698ffe2f0f37d2) --- .../item_wise_sales_history.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 4a245e1f778..56e1eb57b81 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -156,24 +156,24 @@ def get_data(filters): customer_record = customer_details.get(record.customer) item_record = item_details.get(record.item_code) row = { - "item_code": record.item_code, - "item_name": item_record.item_name, - "item_group": item_record.item_group, - "description": record.description, - "quantity": record.qty, - "uom": record.uom, - "rate": record.base_rate, - "amount": record.base_amount, - "sales_order": record.name, - "transaction_date": record.transaction_date, - "customer": record.customer, - "customer_name": customer_record.customer_name, - "customer_group": customer_record.customer_group, - "territory": record.territory, - "project": record.project, - "delivered_quantity": flt(record.delivered_qty), - "billed_amount": flt(record.billed_amt), - "company": record.company + "item_code": record.get('item_code'), + "item_name": item_record.get('item_name'), + "item_group": item_record.get('item_group'), + "description": record.get('description'), + "quantity": record.get('qty'), + "uom": record.get('uom'), + "rate": record.get('base_rate'), + "amount": record.get('base_amount'), + "sales_order": record.get('name'), + "transaction_date": record.get('transaction_date'), + "customer": record.get('customer'), + "customer_name": customer_record.get('customer_name'), + "customer_group": customer_record.get('customer_group'), + "territory": record.get('territory'), + "project": record.get('project'), + "delivered_quantity": flt(record.get('delivered_qty')), + "billed_amount": flt(record.get('billed_amt')), + "company": record.get('company') } data.append(row) From 3d8eaa5392fcc420820ac86eb01a6cf67cec1cc7 Mon Sep 17 00:00:00 2001 From: Mohamed-D-Ouf <65343412+Mohamed-D-Ouf@users.noreply.github.com> Date: Wed, 9 Mar 2022 08:55:36 +0300 Subject: [PATCH 025/136] fix: add filters in default_discount_account (#30095) --- erpnext/setup/doctype/item_group/item_group.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index 885d874720d..f570c2faec6 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -14,6 +14,16 @@ frappe.ui.form.on("Item Group", { ] } } + frm.fields_dict['item_group_defaults'].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) { + const row = locals[cdt][cdn]; + return { + filters: { + 'report_type': 'Profit and Loss', + 'company': row.company, + "is_group": 0 + } + }; + } frm.fields_dict["item_group_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) { const row = locals[cdt][cdn]; return { From 4a5ea559656432e757ec4b3632565e382624bdc8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Mar 2022 16:10:26 +0530 Subject: [PATCH 026/136] fix(ux): Improve label for better understanding (cherry picked from commit f3a95d3c27a5430c4a9176b685da20489a7e2429) --- .../doctype/sales_invoice/sales_invoice.json | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 5062c1c807a..973c8371ea2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2,13 +2,14 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-05-24 19:29:05", + "creation": "2022-01-25 10:29:57.771398", "doctype": "DocType", "engine": "InnoDB", "field_order": [ "customer_section", "title", "naming_series", + "tax_invoice_number", "customer", "customer_name", "tax_id", @@ -651,7 +652,6 @@ "hide_seconds": 1, "label": "Ignore Pricing Rule", "no_copy": 1, - "permlevel": 0, "print_hide": 1 }, { @@ -1974,9 +1974,10 @@ }, { "default": "0", + "description": "Issue a debit note with 0 qty against an existing Sales Invoice", "fieldname": "is_debit_note", "fieldtype": "Check", - "label": "Is Debit Note" + "label": "Is Rate Adjustment Entry (Debit Note)" }, { "default": "0", @@ -2026,6 +2027,12 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "fieldname": "tax_invoice_number", + "fieldtype": "Data", + "label": "Tax Invoice Number", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -2038,7 +2045,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-12-23 20:19:38.667508", + "modified": "2022-03-07 16:08:53.517903", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2089,8 +2096,9 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file From 58f41722b657d719341680e0aff01fa36e4a79b4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 15:38:26 +0530 Subject: [PATCH 027/136] fix: dont fetch entire barcode table in get_item_details (#30131) (#30134) (cherry picked from commit 64905188c46b90452e30927323f892d8b210f14a) Co-authored-by: Ankush Menat --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e7b4ca2de38..59f02e36114 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -360,7 +360,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not out[d[1]]: out[d[1]] = frappe.get_cached_value('Company', args.company, d[2]) if d[2] else None - for fieldname in ("item_name", "item_group", "barcodes", "brand", "stock_uom"): + for fieldname in ("item_name", "item_group", "brand", "stock_uom"): out[fieldname] = item.get(fieldname) if args.get("manufacturer"): From 856534c8a08f34915ebe27f798c25bd88068d556 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Mar 2022 15:42:06 +0530 Subject: [PATCH 028/136] fix: Remove tax invoice no field (cherry picked from commit 8a2fe7a2e39c28ccb52238651b439eba17d153ab) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 973c8371ea2..82854ba2a6c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -9,7 +9,6 @@ "customer_section", "title", "naming_series", - "tax_invoice_number", "customer", "customer_name", "tax_id", @@ -2027,12 +2026,6 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 - }, - { - "fieldname": "tax_invoice_number", - "fieldtype": "Data", - "label": "Tax Invoice Number", - "read_only": 1 } ], "icon": "fa fa-file-text", From 8d7418171bf7e7d228f20c32edc85c3fa0439373 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Mar 2022 15:43:26 +0530 Subject: [PATCH 029/136] fix: Update timestamp (cherry picked from commit 9b8258479c6e71a06303d4774df5ab3a749d9de9) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 82854ba2a6c..80b95db8868 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2038,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-03-07 16:08:53.517903", + "modified": "2022-03-08 16:08:53.517903", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 7f234c7e3423767be26c36f1ac7bb628ff30bc9e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Mar 2022 13:02:08 +0530 Subject: [PATCH 030/136] feat: Include child item group products in Item Group Page & cleanup - Added 'Include descendants' checkbox, which will pull child item group products too - Build item group filters in query engine file - Include logic in filter engine - Clean up Website section of Item Group page (UX) - Add util to fetch child item groups including self (cherry picked from commit b2755f6fdddd3e1b0a305b57c18651c98fee8f7e) --- erpnext/e_commerce/api.py | 1 - .../e_commerce/product_data_engine/filters.py | 22 ++++++--- .../e_commerce/product_data_engine/query.py | 48 ++++++++----------- .../setup/doctype/item_group/item_group.json | 31 +++++++++--- .../setup/doctype/item_group/item_group.py | 8 +++- 5 files changed, 69 insertions(+), 41 deletions(-) diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py index 3ad1da4a92f..b363e0f56dd 100644 --- a/erpnext/e_commerce/api.py +++ b/erpnext/e_commerce/api.py @@ -47,7 +47,6 @@ def get_product_filter_data(query_args=None): sub_categories = [] if item_group: - field_filters['item_group'] = item_group sub_categories = get_child_groups_for_website(item_group, immediate=True) engine = ProductQuery() diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py index 6d44b2cb977..ad03c4b286c 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -14,6 +14,8 @@ class ProductFiltersBuilder: self.item_group = item_group def get_field_filters(self): + from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website + if not self.item_group and not self.doc.enable_field_filters: return @@ -25,18 +27,26 @@ class ProductFiltersBuilder: fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] for df in fields: - item_filters, item_or_filters = {}, [] + item_filters, item_or_filters = {"published_in_website": 1}, [] link_doctype_values = self.get_filtered_link_doctype_records(df) if df.fieldtype == "Link": if self.item_group: - item_or_filters.extend([ - ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups - ]) + include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants") + if include_child: + include_groups = get_child_groups_for_website(self.item_group, include_self=True) + include_groups = [x.name for x in include_groups] + item_or_filters.extend([ + ["item_group", "in", include_groups], + ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups + ]) + else: + item_or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups + ]) # Get link field values attached to published items - item_filters['published_in_website'] = 1 item_values = frappe.get_all( "Item", fields=[df.fieldname], diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index 1a2ddeb0251..27e37ce6fe5 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -46,10 +46,10 @@ class ProductQuery: self.filter_with_discount = bool(fields.get("discount")) result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 - website_item_groups = self.get_website_item_group_results(item_group, website_item_groups) - if fields: self.build_fields_filters(fields) + if item_group: + self.build_item_group_filters(item_group) if search_term: self.build_search_filters(search_term) if self.settings.hide_variants: @@ -61,8 +61,6 @@ class ProductQuery: else: result, count = self.query_items(start=start) - result = self.combine_web_item_group_results(item_group, result, website_item_groups) - # sort combined results by ranking result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) @@ -168,6 +166,25 @@ class ProductQuery: # `=` will be faster than `IN` for most cases self.filters.append([field, "=", values]) + def build_item_group_filters(self, item_group): + "Add filters for Item group page and include Website Item Groups." + from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website + item_group_filters = [] + + item_group_filters.append(["Website Item", "item_group", "=", item_group]) + # Consider Website Item Groups + item_group_filters.append(["Website Item Group", "item_group", "=", item_group]) + + if frappe.db.get_value("Item Group", item_group, "include_descendants"): + # include child item group's items as well + # eg. Group Node A, will show items of child 1 and child 2 as well + # on it's web page + include_groups = get_child_groups_for_website(item_group, include_self=True) + include_groups = [x.name for x in include_groups] + item_group_filters.append(["Website Item", "item_group", "in", include_groups]) + + self.or_filters.extend(item_group_filters) + def build_search_filters(self, search_term): """Query search term in specified fields @@ -191,19 +208,6 @@ class ProductQuery: for field in search_fields: self.or_filters.append([field, "like", search]) - def get_website_item_group_results(self, item_group, website_item_groups): - """Get Web Items for Item Group Page via Website Item Groups.""" - if item_group: - website_item_groups = frappe.db.get_all( - "Website Item", - fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], - filters=[ - ["Website Item Group", "item_group", "=", item_group], - ["published", "=", 1] - ] - ) - return website_item_groups - def add_display_details(self, result, discount_list, cart_items): """Add price and availability details in result.""" for item in result: @@ -279,16 +283,6 @@ class ProductQuery: return [] - def combine_web_item_group_results(self, item_group, result, website_item_groups): - """Combine results with context of website item groups into item results.""" - if item_group and website_item_groups: - items_list = {row.name for row in result} - for row in website_item_groups: - if row.wig_parent not in items_list: - result.append(row) - - return result - def filter_results_by_discount(self, fields, result): if fields and fields.get("discount"): discount_percent = frappe.utils.flt(fields["discount"][0]) diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 3e0680f4f51..a090c8d76c5 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -20,12 +20,14 @@ "sec_break_taxes", "taxes", "sb9", - "show_in_website", "route", - "weightage", - "slideshow", "website_title", "description", + "show_in_website", + "include_descendants", + "column_break_16", + "weightage", + "slideshow", "website_specifications", "website_filters_section", "filter_fields", @@ -111,7 +113,7 @@ }, { "default": "0", - "description": "Check this if you want to show in website", + "description": "Make Item Group visible in website", "fieldname": "show_in_website", "fieldtype": "Check", "label": "Show in Website" @@ -124,6 +126,7 @@ "unique": 1 }, { + "depends_on": "show_in_website", "fieldname": "weightage", "fieldtype": "Int", "label": "Weightage" @@ -186,6 +189,8 @@ "report_hide": 1 }, { + "collapsible": 1, + "depends_on": "show_in_website", "fieldname": "website_filters_section", "fieldtype": "Section Break", "label": "Website Filters" @@ -203,9 +208,21 @@ "options": "Website Attribute" }, { + "depends_on": "show_in_website", "fieldname": "website_title", "fieldtype": "Data", "label": "Title" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Include Website Items belonging to child Item Groups", + "fieldname": "include_descendants", + "fieldtype": "Check", + "label": "Include Descendants" } ], "icon": "fa fa-sitemap", @@ -214,11 +231,12 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2021-02-18 13:40:30.049650", + "modified": "2022-03-07 09:44:47.561532", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", "name_case": "Title Case", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_item_group", "owner": "Administrator", "permissions": [ @@ -285,5 +303,6 @@ "search_fields": "parent_item_group", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 7695affde60..5bcd0e4e21e 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -112,7 +112,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): from erpnext.stock.doctype.item.item import validate_item_default_company_links validate_item_default_company_links(self.item_group_defaults) -def get_child_groups_for_website(item_group_name, immediate=False): +def get_child_groups_for_website(item_group_name, immediate=False, include_self=False): """Returns child item groups *excluding* passed group.""" item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) filters = { @@ -124,6 +124,12 @@ def get_child_groups_for_website(item_group_name, immediate=False): if immediate: filters["parent_item_group"] = item_group_name + if include_self: + filters.update({ + "lft": [">=", item_group.lft], + "rgt": ["<=", item_group.rgt] + }) + return frappe.get_all( "Item Group", filters=filters, From c8026eb9df98e6bd7fd5bf07033ad6933ff4f042 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Mar 2022 12:24:57 +0530 Subject: [PATCH 031/136] test: Test include_descendants in Item Group Product Listing - Also made include_descendants field's visibility dependant on show_in_website (cherry picked from commit 3507cf59852c6d6814f6650b4b1a6e6584e69aa6) --- .../test_item_group_product_data_engine.py | 53 ++++++++++++++++--- .../setup/doctype/item_group/item_group.json | 3 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py index f0f7918d00e..6549ba692af 100644 --- a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py @@ -13,8 +13,7 @@ test_dependencies = ["Item", "Item Group"] class TestItemGroupProductDataEngine(unittest.TestCase): "Test Products & Sub-Category Querying for Product Listing on Item Group Page." - @classmethod - def setUpClass(cls): + def setUp(self): item_codes = [ ("Test Mobile A", "_Test Item Group B"), ("Test Mobile B", "_Test Item Group B"), @@ -28,8 +27,10 @@ class TestItemGroupProductDataEngine(unittest.TestCase): if not frappe.db.exists("Website Item", {"item_code": item_code}): create_regular_web_item(item_code, item_args=item_args) - @classmethod - def tearDownClass(cls): + frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) + frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) + + def tearDown(self): frappe.db.rollback() def test_product_listing_in_item_group(self): @@ -87,7 +88,6 @@ class TestItemGroupProductDataEngine(unittest.TestCase): def test_item_group_with_sub_groups(self): "Test Valid Sub Item Groups in Item Group Page." - frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) result = get_product_filter_data(query_args={ @@ -114,4 +114,45 @@ class TestItemGroupProductDataEngine(unittest.TestCase): # check if child group is fetched if shown in website self.assertIn("_Test Item Group B - 1", child_groups) - self.assertIn("_Test Item Group B - 2", child_groups) \ No newline at end of file + self.assertIn("_Test Item Group B - 2", child_groups) + + def test_item_group_page_with_descendants_included(self): + """ + Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3). + > _Test Item Group B [Level 1] + > _Test Item Group B - 1 [Level 2] + > _Test Item Group B - 1 - 1 [Level 3] + """ + frappe.get_doc({ # create Level 3 nested child group + "doctype": "Item Group", + "is_group": 1, + "item_group_name": "_Test Item Group B - 1 - 1", + "parent_item_group": "_Test Item Group B - 1" + }).insert() + + create_regular_web_item( # create an item belonging to level 3 item group + "Test Mobile F", + item_args={"item_group": "_Test Item Group B - 1 - 1"} + ) + + frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1) + + # enable 'include descendants' in Level 1 + frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1) + + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B" + }) + + items = result.get("items") + item_codes = [item.get("item_code") for item in items] + + # check if all sub groups' items are pulled + self.assertEqual(len(items), 6) + self.assertIn("Test Mobile A", item_codes) + self.assertIn("Test Mobile C", item_codes) + self.assertIn("Test Mobile E", item_codes) + self.assertIn("Test Mobile F", item_codes) \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index a090c8d76c5..50f923d87e0 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -219,6 +219,7 @@ }, { "default": "0", + "depends_on": "show_in_website", "description": "Include Website Items belonging to child Item Groups", "fieldname": "include_descendants", "fieldtype": "Check", @@ -231,7 +232,7 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2022-03-07 09:44:47.561532", + "modified": "2022-03-09 12:27:11.055782", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", From ca41263736d083ac83acce73680c27292a42bc0c Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Mar 2022 16:04:12 +0530 Subject: [PATCH 032/136] fix: Linter (cherry picked from commit 7b37a74023b088b8dcc5114b954c716ebf7f6eae) --- erpnext/assets/doctype/asset/asset_dashboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index c9efe3d0848..cae34c93f7e 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -1,5 +1,6 @@ + def get_data(): return { 'non_standard_fieldnames': { From 51f7ca856b544f0eed500b0d2ae9c89743e7f82a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 16:51:58 +0530 Subject: [PATCH 033/136] =?UTF-8?q?revert:=20BU=20Schl=C3=BCssel=20(a21f76?= =?UTF-8?q?f)=20(#30142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 363ed9ccba3f848908113e6d728735a1c894aec8) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- erpnext/regional/report/datev/datev.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index adc77e8921c..5d6e8dff3fa 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -345,8 +345,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): /* against number or, if empty, party against number */ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', - /* disable automatic VAT deduction */ - '40' as 'BU-Schlüssel', + '' as 'BU-Schlüssel', gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', From f721a2929f88d3309ba730a552d40dc3f9eccf2a Mon Sep 17 00:00:00 2001 From: hrzzz Date: Fri, 25 Feb 2022 16:56:23 -0300 Subject: [PATCH 034/136] fix(translation) - correction for translation (cherry picked from commit 16de29a3cb13b771d41f3b26a2d80de8d2871b92) # Conflicts: # erpnext/assets/doctype/asset/asset_dashboard.py --- erpnext/assets/doctype/asset/asset_dashboard.py | 6 +++++- .../assets/doctype/asset_maintenance/asset_maintenance.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index c9efe3d0848..461a67dbfc6 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -1,4 +1,8 @@ +<<<<<<< HEAD +======= +from frappe import _ +>>>>>>> 16de29a3cb (fix(translation) - correction for translation) def get_data(): return { @@ -7,7 +11,7 @@ def get_data(): }, 'transactions': [ { - 'label': ['Movement'], + 'label': _('Movement'), 'items': ['Asset Movement'] } ] diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 52996e93475..5c03b98873b 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -48,7 +48,7 @@ frappe.ui.form.on('Asset Maintenance', { `).appendTo(rows); From 4b4b1048436c745892226f5c52471e858377346d Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 9 Mar 2022 17:29:38 +0530 Subject: [PATCH 035/136] fix: Merge conflicts --- erpnext/assets/doctype/asset/asset_dashboard.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index 461a67dbfc6..c81b611a418 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -1,8 +1,5 @@ -<<<<<<< HEAD - -======= from frappe import _ ->>>>>>> 16de29a3cb (fix(translation) - correction for translation) + def get_data(): return { From d68cf1885f3c005bd08affd33c1fd3816b27f958 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:32:29 +0530 Subject: [PATCH 036/136] test: Added test for monthly attendance report (backport #29989) (#30143) Co-authored-by: Rucha Mahabal Co-authored-by: Chillar Anand --- .../test_monthly_attendance_sheet.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py new file mode 100644 index 00000000000..b196fb5b989 --- /dev/null +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -0,0 +1,45 @@ +import frappe +from dateutil.relativedelta import relativedelta + +from frappe.tests.utils import FrappeTestCase +from frappe.utils import now_datetime + +from erpnext.hr.doctype.attendance.attendance import mark_attendance +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute + + +class TestMonthlyAttendanceSheet(FrappeTestCase): + def setUp(self): + self.employee = make_employee("test_employee@example.com") + frappe.db.delete('Attendance', {'employee': self.employee}) + + def test_monthly_attendance_sheet_report(self): + now = now_datetime() + previous_month = now.month - 1 + previous_month_first = now.replace(day=1).replace(month=previous_month).date() + + company = frappe.db.get_value('Employee', self.employee, 'company') + + # mark different attendance status on first 3 days of previous month + mark_attendance(self.employee, previous_month_first, 'Absent') + mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present') + mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave') + + filters = frappe._dict({ + 'month': previous_month, + 'year': now.year, + 'company': company, + }) + report = execute(filters=filters) + employees = report[1][0] + datasets = report[3]['data']['datasets'] + absent = datasets[0]['values'] + present = datasets[1]['values'] + leaves = datasets[2]['values'] + + # ensure correct attendance is reflect on the report + self.assertIn(self.employee, employees) + self.assertEqual(absent[0], 1) + self.assertEqual(present[1], 1) + self.assertEqual(leaves[2], 1) From 458f264575c39caa9c8d19be1c6edfb26fc7b0c9 Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 9 Mar 2022 17:35:01 +0530 Subject: [PATCH 037/136] fix: Remove extra line --- erpnext/assets/doctype/asset/asset_dashboard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index daa24207a55..c81b611a418 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -1,7 +1,6 @@ from frappe import _ - def get_data(): return { 'non_standard_fieldnames': { From 63e7d8c6512b870ab108f64bb88c71d14d9ad164 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:48:49 +0530 Subject: [PATCH 038/136] perf(asset): fetch only distinct depreciable assets (#30138) (cherry picked from commit 66f20209f657ae1825bc9971dbd19f186e454d50) Co-authored-by: Saqib Ansari --- erpnext/assets/doctype/asset/depreciation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 874fb630f87..6e042422101 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -23,7 +23,7 @@ def post_depreciation_entries(date=None): frappe.db.commit() def get_depreciable_assets(date): - return frappe.db.sql_list("""select a.name + return frappe.db.sql_list("""select distinct a.name from tabAsset a, `tabDepreciation Schedule` ds where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 and a.status in ('Submitted', 'Partially Depreciated') From eb90d50946a29cf4a8c7983848022fbab9a22d04 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:49:11 +0530 Subject: [PATCH 039/136] fix: ignore non-unique swift numbers while migrating (#30139) (cherry picked from commit 4d8798b0ead57eb3ae3a0b27755aad60a84989b9) Co-authored-by: Saqib Ansari --- .../v12_0/move_bank_account_swift_number_to_bank.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py index c0b262395d8..bca24c8dcd4 100644 --- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py +++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py @@ -6,10 +6,13 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'bank', force=1) if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): - frappe.db.sql(""" - UPDATE `tabBank` b, `tabBank Account` ba - SET b.swift_number = ba.swift_number WHERE b.name = ba.bank - """) + try: + frappe.db.sql(""" + UPDATE `tabBank` b, `tabBank Account` ba + SET b.swift_number = ba.swift_number WHERE b.name = ba.bank + """) + except Exception as e: + frappe.log_error(e, title="Patch Migration Failed") frappe.reload_doc('accounts', 'doctype', 'bank_account') frappe.reload_doc('accounts', 'doctype', 'payment_request') From e3387a06b61c9775b5cd522d4aea847e6b8ca135 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 17:49:34 +0530 Subject: [PATCH 040/136] fix: customer credit limit validation on update (#30126) (cherry picked from commit a0dc79332df5b2a567a13a0b052bfc9bdf4e3153) Co-authored-by: Saqib Ansari --- erpnext/selling/doctype/customer/customer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index df871491422..c78227e4c86 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -219,7 +219,9 @@ class Customer(TransactionBase): else: company_record.append(limit.company) - outstanding_amt = get_customer_outstanding(self.name, limit.company) + outstanding_amt = get_customer_outstanding( + self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check + ) if flt(limit.credit_limit) < outstanding_amt: frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt)) From 31ba09b3d0cd833551f3fcde94b108d3c3282f75 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Mar 2022 10:07:47 +0530 Subject: [PATCH 041/136] fix: program enrollment button labels (backport #30148) (#30149) Co-authored-by: Rucha Mahabal --- erpnext/www/lms/macros/hero.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html index 95ba8f7df28..dd3c23a0145 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -39,16 +39,13 @@ frappe.call(opts).then(res => { let success_dialog = new frappe.ui.Dialog({ title: __('Success'), - primary_action_label: __('View Program Content'), + primary_action_label: __('OK'), primary_action: function() { window.location.reload(); - }, - secondary_action: function() { - window.location.reload(); } }) success_dialog.show(); - success_dialog.set_message(__('You have successfully enrolled for the program ')); + success_dialog.set_message(__('You have successfully enrolled for the program.')); }) } From cef8820369184b8aace1361aa0098195926cc9f0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Mar 2022 18:01:10 +0530 Subject: [PATCH 042/136] fix(psoa): add company filter to account (cherry picked from commit fc42041f8fff7bd9f3b374992565bf3eccfaf43d) --- .../process_statement_of_accounts.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 088c190f451..29f2e98e779 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', { } } }); + frm.set_query("account", function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); if(frm.doc.__islocal){ frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value('to_date', frappe.datetime.get_today()); From 92a59fabe56b80ad1e5e201698e7e6f7b80ef92a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Mar 2022 13:26:11 +0530 Subject: [PATCH 043/136] chore: imports (#30157) Co-authored-by: Ankush Menat --- .../monthly_attendance_sheet/test_monthly_attendance_sheet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py index b196fb5b989..952af8117e2 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -1,6 +1,5 @@ import frappe from dateutil.relativedelta import relativedelta - from frappe.tests.utils import FrappeTestCase from frappe.utils import now_datetime From 6c21642f32d92fdf77cdfadedd8ca255306028ef Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Mar 2022 13:36:52 +0530 Subject: [PATCH 044/136] fix(psoa): no such element: dict object['account'] (#30156) --- .../process_statement_of_accounts.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index f8d191cc3f8..82705a9cea4 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -64,10 +64,10 @@ {{ frappe.format(row.account, {fieldtype: "Link"}) or " " }} - {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} - {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} {% endif %} From 5081438748f47634df4c4e3a39641389d9417cc5 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Thu, 10 Mar 2022 08:13:35 +0000 Subject: [PATCH 045/136] fix: do not reset asset_category (#29696) (cherry picked from commit 5193a637810268c7730e2ce386b2c234924cc28f) --- erpnext/stock/doctype/item/item.js | 16 ++++++++-------- erpnext/stock/doctype/item/item.py | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 1ce09f0152c..2a2eafbb391 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -165,21 +165,21 @@ frappe.ui.form.on("Item", { frm.set_value('has_batch_no', 0); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); - frm.call({ - method: "set_asset_naming_series", - doc: frm.doc, - callback: function() { + frappe.call({ + method: "erpnext.stock.doctype.item.item.get_asset_naming_series", + callback: function(r) { frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1); - frm.trigger("set_asset_naming_series"); + frm.events.set_asset_naming_series(frm, r.message); } }); frm.trigger('auto_create_assets'); }, - set_asset_naming_series: function(frm) { - if (frm.doc.__onload && frm.doc.__onload.asset_naming_series) { - frm.set_df_property("asset_naming_series", "options", frm.doc.__onload.asset_naming_series); + set_asset_naming_series: function(frm, asset_naming_series) { + if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) { + let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series; + frm.set_df_property("asset_naming_series", "options", naming_series); } }, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 7bc875ac12f..5ab2c4acfdd 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -50,15 +50,7 @@ class DataValidationError(frappe.ValidationError): class Item(Document): def onload(self): self.set_onload('stock_exists', self.stock_ledger_created()) - self.set_asset_naming_series() - - @frappe.whitelist() - def set_asset_naming_series(self): - if not hasattr(self, '_asset_naming_series'): - from erpnext.assets.doctype.asset.asset import get_asset_naming_series - self._asset_naming_series = get_asset_naming_series() - - self.set_onload('asset_naming_series', self._asset_naming_series) + self.set_onload('asset_naming_series', get_asset_naming_series()) def autoname(self): if frappe.db.get_default("item_naming_by") == "Naming Series": @@ -1000,7 +992,7 @@ def get_uom_conv_factor(uom, stock_uom): if uom == stock_uom: return 1.0 - from_uom, to_uom = uom, stock_uom # renaming for readability + from_uom, to_uom = uom, stock_uom # renaming for readability exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) if exact_match: @@ -1012,9 +1004,9 @@ def get_uom_conv_factor(uom, stock_uom): # This attempts to try and get conversion from intermediate UOM. # case: - # g -> mg = 1000 - # g -> kg = 0.001 - # therefore kg -> mg = 1000 / 0.001 = 1,000,000 + # g -> mg = 1000 + # g -> kg = 0.001 + # therefore kg -> mg = 1000 / 0.001 = 1,000,000 intermediate_match = frappe.db.sql(""" select (first.value / second.value) as value from `tabUOM Conversion Factor` first @@ -1073,3 +1065,11 @@ def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> Non frappe.bold(item_default.company), frappe.bold(frappe.unscrub(field)) ), title=_("Invalid Item Defaults")) + + +@frappe.whitelist() +def get_asset_naming_series(): + from erpnext.assets.doctype.asset.asset import get_asset_naming_series + + return get_asset_naming_series() + From bb0be1699b994b415d10b5ef9690f402e848e939 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Mon, 7 Mar 2022 16:53:59 +0530 Subject: [PATCH 046/136] fix: Ignore missing customer group while fetching price list (cherry picked from commit 9ace7d606cb5a63da8434200e9811d550de5cb5a) --- erpnext/accounts/party.py | 11 +++++------ erpnext/accounts/test_party.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 erpnext/accounts/test_party.py diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 907964720ff..f1862b31e7e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -152,7 +152,7 @@ def set_contact_details(party_details, party, party_type): def set_other_values(party_details, party, party_type): # copy - if party_type=="Customer": + if party_type == "Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] @@ -171,12 +171,11 @@ def get_default_price_list(party): return party.default_price_list if party.doctype == "Customer": - price_list = frappe.get_cached_value("Customer Group", - party.customer_group, "default_price_list") - if price_list: - return price_list + try: + return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") + except frappe.exceptions.DoesNotExistError: + return - return None def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list diff --git a/erpnext/accounts/test_party.py b/erpnext/accounts/test_party.py new file mode 100644 index 00000000000..f7a1a858ab8 --- /dev/null +++ b/erpnext/accounts/test_party.py @@ -0,0 +1,16 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.party import get_default_price_list + + +class PartyTestCase(FrappeTestCase): + def test_get_default_price_list_should_return_none_for_invalid_group(self): + customer = frappe.get_doc({ + 'doctype': 'Customer', + 'customer_name': 'test customer', + }).insert(ignore_permissions=True, ignore_mandatory=True) + customer.customer_group = None + customer.save() + price_list = get_default_price_list(customer) + assert price_list is None From 3f0a4d9921855f2dc51e6236e81373ea8f501168 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Mar 2022 13:54:43 +0530 Subject: [PATCH 047/136] fix: Ignore missing customer group while fetching price list (cherry picked from commit e5fb871ef4a9e738e3a5ddc6d0e8eb2dfdd2ea16) --- erpnext/accounts/party.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index f1862b31e7e..de34a879db7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -171,10 +171,7 @@ def get_default_price_list(party): return party.default_price_list if party.doctype == "Customer": - try: - return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") - except frappe.exceptions.DoesNotExistError: - return + return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list") def set_price_list(party_details, party, party_type, given_price_list, pos=None): From 4febcd86f80488a90359d9c6b7e3590332a3cb31 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Mar 2022 14:38:08 +0530 Subject: [PATCH 048/136] fix: flaky tests (backport #30154) (#30161) Co-authored-by: Rucha Mahabal --- .../hr/doctype/attendance/test_attendance.py | 16 +++++++++--- .../test_leave_application.py | 25 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index c74967d213e..6095413771c 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -25,7 +25,9 @@ class TestAttendance(FrappeTestCase): self.assertEqual(attendance, fetch_attendance) def test_unmarked_days(self): - first_day = get_first_day(getdate()) + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) frappe.db.delete('Attendance', {'employee': employee}) @@ -34,7 +36,7 @@ class TestAttendance(FrappeTestCase): holiday_list = make_holiday_list() frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) - first_sunday = get_first_sunday(holiday_list) + first_sunday = get_first_sunday(holiday_list, for_date=first_day) mark_attendance(employee, first_day, 'Present') month_name = get_month_name(first_day) @@ -49,7 +51,9 @@ class TestAttendance(FrappeTestCase): self.assertIn(first_sunday, unmarked_days) def test_unmarked_days_excluding_holidays(self): - first_day = get_first_day(getdate()) + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) frappe.db.delete('Attendance', {'employee': employee}) @@ -58,7 +62,7 @@ class TestAttendance(FrappeTestCase): holiday_list = make_holiday_list() frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) - first_sunday = get_first_sunday(holiday_list) + first_sunday = get_first_sunday(holiday_list, for_date=first_day) mark_attendance(employee, first_day, 'Present') month_name = get_month_name(first_day) @@ -83,6 +87,10 @@ class TestAttendance(FrappeTestCase): relieving_date=relieving_date) frappe.db.delete('Attendance', {'employee': employee}) + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + holiday_list = make_holiday_list() + frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) + attendance_date = add_days(first_day, 2) mark_attendance(employee, attendance_date, 'Present') month_name = get_month_name(first_day) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 38b8eef0c9b..c9f377dfaca 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -133,7 +133,9 @@ class TestLeaveApplication(unittest.TestCase): holiday_list = make_holiday_list() employee = get_employee() - frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) + original_holiday_list = employee.holiday_list + frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) + first_sunday = get_first_sunday(holiday_list) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) @@ -143,6 +145,8 @@ class TestLeaveApplication(unittest.TestCase): leave_application.cancel() + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_attendance_update_for_exclude_holidays(self): # Case 2: leave type with 'Include holidays within leaves as leaves' disabled frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) @@ -157,7 +161,8 @@ class TestLeaveApplication(unittest.TestCase): holiday_list = make_holiday_list() employee = get_employee() - frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) + original_holiday_list = employee.holiday_list + frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) # already marked attendance on a holiday should be deleted in this case @@ -177,7 +182,7 @@ class TestLeaveApplication(unittest.TestCase): attendance.flags.ignore_validate = True attendance.save() - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company) leave_application.reload() # holiday should be excluded while marking attendance self.assertEqual(leave_application.total_leave_days, 3) @@ -189,6 +194,8 @@ class TestLeaveApplication(unittest.TestCase): # attendance on non-holiday updated self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave") + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_block_list(self): self._clear_roles() @@ -327,7 +334,8 @@ class TestLeaveApplication(unittest.TestCase): employee = get_employee() default_holiday_list = make_holiday_list() - frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list) + original_holiday_list = employee.holiday_list + frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list) first_sunday = get_first_sunday(default_holiday_list) optional_leave_date = add_days(first_sunday, 1) @@ -378,6 +386,8 @@ class TestLeaveApplication(unittest.TestCase): # check leave balance is reduced self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9) + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_leaves_allowed(self): employee = get_employee() leave_period = get_leave_period() @@ -781,9 +791,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el allocate_leave.submit() -def get_first_sunday(holiday_list): - month_start_date = get_first_day(nowdate()) - month_end_date = get_last_day(nowdate()) +def get_first_sunday(holiday_list, for_date=None): + date = for_date or getdate() + month_start_date = get_first_day(date) + month_end_date = get_last_day(date) first_sunday = frappe.db.sql(""" select holiday_date from `tabHoliday` where parent = %s From 62b50b6b638ac2c82170277d75fad6c17bcfb127 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Mar 2022 13:54:00 +0530 Subject: [PATCH 049/136] fix: 'save_quotations_as_draft' checkbox not honoured - Make sure `request_for_quotation` considers `save_quotations_as_draft` - Added test for checkout disabled quote (cherry picked from commit a13e06156b3c195d2340dafcebe0f12d2c95dba8) # Conflicts: # erpnext/e_commerce/shopping_cart/test_shopping_cart.py --- erpnext/e_commerce/shopping_cart/cart.py | 6 ++- .../shopping_cart/test_shopping_cart.py | 37 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index fff9f079744..7ab56889569 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -121,7 +121,11 @@ def place_order(): def request_for_quotation(): quotation = _get_cart_quotation() quotation.flags.ignore_permissions = True - quotation.submit() + + if get_shopping_cart_settings().save_quotations_as_draft: + quotation.save() + else: + quotation.submit() return quotation.name @frappe.whitelist() diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index ba3a36685df..bdbdaa0e45a 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -5,7 +5,12 @@ import unittest import frappe +<<<<<<< HEAD from frappe.utils import add_months, nowdate +======= +from frappe.tests.utils import change_settings +from frappe.utils import add_months, cint, nowdate +>>>>>>> a13e06156b (fix: 'save_quotations_as_draft' checkbox not honoured) from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.e_commerce.doctype.website_item.website_item import make_website_item @@ -13,6 +18,7 @@ from erpnext.e_commerce.shopping_cart.cart import ( _get_cart_quotation, get_cart_quotation, get_party, + request_for_quotation, update_cart, ) from erpnext.tests.utils import change_settings, create_test_contact_and_address @@ -23,11 +29,6 @@ class TestShoppingCart(unittest.TestCase): Note: Shopping Cart == Quotation """ - - @classmethod - def tearDownClass(cls): - frappe.db.sql("delete from `tabTax Rule`") - def setUp(self): frappe.set_user("Administrator") create_test_contact_and_address() @@ -43,6 +44,10 @@ class TestShoppingCart(unittest.TestCase): frappe.set_user("Administrator") self.disable_shopping_cart() + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabTax Rule`") + def test_get_cart_new_user(self): self.login_as_new_user() @@ -177,6 +182,28 @@ class TestShoppingCart(unittest.TestCase): # test if items are rendered without error frappe.render_template("templates/includes/cart/cart_items.html", cart) + @change_settings("E Commerce Settings",{ + "save_quotations_as_draft": 1 + }) + def test_cart_without_checkout_and_draft_quotation(self): + "Test impact of 'save_quotations_as_draft' checkbox." + frappe.local.shopping_cart_settings = None + + # add item to cart + update_cart("_Test Item", 1) + quote_name = request_for_quotation() # Request for Quote + quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) + + self.assertEqual(quote_doctstatus, 0) + + frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0) + frappe.local.shopping_cart_settings = None + update_cart("_Test Item", 1) + quote_name = request_for_quotation() # Request for Quote + quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) + + self.assertEqual(quote_doctstatus, 1) + def create_tax_rule(self): tax_rule = frappe.get_test_records("Tax Rule")[0] try: From aec1916a0f87255367db61797a5b2700b6a4f09a Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 10 Mar 2022 14:49:05 +0530 Subject: [PATCH 050/136] fix: Merge Conflicts in test_shopping_cart imports --- erpnext/e_commerce/shopping_cart/test_shopping_cart.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index bdbdaa0e45a..72d6962708d 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -5,12 +5,9 @@ import unittest import frappe -<<<<<<< HEAD -from frappe.utils import add_months, nowdate -======= from frappe.tests.utils import change_settings from frappe.utils import add_months, cint, nowdate ->>>>>>> a13e06156b (fix: 'save_quotations_as_draft' checkbox not honoured) + from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.e_commerce.doctype.website_item.website_item import make_website_item From b529b5c0cb4052cc0708d36c547ecebdf9e95f2d Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 10 Mar 2022 14:53:48 +0530 Subject: [PATCH 051/136] fix: Remove extra empty line after frappe imports --- erpnext/e_commerce/shopping_cart/test_shopping_cart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index 72d6962708d..b15bac68234 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -8,7 +8,6 @@ import frappe from frappe.tests.utils import change_settings from frappe.utils import add_months, cint, nowdate - from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.shopping_cart.cart import ( From a5b25db880d7ae065c341dba7adc49d198c04445 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Mar 2022 15:44:25 +0530 Subject: [PATCH 052/136] fix: dont reset UOM in MR on every get_item_detail call (#30164) (#30165) (cherry picked from commit 412645597567df133dd91a6cf44db93207575052) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/material_request/material_request.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 087a7883e09..cc64b5caa52 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -214,6 +214,7 @@ frappe.ui.form.on('Material Request', { material_request_type: frm.doc.material_request_type, plc_conversion_rate: 1, rate: item.rate, + uom: item.uom, conversion_factor: item.conversion_factor }, overwrite_warehouse: overwrite_warehouse @@ -392,6 +393,7 @@ frappe.ui.form.on("Material Request Item", { item_code: function(frm, doctype, name) { const item = locals[doctype][name]; item.rate = 0; + item.uom = ''; set_schedule_date(frm); frm.events.get_item_data(frm, item, true); }, From de5500817f528e1de76988010f2f501a50f65dde Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 16:09:26 +0530 Subject: [PATCH 053/136] test: refactor item merge test and disable commits (cherry picked from commit b4d4ae6aa3f258357a974e0a76247b3e752bdbf2) --- erpnext/stock/doctype/item/test_item.py | 19 ++++++++++--------- .../repost_item_valuation.py | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index c912101a4ac..68e545ba140 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -370,23 +370,24 @@ class TestItem(ERPNextTestCase): variant.save() def test_item_merging(self): - create_item("Test Item for Merging 1") - create_item("Test Item for Merging 2") + old = create_item(frappe.generate_hash(length=20)).name + new = create_item(frappe.generate_hash(length=20)).name - make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC", + make_stock_entry(item_code=old, target="_Test Warehouse - _TC", qty=1, rate=100) - make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", + make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", + qty=1, rate=100) + make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", qty=1, rate=100) - frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) + frappe.rename_doc("Item", old, new, merge=True) - self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1")) + self.assertFalse(frappe.db.exists("Item", old)) self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"})) - + {"item_code": new, "warehouse": "_Test Warehouse - _TC"})) self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})) def test_item_merging_with_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle 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 c6baa46c5eb..4b83e5e8532 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -118,7 +118,8 @@ def repost(doc): doc.set_status('Failed') raise finally: - frappe.db.commit() + if not frappe.flags.in_test: + frappe.db.commit() def repost_sl_entries(doc): if doc.based_on == 'Transaction': From 33ddbb4f3cd05058f8b3431db910c8619f95ed6a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 16:30:29 +0530 Subject: [PATCH 054/136] fix: handle duplicate bins during item merge renames (cherry picked from commit 73901aad6f88c06cfb6dab8da133fba4175bd692) --- erpnext/stock/doctype/item/item.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 5ab2c4acfdd..efcaa90198c 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -393,6 +393,7 @@ class Item(Document): self.validate_properties_before_merge(new_name) self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) + self.delete_old_bins(old_name) def after_rename(self, old_name, new_name, merge): if merge: @@ -421,6 +422,9 @@ class Item(Document): frappe.db.set_value(dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False) + def delete_old_bins(self, old_name): + frappe.db.delete("Bin", {"item_code": old_name}) + def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name): records = frappe.db.sql(""" SELECT parent, COUNT(*) as records FROM `tabStock Reconciliation Item` @@ -501,11 +505,11 @@ class Item(Document): 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_warehouses = frappe.db.sql_list("""select distinct warehouse - from tabBin where item_code=%s""", new_name) + repost_stock_for_warehouses = frappe.get_all("Stock Ledger Entry", + "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True) # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) + frappe.db.delete("Bin", {"item_code": new_name}) for warehouse in repost_stock_for_warehouses: repost_stock(new_name, warehouse) From f06554236fe2a8e84a270f5dc7e46f85fceb5b95 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Mar 2022 18:09:09 +0530 Subject: [PATCH 055/136] fix: fetch new fields from routing to bom (backport #30169) (#30171) * fix: fetch new fields in bom from routing (cherry picked from commit 18e2a33a9be4d4efea13ce5711343413b31358b8) * fix: dont hardcode hour rate precision (cherry picked from commit 362102e802bb501312a39bb21810336de26696b9) # Conflicts: # erpnext/manufacturing/doctype/bom_operation/bom_operation.json * fix: resolve conflict Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/bom/bom.py | 5 +++-- .../manufacturing/doctype/bom_operation/bom_operation.json | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b97dcab632f..cde25a2bc22 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -192,12 +192,13 @@ class BOM(WebsiteGenerator): if self.routing: self.set("operations", []) fields = ["sequence_id", "operation", "workstation", "description", - "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"] + "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate", + "set_cost_based_on_bom_qty"] for row in frappe.get_all("BOM Operation", fields = fields, filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): child = self.append('operations', row) - child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2) + child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate")) def set_bom_material_details(self): for item in self.get("items"): diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index ec617f3aaa9..9877b2882af 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -65,7 +65,8 @@ "label": "Hour Rate", "oldfieldname": "hour_rate", "oldfieldtype": "Currency", - "options": "currency" + "options": "currency", + "precision": "2" }, { "description": "In minutes", @@ -177,7 +178,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-13 16:45:01.092868", + "modified": "2022-03-10 06:19:08.462027", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From e428c360538c784e0e2d2f768c5d15b47cb88a6a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 17:07:57 +0530 Subject: [PATCH 056/136] fix: only update valuation rate if not None (cherry picked from commit 7dd10367f49b9f67def80aa0daa612848f00092c) --- erpnext/stock/stock_ledger.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index dd7fb66f983..4b9686cb2f6 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -843,11 +843,13 @@ class update_entries_after(object): for warehouse, data in self.data.items(): bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_name, { - "valuation_rate": data.valuation_rate, + updated_values = { "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value - }) + } + if data.valuation_rate is not None: + updated_values["valuation_rate"] = data.valuation_rate + frappe.db.set_value('Bin', bin_name, updated_values) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): From b13389b99a79709b8ebffb6732b503c130db9641 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 17:37:29 +0530 Subject: [PATCH 057/136] test: flaky MR report test all test records are on same day so sorting was random in report rows. (cherry picked from commit 6c54be0dcd83cc021844017f2ceeed58a88ca920) --- ...test_requested_items_to_order_and_receive.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py index f3c751c5c3c..a533da00e3a 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py @@ -17,8 +17,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase): def setUp(self) -> None: create_item("Test MR Report Item") self.setup_material_request() # to order and receive - self.setup_material_request(order=True) # to receive (ordered) - self.setup_material_request(order=True, receive=True) # complete (ordered & received) + self.setup_material_request(order=True, days=1) # to receive (ordered) + self.setup_material_request(order=True, receive=True, days=2) # complete (ordered & received) self.filters = frappe._dict( company="_Test Company", from_date=today(), to_date=add_days(today(), 30), @@ -32,9 +32,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase): data = get_data(self.filters) self.assertEqual(len(data), 2) # MRs today should be fetched - self.filters.from_date = add_days(today(), 1) - data = get_data(self.filters) - self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow + data = get_data(self.filters.update({"from_date": add_days(today(), 10)})) + self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future def test_ordered_received_material_requests(self): data = get_data(self.filters) @@ -44,19 +43,19 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase): self.assertEqual(data[0].ordered_qty, 0.0) self.assertEqual(data[1].ordered_qty, 57.0) - def setup_material_request(self, order=False, receive=False): + def setup_material_request(self, order=False, receive=False, days=0): po = None test_records = frappe.get_test_records('Material Request') mr = frappe.copy_doc(test_records[0]) - mr.transaction_date = today() - mr.schedule_date = add_days(today(), 1) + mr.transaction_date = add_days(today(), days) + mr.schedule_date = add_days(mr.transaction_date, 1) for row in mr.items: row.item_code = "Test MR Report Item" row.item_name = "Test MR Report Item" row.description = "Test MR Report Item" row.uom = "Nos" - row.schedule_date = add_days(today(), 1) + row.schedule_date = mr.schedule_date mr.submit() if order or receive: From a04c7fc27de7c90c354c5252f047ab8132ffc7d0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 17:39:55 +0530 Subject: [PATCH 058/136] test: submit PR directly (cherry picked from commit 472fe8e3191764d281b54c0c49c83a88b26deed9) --- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b13d6d3d05a..a737d873a96 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -788,8 +788,7 @@ class TestPurchaseReceipt(ERPNextTestCase): update_purchase_receipt_status, ) - pr = make_purchase_receipt(do_not_submit=True) - pr.submit() + pr = make_purchase_receipt() update_purchase_receipt_status(pr.name, "Closed") self.assertEqual( From 44fd94f6b20517620a73b5648086add509477121 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Mar 2022 20:56:36 +0530 Subject: [PATCH 059/136] fix: Shipping rule application fixes (cherry picked from commit d596e0e4dff86cf8cde640bd18a54ee159276a4c) --- .../doctype/shipping_rule/shipping_rule.py | 3 +-- erpnext/controllers/taxes_and_totals.py | 5 ++++- .../public/js/controllers/taxes_and_totals.js | 4 +++- .../doctype/sales_order/test_sales_order.py | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 792e7d21a78..7e5129911e4 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -71,8 +71,7 @@ class ShippingRule(Document): if doc.currency != doc.company_currency: shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) - if shipping_amount: - self.add_shipping_rule_to_tax_table(doc, shipping_amount) + self.add_shipping_rule_to_tax_table(doc, shipping_amount) def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e218c0cd031..a642b8a083e 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object): self.set_discount_amount() self.apply_discount_amount() + self.calculate_shipping_charges() + if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: self.calculate_total_advance() @@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object): self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() - self.calculate_shipping_charges() self.calculate_taxes() self.manipulate_grand_total_for_inclusive_tax() self.calculate_totals() @@ -276,6 +277,8 @@ class calculate_taxes_and_totals(object): shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) + self._calculate() + def calculate_taxes(self): rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') if not rounding_adjustment_computed: diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2b80efd6e33..8068879810f 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -39,6 +39,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this._calculate_taxes_and_totals(); this.calculate_discount_amount(); + this.calculate_shipping_charges(); + // Advance calculation applicable to Sales /Purchase Invoice if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { @@ -81,7 +83,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); - this.calculate_shipping_charges(); this.calculate_taxes(); this.manipulate_grand_total_for_inclusive_tax(); this.calculate_totals(); @@ -273,6 +274,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { this.shipping_rule(); + this._calculate_taxes_and_totals(); } }, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 1102fe96fc4..dd3a007c411 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1405,6 +1405,28 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) self.assertEqual(mr.status, "Manufactured") + def test_sales_order_with_shipping_rule(self): + from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule + shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test") + sales_order = make_sales_order(do_not_save=True) + sales_order.shipping_rule = shipping_rule.name + + sales_order.items[0].qty = 1 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 50) + + sales_order.items[0].qty = 2 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 100) + + sales_order.items[0].qty = 3 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 200) + + sales_order.items[0].qty = 21 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable From 5eeb09c84e0f573eb07951411c25e90b41833910 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Mar 2022 10:50:03 +0530 Subject: [PATCH 060/136] fix: Sales and Purchase retrun optimization (cherry picked from commit 395b15058caedbcd6bc461d9d397a563bd2e981b) --- erpnext/controllers/sales_and_purchase_return.py | 13 ++++++++----- .../stock/doctype/delivery_note/delivery_note.py | 3 +++ .../doctype/purchase_receipt/purchase_receipt.py | 3 +++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index df3c5f10c1b..721aabbdb8e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -208,7 +208,7 @@ def get_already_returned_items(doc): return items -def get_returned_qty_map_for_row(row_name, doctype): +def get_returned_qty_map_for_row(return_against, supplier, row_name, doctype): child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) @@ -226,9 +226,12 @@ def get_returned_qty_map_for_row(row_name, doctype): if doctype == "Purchase Receipt": fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] + # Used retrun against and supplier and is_retrun because there is an index added for it data = frappe.db.get_list(doctype, fields = fields, filters = [ + [doctype, "return_against", "=", return_against], + [doctype, "supplier", "=", supplier], [doctype, "docstatus", "=", 1], [doctype, "is_return", "=", 1], [child_doctype, reference_field, "=", row_name] @@ -307,7 +310,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.serial_no = '\n'.join(serial_nos) if doctype == "Purchase Receipt": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype) target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) @@ -321,7 +324,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype) target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) @@ -335,7 +338,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_invoice_item = source_doc.name elif doctype == "Delivery Note": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) @@ -348,7 +351,7 @@ def make_return_doc(doctype, source_name, target_doc=None): if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": - returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 00836fc8157..c8b8a0c0165 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -779,3 +779,6 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): }, target_doc, set_missing_values) return doclist + +def on_doctype_update(): + frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index afaa8b02a89..db2656e8c59 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -888,3 +888,6 @@ def get_item_account_wise_additional_cost(purchase_document): account.base_amount * item.get(based_on_field) / total_item_cost return item_account_wise_cost + +def on_doctype_update(): + frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) From f31a7bde82e5d80faa7420c8ed7bfb2b4075607d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Mar 2022 12:29:17 +0530 Subject: [PATCH 061/136] fix: Update party type (cherry picked from commit e9d458b822e7436632d0d53fdc4a068267876a0a) --- erpnext/controllers/sales_and_purchase_return.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 721aabbdb8e..7d4ef587526 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -208,10 +208,15 @@ def get_already_returned_items(doc): return items -def get_returned_qty_map_for_row(return_against, supplier, row_name, doctype): +def get_returned_qty_map_for_row(return_against, party, row_name, doctype): child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) + if doctype in ('Purchase Receipt', 'Purchase Invoice'): + party_type = 'supplier' + else: + party_type = 'customer' + fields = [ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype) @@ -231,7 +236,7 @@ def get_returned_qty_map_for_row(return_against, supplier, row_name, doctype): fields = fields, filters = [ [doctype, "return_against", "=", return_against], - [doctype, "supplier", "=", supplier], + [doctype, party_type, "=", party], [doctype, "docstatus", "=", 1], [doctype, "is_return", "=", 1], [child_doctype, reference_field, "=", row_name] @@ -338,7 +343,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_invoice_item = source_doc.name elif doctype == "Delivery Note": - returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype) + returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) @@ -351,7 +356,7 @@ def make_return_doc(doctype, source_name, target_doc=None): if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": - returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype) + returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) From abc2dd9e3e2862fbde6ffa0556dab9ac3355e23a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Mar 2022 14:18:54 +0530 Subject: [PATCH 062/136] fix: Update modified timestamp (cherry picked from commit a5befb6bf82546deaf9ea21da2eb828aca1bb51e) --- erpnext/stock/doctype/delivery_note/delivery_note.json | 2 +- erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 55a4c956a67..7ebc4eed751 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-09 14:29:13.428984", + "modified": "2022-03-10 14:29:13.428984", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index b54a90eed35..6d4b4a19bd2 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1165,7 +1165,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-02-01 11:40:52.690984", + "modified": "2022-03-10 11:40:52.690984", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 625034a0166d990676665dcd491460abae24189a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Mar 2022 19:01:26 +0530 Subject: [PATCH 063/136] test: standalone SI creates and attaches serial nos (cherry picked from commit 1c37d2711abebe7db43c89c49869ce63e533e2a7) --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 69ab1738bc6..ad0fc63e47b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2540,6 +2540,14 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + def test_standalone_serial_no_return(self): + si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1) + si.submit() + self.assertTrue(si.items[0].serial_no) + + return si + + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' From 2a00380e5b5663dda9c502964a9607c4f00f7d07 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Mar 2022 19:06:07 +0530 Subject: [PATCH 064/136] fix: attach sr no si standalone credit note (cherry picked from commit 1a256c62c422c518ba074cec6b275482d8de7d38) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 9 ++++++++- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 4 +--- erpnext/stock/doctype/delivery_note/delivery_note.py | 8 +++++++- .../stock/doctype/delivery_note/test_delivery_note.py | 5 +++++ erpnext/stock/doctype/serial_no/serial_no.py | 7 +++++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 409677f3c26..c0430e1b9de 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -44,7 +44,11 @@ from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timeshe from erpnext.setup.doctype.company.company import update_company_current_month_sales from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so -from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivery_note_serial_no, + get_serial_nos, + update_serial_nos_after_submit, +) from erpnext.stock.utils import calculate_mapped_packed_items_return form_grid_templates = { @@ -228,6 +232,9 @@ class SalesInvoice(SellingController): # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: self.update_stock_ledger() + if self.is_return and self.update_stock: + update_serial_nos_after_submit(self, "items") + # this sequence because outstanding may get -ve self.make_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ad0fc63e47b..5e4a6d619cb 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2542,11 +2542,9 @@ class TestSalesInvoice(unittest.TestCase): def test_standalone_serial_no_return(self): si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1) - si.submit() + si.reload() self.assertTrue(si.items[0].serial_no) - return si - def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c8b8a0c0165..e7ab101d5a8 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -13,7 +13,10 @@ from frappe.utils import cint, flt from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos -from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivery_note_serial_no, + update_serial_nos_after_submit, +) from erpnext.stock.utils import calculate_mapped_packed_items_return form_grid_templates = { @@ -220,6 +223,9 @@ class DeliveryNote(SellingController): # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() + if self.is_return: + update_serial_nos_after_submit(self, "items") + self.make_gl_entries() self.repost_future_sle_and_gle() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index bd18e788ba6..4d26397f482 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -822,6 +822,11 @@ class TestDeliveryNote(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) + def test_standalone_serial_no_return(self): + dn = create_delivery_note(item_code="_Test Serialized Item With Series", is_return=True, qty=-1) + dn.reload() + self.assertTrue(dn.items[0].serial_no) + def create_return_delivery_note(**args): args = frappe._dict(args) from erpnext.controllers.sales_and_purchase_return import make_return_doc diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index e300d46db83..350d5fec50f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -413,7 +413,7 @@ def update_serial_nos(sle, item_det): if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) - frappe.db.set(sle, "serial_no", serial_nos) + sle.db_set("serial_no", serial_nos) validate_serial_no(sle, item_det) if sle.serial_no: auto_make_serial_nos(sle) @@ -535,13 +535,16 @@ def update_serial_nos_after_submit(controller, parentfield): if controller.doctype == "Stock Entry": warehouse = d.t_warehouse qty = d.transfer_qty + elif controller.doctype in ("Sales Invoice", "Delivery Note"): + warehouse = d.warehouse + qty = d.stock_qty else: warehouse = d.warehouse qty = (d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty) for sle in stock_ledger_entries: if sle.voucher_detail_no==d.name: - if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ + if not accepted_serial_nos_updated and qty and abs(sle.actual_qty) == abs(qty) \ and sle.warehouse == warehouse and sle.serial_no != d.serial_no: d.serial_no = sle.serial_no frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) From 39d6cebf1a0444de9b7cd5223221e8637fb2fa85 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Mar 2022 17:37:38 +0530 Subject: [PATCH 065/136] fix: Nil and Exempted values in GSTR-3B Report (cherry picked from commit abe580e8b0fc6f5fae720c4ab4713eb2e239abf6) --- .../doctype/gstr_3b_report/gstr_3b_report.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 6b31bcc05fc..05b0c3c8f09 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -128,7 +128,8 @@ class GSTR3BReport(Document): def get_inward_nil_exempt(self, state): inward_nil_exempt = frappe.db.sql(""" - SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst + SELECT p.place_of_supply, p.supplier_address, + i.base_amount, i.is_nil_exempt, i.is_non_gst FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i WHERE p.docstatus = 1 and p.name = i.parent and p.is_opening = 'No' @@ -136,7 +137,7 @@ class GSTR3BReport(Document): and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s - GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", + """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) inward_nil_exempt_details = { @@ -150,18 +151,24 @@ class GSTR3BReport(Document): } } + address_state_map = get_address_state_map() + for d in inward_nil_exempt: - if d.place_of_supply: - if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ - and state == d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["gst"]["intra"] += d.base_amount - elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ - and state != d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["gst"]["inter"] += d.base_amount - elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount - elif d.is_non_gst == 1 and state != d.place_of_supply.split("-")[1]: - inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount + if not d.place_of_supply: + d.place_of_supply = "00-" + cstr(state) + + supplier_state = address_state_map.get(d.supplier_address) or state + + if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ + and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["gst"]["intra"] += d.base_amount + elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ + and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["gst"]["inter"] += d.base_amount + elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount + elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]): + inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount return inward_nil_exempt_details @@ -420,6 +427,11 @@ class GSTR3BReport(Document): return ",".join(missing_field_invoices) +def get_address_state_map(): + return frappe._dict( + frappe.get_all('Address', fields=['name', 'gst_state'], as_list=1) + ) + def get_json(template): file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template)) with open(file_path, 'r') as f: From 8a567b8d8c2745284415cc1c3d6ef99db05077a2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Feb 2022 15:58:12 +0530 Subject: [PATCH 066/136] fix: Multi-currency bank reconciliation fixes (cherry picked from commit cbb5ffb6feff3e6b47a3d815ab986e90b66b4db3) --- .../bank_reconciliation_tool/bank_reconciliation_tool.py | 2 +- .../doctype/bank_transaction/bank_transaction.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 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 f3351ddcba4..317fcc02b50 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -232,7 +232,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers): }), transaction.currency, company_account) if total_amount > transaction.unallocated_amount: - frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction")) + frappe.throw(_("The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction")) account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") for voucher in vouchers: diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index b8e8d970220..e3fc1dc73f8 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -109,8 +109,13 @@ def get_paid_amount(payment_entry, currency, bank_account): paid_amount_field = "paid_amount" if payment_entry.payment_document == 'Payment Entry': doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry) - paid_amount_field = ("base_paid_amount" - if doc.paid_to_account_currency == currency else "paid_amount") + + if doc.payment_type == 'Receive': + paid_amount_field = ("received_amount" + if doc.paid_to_account_currency == currency else "base_received_amount") + elif doc.payment_type == 'Pay': + paid_amount_field = ("paid_amount" + if doc.paid_to_account_currency == currency else "base_paid_amount") return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field) From f7b06a622fdff62b3b80e8ab8af4c81d82dac22e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 31 Jan 2022 21:50:42 +0530 Subject: [PATCH 067/136] refactor: Employee Leave Balance Report - incorrect opening balance on boundary allocation dates - carry forwarded leaves accounted in leaves allocated column, should be part of opening balance - leaves allocated column also adds expired leave allocations causing negative balances, should only consider non-expiry records (cherry picked from commit 538b64b1fa460327236ef8b260aec6c52adf21ff) --- .../employee_leave_balance.py | 139 +++++++++++++----- 1 file changed, 103 insertions(+), 36 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index b375b18b079..6280ef323b7 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -6,8 +6,9 @@ from itertools import groupby import frappe from frappe import _ -from frappe.utils import add_days +from frappe.utils import add_days, date_diff, getdate +from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation from erpnext.hr.doctype.leave_application.leave_application import ( get_leave_balance_on, get_leaves_for_period, @@ -46,27 +47,27 @@ def get_columns(): 'label': _('Opening Balance'), 'fieldtype': 'float', 'fieldname': 'opening_balance', - 'width': 130, + 'width': 150, }, { - 'label': _('Leave Allocated'), + 'label': _('New Leave(s) Allocated'), 'fieldtype': 'float', 'fieldname': 'leaves_allocated', - 'width': 130, + 'width': 200, }, { - 'label': _('Leave Taken'), + 'label': _('Leave(s) Taken'), 'fieldtype': 'float', 'fieldname': 'leaves_taken', - 'width': 130, + 'width': 150, }, { - 'label': _('Leave Expired'), + 'label': _('Leave(s) Expired'), 'fieldtype': 'float', 'fieldname': 'leaves_expired', - 'width': 130, + 'width': 150, }, { 'label': _('Closing Balance'), 'fieldtype': 'float', 'fieldname': 'closing_balance', - 'width': 130, + 'width': 150, }] return columns @@ -108,10 +109,9 @@ def get_data(filters): leaves_taken = get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 - new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type) - - - opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition + new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves( + filters.from_date, filters.to_date, employee.name, leave_type) + opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves) row.leaves_allocated = new_allocation row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 @@ -125,6 +125,55 @@ def get_data(filters): return data + +def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves): + # allocation boundary condition + # opening balance is the closing leave balance 1 day before the filter start date + opening_balance_date = add_days(filters.from_date, -1) + allocation = get_previous_allocation(filters.from_date, leave_type, employee) + + if allocation and allocation.get("to_date") and opening_balance_date and \ + getdate(allocation.get("to_date")) == getdate(opening_balance_date): + # if opening balance date is same as the previous allocation's expiry + # then opening balance should only consider carry forwarded leaves + opening_balance = carry_forwarded_leaves + else: + # else directly get closing leave balance on the previous day + opening_balance = get_closing_balance_on(opening_balance_date, employee, leave_type, filters) + + return opening_balance + + +def get_closing_balance_on(date, employee, leave_type, filters): + closing_balance = get_leave_balance_on(employee, leave_type, date) + leave_allocation = get_leave_allocation_for_date(employee, leave_type, date) + if leave_allocation: + # if balance is greater than the days remaining for leave allocation's end date + # then balance should be = remaining days + remaining_days = date_diff(leave_allocation[0].to_date, filters.from_date) + 1 + if remaining_days < closing_balance: + closing_balance = remaining_days + + return closing_balance + + +def get_leave_allocation_for_date(employee, leave_type, date): + allocation = frappe.qb.DocType('Leave Allocation') + records = ( + frappe.qb.from_(allocation) + .select( + allocation.name, allocation.to_date + ).where( + (allocation.docstatus == 1) + & (allocation.employee == employee) + & (allocation.leave_type == leave_type) + & ((allocation.from_date <= date) & (allocation.to_date >= date)) + ) + ).run(as_dict=True) + + return records + + def get_conditions(filters): conditions={ 'status': 'Active', @@ -140,6 +189,7 @@ def get_conditions(filters): return conditions + def get_department_leave_approver_map(department=None): # get current department and all its child @@ -171,39 +221,55 @@ def get_department_leave_approver_map(department=None): return approvers + def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): - - from frappe.utils import getdate - new_allocation = 0 expired_leaves = 0 + carry_forwarded_leaves = 0 - records= frappe.db.sql(""" - SELECT - employee, leave_type, from_date, to_date, leaves, transaction_name, - transaction_type, is_carry_forward, is_expired - FROM `tabLeave Ledger Entry` - WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 - AND transaction_type = 'Leave Allocation' - AND (from_date between %(from_date)s AND %(to_date)s - OR to_date between %(from_date)s AND %(to_date)s - OR (from_date < %(from_date)s AND to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + records = get_leave_ledger_entries(from_date, to_date, employee, leave_type) for record in records: if record.to_date < getdate(to_date): expired_leaves += record.leaves - if record.from_date >= getdate(from_date): - new_allocation += record.leaves + # new allocation records with `is_expired=1` are created when leave expires + # these new records should not be considered, else it leads to negative leave balance + if record.is_expired: + continue + + if record.from_date >= getdate(from_date): + if record.is_carry_forward: + carry_forwarded_leaves += record.leaves + else: + new_allocation += record.leaves + + return new_allocation, expired_leaves, carry_forwarded_leaves + + +def get_leave_ledger_entries(from_date, to_date, employee, leave_type): + ledger = frappe.qb.DocType('Leave Ledger Entry') + records = ( + frappe.qb.from_(ledger) + .select( + ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date, + ledger.leaves, ledger.transaction_name, ledger.transaction_type, + ledger.is_carry_forward, ledger.is_expired + ).where( + (ledger.docstatus == 1) + & (ledger.transaction_type == 'Leave Allocation') + & (ledger.employee == employee) + & (ledger.leave_type == leave_type) + & ( + (ledger.from_date[from_date: to_date]) + | (ledger.to_date[from_date: to_date]) + | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) + ) + ) + ).run(as_dict=True) + + return records - return new_allocation, expired_leaves def get_chart_data(data): labels = [] @@ -224,6 +290,7 @@ def get_chart_data(data): return chart + def get_dataset_for_chart(employee_data, datasets, labels): leaves = [] employee_data = sorted(employee_data, key=lambda k: k['employee_name']) From d7a75a695d76d0aa30034bc41d09c561d98191e9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 3 Feb 2022 23:34:46 +0530 Subject: [PATCH 068/136] fix: expired leaves not calculated properly because of newly created expiry ledger entries (cherry picked from commit 1ea749cf1a8f4f25cb5465ac607f242779e0aaac) --- .../report/employee_leave_balance/employee_leave_balance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 6280ef323b7..5172fb8fc28 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -230,14 +230,14 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): records = get_leave_ledger_entries(from_date, to_date, employee, leave_type) for record in records: - if record.to_date < getdate(to_date): - expired_leaves += record.leaves - # new allocation records with `is_expired=1` are created when leave expires # these new records should not be considered, else it leads to negative leave balance if record.is_expired: continue + if record.to_date < getdate(to_date): + expired_leaves += record.leaves + if record.from_date >= getdate(from_date): if record.is_carry_forward: carry_forwarded_leaves += record.leaves From 60eb38c911fdf3f380d1737608952297589b5f63 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 00:01:05 +0530 Subject: [PATCH 069/136] refactor: Leaves Taken calculation - fix issue where Leaves Taken also adds leaves expiring on boundary date as leaves taken, causing ambiguity - remove unnecessary `skip_expiry_leaves` function - `get_allocation_expiry` considering cancelled entries too (cherry picked from commit 26b40e7cfd6649a08e430ce5ce9000f61cd09d89) --- .../doctype/leave_application/leave_application.py | 14 +++++--------- .../leave_ledger_entry/leave_ledger_entry.py | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index ef5f4bcb0ff..68b345631cf 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -481,7 +481,8 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date): 'leave_type': leave_type, 'is_carry_forward': 1, 'transaction_type': 'Leave Allocation', - 'to_date': ['between', (from_date, to_date)] + 'to_date': ['between', (from_date, to_date)], + 'docstatus': 1 },fields=['to_date']) return expiry[0]['to_date'] if expiry else None @@ -648,18 +649,18 @@ def get_remaining_leaves(allocation, leaves_taken, date, expiry): return _get_remaining_leaves(total_leaves, allocation.to_date) -def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False): +def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 for leave_entry in leave_entries: inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date) - if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': + if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': leave_days += leave_entry.leaves elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ - and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)): + and not skip_expired_leaves: leave_days += leave_entry.leaves elif leave_entry.transaction_type == 'Leave Application': @@ -681,11 +682,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_ return leave_days -def skip_expiry_leaves(leave_entry, date): - ''' Checks whether the expired leaves coincide with the to_date of leave balance check. - This allows backdated leave entry creation for non carry forwarded allocation ''' - end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date']) - return True if end_date == date and not leave_entry.is_carry_forward else False def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date. ''' diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 5c5299ea7eb..a5923e0021c 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -171,7 +171,7 @@ def expire_carried_forward_allocation(allocation): ''' Expires remaining leaves in the on carried forward allocation ''' from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, - allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True) + allocation.from_date, allocation.to_date, skip_expired_leaves=False) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created From 4632d13dca23bd81357b9345282605ea7a503b37 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 12:29:00 +0530 Subject: [PATCH 070/136] refactor: balance in Balance Summary report near allocation expiry date - Leave Balance shows minimum leaves remaining after comparing with remaining days for allocation expiry causing ambiguity - refactor remaining leaves calculation to return both, actual leave balance and balance for consumption - show actual balance in leave application, use balance for consumption only in validations (cherry picked from commit 55ac8519bfecdb42f45ad255835070097544dfcb) --- .../leave_application/leave_application.py | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 68b345631cf..55968eb107a 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -30,11 +30,13 @@ from erpnext.hr.utils import ( validate_active_employee, ) +from typing import Dict class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass +class InsufficientLeaveBalanceError(frappe.ValidationError): pass from frappe.model.document import Document @@ -261,15 +263,18 @@ class LeaveApplication(Document): frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) if not is_lwp(self.leave_type): - self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, - consider_all_leaves_in_the_allocation_period=True) - if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance): + leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, + consider_all_leaves_in_the_allocation_period=True, for_consumption=True) + self.leave_balance = leave_balance.get("leave_balance") + leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption") + + if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) + frappe.msgprint(_("Insufficient leave balance for Leave Type {0}") + .format(frappe.bold(self.leave_type)), title=_("Warning"), indicator="orange") else: - frappe.throw(_("There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) + frappe.throw(_("Insufficient leave balance for Leave Type {0}") + .format(self.leave_type), InsufficientLeaveBalanceError, title=_("Insufficient Balance")) def validate_leave_overlap(self): if not self.name: @@ -426,7 +431,7 @@ class LeaveApplication(Document): if self.status != 'Approved' and submit: return - expiry_date = get_allocation_expiry(self.employee, self.leave_type, + expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type, self.to_date, self.from_date) lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") @@ -473,7 +478,7 @@ class LeaveApplication(Document): create_leave_ledger_entry(self, args, submit) -def get_allocation_expiry(employee, leave_type, to_date, from_date): +def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date): ''' Returns expiry of carry forward allocation in leave ledger entry ''' expiry = frappe.get_all("Leave Ledger Entry", filters={ @@ -545,7 +550,8 @@ def get_leave_details(employee, date): return ret @frappe.whitelist() -def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False): +def get_leave_balance_on(employee, leave_type, date, to_date=None, + consider_all_leaves_in_the_allocation_period=False, for_consumption=False): ''' Returns leave balance till date :param employee: employee name @@ -553,6 +559,11 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ :param date: date to check balance on :param to_date: future date to check for allocation expiry :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date + :param for_consumption: flag to check if leave balance is required for consumption or display + eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave + in this case leave_balance = 10 but leave_balance_for_consumption = 1 + if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1} + else, returns leave_balance (in this case 10) ''' if not to_date: @@ -562,11 +573,17 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ allocation = allocation_records.get(leave_type, frappe._dict()) end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date - expiry = get_allocation_expiry(employee, leave_type, to_date, date) + cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) - return get_remaining_leaves(allocation, leaves_taken, date, expiry) + remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) + + if for_consumption: + return remaining_leaves + else: + return remaining_leaves.get('leave_balance') + def get_leave_allocation_records(employee, date, leave_type=None): """Returns the total allocated leaves and carry forwarded leaves based on ledger entries""" @@ -629,25 +646,34 @@ def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): }, fields=['SUM(total_leave_days) as leaves'])[0] return leaves['leaves'] if leaves['leaves'] else 0.0 -def get_remaining_leaves(allocation, leaves_taken, date, expiry): - ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' +def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]: + '''Returns a dict of leave_balance and leave_balance_for_consumption + leave_balance returns the available leave balance + leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry + ''' def _get_remaining_leaves(remaining_leaves, end_date): - + ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' if remaining_leaves > 0: remaining_days = date_diff(end_date, date) + 1 remaining_leaves = min(remaining_days, remaining_leaves) return remaining_leaves - total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken) + leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken) - if expiry and allocation.unused_leaves: - remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) - remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry) + # balance for carry forwarded leaves + if cf_expiry and allocation.unused_leaves: + cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) + remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry) - total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves) + leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves) + leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) - return _get_remaining_leaves(total_leaves, allocation.to_date) + remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date) + return { + 'leave_balance': leave_balance, + 'leave_balance_for_consumption': remaining_leaves + } def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) From 420cb8269c88dfb0cdb38365a16e6965e4c99a63 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 12:39:58 +0530 Subject: [PATCH 071/136] fix: sort imports, sider issues (cherry picked from commit b5c686ac4035491f5e2bf3a4709f54f94c04dd06) --- erpnext/hr/doctype/leave_application/leave_application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 55968eb107a..db7560d78aa 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from typing import Dict import frappe from frappe import _ @@ -30,13 +31,13 @@ from erpnext.hr.utils import ( validate_active_employee, ) -from typing import Dict class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass -class InsufficientLeaveBalanceError(frappe.ValidationError): pass +class InsufficientLeaveBalanceError(frappe.ValidationError): + pass from frappe.model.document import Document From e96b661ca190b04ae13f817bc07404d57563aa18 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 4 Feb 2022 13:04:25 +0530 Subject: [PATCH 072/136] fix: show actual balance instead of consumption balance in opening balance - not changing opening balance based on remaining days (cherry picked from commit dbfa463738a7f91f9d5c21a700f604f3325cd923) --- .../employee_leave_balance.py | 66 +++++-------------- 1 file changed, 16 insertions(+), 50 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 5172fb8fc28..3f0337e508c 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -138,42 +138,12 @@ def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves): # then opening balance should only consider carry forwarded leaves opening_balance = carry_forwarded_leaves else: - # else directly get closing leave balance on the previous day - opening_balance = get_closing_balance_on(opening_balance_date, employee, leave_type, filters) + # else directly get leave balance on the previous day + opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date) return opening_balance -def get_closing_balance_on(date, employee, leave_type, filters): - closing_balance = get_leave_balance_on(employee, leave_type, date) - leave_allocation = get_leave_allocation_for_date(employee, leave_type, date) - if leave_allocation: - # if balance is greater than the days remaining for leave allocation's end date - # then balance should be = remaining days - remaining_days = date_diff(leave_allocation[0].to_date, filters.from_date) + 1 - if remaining_days < closing_balance: - closing_balance = remaining_days - - return closing_balance - - -def get_leave_allocation_for_date(employee, leave_type, date): - allocation = frappe.qb.DocType('Leave Allocation') - records = ( - frappe.qb.from_(allocation) - .select( - allocation.name, allocation.to_date - ).where( - (allocation.docstatus == 1) - & (allocation.employee == employee) - & (allocation.leave_type == leave_type) - & ((allocation.from_date <= date) & (allocation.to_date >= date)) - ) - ).run(as_dict=True) - - return records - - def get_conditions(filters): conditions={ 'status': 'Active', @@ -191,28 +161,24 @@ def get_conditions(filters): def get_department_leave_approver_map(department=None): - # get current department and all its child department_list = frappe.get_list('Department', - filters={ - 'disabled': 0 - }, - or_filters={ - 'name': department, - 'parent_department': department - }, - fields=['name'], - pluck='name' - ) + filters={'disabled': 0}, + or_filters={ + 'name': department, + 'parent_department': department + }, + pluck='name' + ) # retrieve approvers list from current department and from its subsequent child departments approver_list = frappe.get_all('Department Approver', - filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, - fields=['parent', 'approver'], - as_list=1 - ) + filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, + fields=['parent', 'approver'], + as_list=True + ) approvers = {} From 6af8e9790bb3b44bf049e4b5ce137db384741699 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 09:58:12 +0530 Subject: [PATCH 073/136] test: employee leave balance report - fix expired leaves calculation when filters span across 2 different allocation periods (cherry picked from commit c050ce49c2b3ad7b36640edf01099bb9cb002e9d) # Conflicts: # erpnext/payroll/doctype/salary_slip/test_salary_slip.py --- .../doctype/holiday_list/test_holiday_list.py | 22 +++ .../test_leave_application.py | 9 +- .../employee_leave_balance.py | 10 +- .../test_employee_leave_balance.py | 162 ++++++++++++++++++ .../doctype/salary_slip/test_salary_slip.py | 14 +- 5 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py index c9239edb720..aed901aa6da 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import unittest +from contextlib import contextmanager from datetime import timedelta import frappe @@ -30,3 +31,24 @@ def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getd "holidays" : holiday_dates }).insert() return doc + + +@contextmanager +def set_holiday_list(holiday_list, company_name): + """ + Context manager for setting holiday list in tests + """ + try: + company = frappe.get_doc('Company', company_name) + previous_holiday_list = company.default_holiday_list + + company.default_holiday_list = holiday_list + company.save() + + yield + + finally: + # restore holiday list setup + company = frappe.get_doc('Company', company_name) + company.default_holiday_list = previous_holiday_list + company.save() diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index c9f377dfaca..2e93c41a7f7 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -513,7 +513,7 @@ class TestLeaveApplication(unittest.TestCase): leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90) - leave_type.submit() + leave_type.insert() create_carry_forwarded_allocation(employee, leave_type) @@ -734,19 +734,22 @@ def create_carry_forwarded_allocation(employee, leave_type): carry_forward=1) leave_allocation.submit() -def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None): +def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None): allocation = frappe.get_doc({ "doctype": "Leave Allocation", "employee": employee or "_T-Employee-00001", "leave_type": leave_type or "_Test Leave Type", "from_date": from_date or "2013-01-01", "to_date": to_date or "2019-12-31", - "new_leaves_allocated": 30 + "new_leaves_allocated": leaves or 30, + "carry_forward": carry_forward }) allocation.insert(ignore_permissions=True) allocation.submit() + return allocation + def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 3f0337e508c..3a5f2fe9629 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -16,6 +16,8 @@ from erpnext.hr.doctype.leave_application.leave_application import ( def execute(filters=None): + filters = frappe._dict(filters or {}) + if filters.to_date <= filters.from_date: frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) @@ -103,7 +105,7 @@ def get_data(filters): or ("HR Manager" in frappe.get_roles(user)): if len(active_employees) > 1: row = frappe._dict() - row.employee = employee.name, + row.employee = employee.name row.employee_name = employee.employee_name leaves_taken = get_leaves_for_period(employee.name, leave_type, @@ -114,7 +116,7 @@ def get_data(filters): opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves) row.leaves_allocated = new_allocation - row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 + row.leaves_expired = expired_leaves row.opening_balance = opening row.leaves_taken = leaves_taken @@ -202,7 +204,11 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): continue if record.to_date < getdate(to_date): + # leave allocations ending before to_date, reduce leaves taken within that period + # since they are already used, they won't expire expired_leaves += record.leaves + expired_leaves += get_leaves_for_period(employee, leave_type, + record.from_date, record.to_date) if record.from_date >= getdate(from_date): if record.is_carry_forward: diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py new file mode 100644 index 00000000000..05316f165f3 --- /dev/null +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -0,0 +1,162 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import unittest + +import frappe +from frappe.utils import ( + add_days, + add_months, + get_year_ending, + get_year_start, + getdate, + flt, +) + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type +from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday, make_allocation_record +from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list, make_leave_application +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation + +test_records = frappe.get_test_records('Leave Type') + +class TestEmployeeLeaveBalance(unittest.TestCase): + def setUp(self): + for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']: + frappe.db.delete(dt) + + frappe.set_user('Administrator') + + self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', get_year_start(getdate()), get_year_ending(getdate())) + + self.date = getdate() + self.year_start = getdate(get_year_start(self.date)) + self.mid_year = add_months(self.year_start, 6) + self.year_end = getdate(get_year_ending(self.date)) + + + def tearDown(self): + frappe.db.rollback() + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_employee_leave_balance(self): + frappe.get_doc(test_records[0]).insert() + + # 5 leaves + allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), leaves=5) + # 30 leaves + allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + # expires 5 leaves + process_expired_allocation() + + # 4 days leave + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type') + leave_application.reload() + + filters = { + 'from_date': allocation1.from_date, + 'to_date': allocation2.to_date, + 'employee': self.employee_id + } + + report = execute(filters) + + expected_data = [{ + 'leave_type': '_Test Leave Type', + 'employee': self.employee_id, + 'employee_name': 'test_emp_leave_balance@example.com', + 'leaves_allocated': flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated), + 'leaves_expired': flt(allocation1.new_leaves_allocated), + 'opening_balance': flt(0), + 'leaves_taken': flt(leave_application.total_leave_days), + 'closing_balance': flt(allocation2.new_leaves_allocated - leave_application.total_leave_days), + 'indent': 1 + }] + + self.assertEqual(report[1], expected_data) + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_opening_balance_on_alloc_boundary_dates(self): + frappe.get_doc(test_records[0]).insert() + + # 30 leaves allocated + allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type') + leave_application.reload() + + # Case 1: opening balance for first alloc boundary + filters = { + 'from_date': self.year_start, + 'to_date': self.year_end, + 'employee': self.employee_id + } + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, 0) + + # Case 2: opening balance after leave application date + filters = { + 'from_date': add_days(leave_application.to_date, 1), + 'to_date': self.year_end, + 'employee': self.employee_id + } + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) + + # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date + # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 + filters = { + 'from_date': add_days(self.year_end, -3), + 'to_date': self.year_end, + 'employee': self.employee_id + } + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_opening_balance_considers_carry_forwarded_leaves(self): + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1) + leave_type.insert() + + # 30 leaves allocated for first half of the year + allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, + to_date=self.mid_year, leave_type=leave_type.name) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application.reload() + # 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation + allocation2 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.mid_year, 1), to_date=self.year_end, + carry_forward=True, leave_type=leave_type.name) + + # Case 1: carry forwarded leaves considered in opening balance for second alloc + filters = { + 'from_date': add_days(self.mid_year, 1), + 'to_date': self.year_end, + 'employee': self.employee_id + } + report = execute(filters) + # available leaves from old alloc + opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days + self.assertEqual(report[1][0].opening_balance, opening_balance) + + # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc + filters = { + 'from_date': add_days(self.mid_year, 2), + 'to_date': self.year_end, + 'employee': self.employee_id + } + report = execute(filters) + # available leaves from old alloc + opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days) + self.assertEqual(report[1][0].opening_balance, opening_balance) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 20060f479ac..bd89bf4e489 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1092,15 +1092,21 @@ def setup_test(): frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) -def make_holiday_list(): +def make_holiday_list(list_name=None, from_date=None, to_date=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) +<<<<<<< HEAD holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): +======= + name = list_name or "Salary Slip Test Holiday List" + holiday_list = frappe.db.exists("Holiday List", name) + if not holiday_list: +>>>>>>> c050ce49c2 (test: employee leave balance report) holiday_list = frappe.get_doc({ "doctype": "Holiday List", - "holiday_list_name": "Salary Slip Test Holiday List", - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], + "holiday_list_name": name, + "from_date": from_date or fiscal_year[1], + "to_date": to_date or fiscal_year[2], "weekly_off": "Sunday" }).insert() holiday_list.get_weekly_off_dates() From 49434e443c0d9c82c4e0f69c613c7a0e69e3405f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 10:19:16 +0530 Subject: [PATCH 074/136] chore: remove unused imports, sort imports, fix sider (cherry picked from commit c7d594984a716ad8f84932b9c3f61a09f4f6b33a) --- .../employee_leave_balance.py | 30 +++++++++---------- .../test_employee_leave_balance.py | 23 +++++++------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 3a5f2fe9629..5c18d11721d 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -6,7 +6,7 @@ from itertools import groupby import frappe from frappe import _ -from frappe.utils import add_days, date_diff, getdate +from frappe.utils import add_days, getdate from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation from erpnext.hr.doctype.leave_application.leave_application import ( @@ -223,21 +223,21 @@ def get_leave_ledger_entries(from_date, to_date, employee, leave_type): ledger = frappe.qb.DocType('Leave Ledger Entry') records = ( frappe.qb.from_(ledger) - .select( - ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date, - ledger.leaves, ledger.transaction_name, ledger.transaction_type, - ledger.is_carry_forward, ledger.is_expired - ).where( - (ledger.docstatus == 1) - & (ledger.transaction_type == 'Leave Allocation') - & (ledger.employee == employee) - & (ledger.leave_type == leave_type) - & ( - (ledger.from_date[from_date: to_date]) - | (ledger.to_date[from_date: to_date]) - | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) - ) + .select( + ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date, + ledger.leaves, ledger.transaction_name, ledger.transaction_type, + ledger.is_carry_forward, ledger.is_expired + ).where( + (ledger.docstatus == 1) + & (ledger.transaction_type == 'Leave Allocation') + & (ledger.employee == employee) + & (ledger.leave_type == leave_type) + & ( + (ledger.from_date[from_date: to_date]) + | (ledger.to_date[from_date: to_date]) + | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) ) + ) ).run(as_dict=True) return records diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py index 05316f165f3..f844be5ff11 100644 --- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -5,22 +5,21 @@ import unittest import frappe -from frappe.utils import ( - add_days, - add_months, - get_year_ending, - get_year_start, - getdate, - flt, -) +from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_start, getdate from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list -from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type -from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday, make_allocation_record -from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list, make_leave_application +from erpnext.hr.doctype.leave_application.test_leave_application import ( + get_first_sunday, + make_allocation_record, +) from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type +from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) test_records = frappe.get_test_records('Leave Type') From 73b104aa459f2bdb89bfd50c0a3f56dec451a148 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 12:12:52 +0530 Subject: [PATCH 075/136] test: Employee Leave Balance Summary (cherry picked from commit 88141d6116bb9699d629e6a22deff36a876de185) --- .../test_employee_leave_balance.py | 6 +- .../employee_leave_balance_summary.py | 1 + .../test_employee_leave_balance_summary.py | 117 ++++++++++++++++++ 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py index f844be5ff11..aecf0a4d4e5 100644 --- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -31,13 +31,13 @@ class TestEmployeeLeaveBalance(unittest.TestCase): frappe.set_user('Administrator') self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') - self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', get_year_start(getdate()), get_year_ending(getdate())) self.date = getdate() self.year_start = getdate(get_year_start(self.date)) self.mid_year = add_months(self.year_start, 6) self.year_end = getdate(get_year_ending(self.date)) + self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end) def tearDown(self): frappe.db.rollback() @@ -56,7 +56,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase): # 4 days leave first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type') + leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') leave_application.reload() filters = { @@ -89,7 +89,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase): allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) # 4 days leave application in the first allocation first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), '_Test Leave Type') + leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') leave_application.reload() # Case 1: opening balance for first alloc boundary diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 936184a9c0d..e484d9e3a11 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -12,6 +12,7 @@ from erpnext.hr.report.employee_leave_balance.employee_leave_balance import ( def execute(filters=None): + filters = frappe._dict(filters or {}) leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") columns = get_columns(leave_types) diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py new file mode 100644 index 00000000000..9b953de0dc2 --- /dev/null +++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py @@ -0,0 +1,117 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import unittest + +import frappe +from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import ( + get_first_sunday, + make_allocation_record, +) +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation +from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) + +test_records = frappe.get_test_records('Leave Type') + +class TestEmployeeLeaveBalance(unittest.TestCase): + def setUp(self): + for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']: + frappe.db.delete(dt) + + frappe.set_user('Administrator') + + self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + + self.date = getdate() + self.year_start = getdate(get_year_start(self.date)) + self.year_end = getdate(get_year_ending(self.date)) + + self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end) + + def tearDown(self): + frappe.db.rollback() + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_employee_leave_balance_summary(self): + frappe.get_doc(test_records[0]).insert() + + # 5 leaves + allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), leaves=5) + # 30 leaves + allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + + # 2 days leave within the first allocation + leave_application1 = make_leave_application(self.employee_id, add_days(self.year_start, -11), add_days(self.year_start, -10), + '_Test Leave Type') + leave_application1.reload() + + # expires 3 leaves + process_expired_allocation() + + # 4 days leave within the second allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application2.reload() + + filters = { + 'date': self.date, + 'company': '_Test Company', + 'employee': self.employee_id + } + + report = execute(filters) + + expected_data = [[ + self.employee_id, + 'test_emp_leave_balance@example.com', + frappe.db.get_value('Employee', self.employee_id, 'department'), + flt( + allocation1.new_leaves_allocated # allocated = 5 + + allocation2.new_leaves_allocated # allocated = 30 + - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2 + - (allocation1.new_leaves_allocated - leave_application1.total_leave_days) # leaves expired from 1st alloc = 3 + - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4 + ) + ]] + + self.assertEqual(report[1], expected_data) + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_get_leave_balance_near_alloc_expiry(self): + frappe.get_doc(test_records[0]).insert() + + # 30 leaves allocated + allocation = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application.reload() + + # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date + # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 + filters = { + 'date': add_days(self.year_end, -3), + 'company': '_Test Company', + 'employee': self.employee_id + } + report = execute(filters) + + expected_data = [[ + self.employee_id, + 'test_emp_leave_balance@example.com', + frappe.db.get_value('Employee', self.employee_id, 'department'), + flt(allocation.new_leaves_allocated - leave_application.total_leave_days) + ]] + + self.assertEqual(report[1], expected_data) From b1d3d8226c79ef59a0722f0b9d560a3c76ebaebb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 14:10:23 +0530 Subject: [PATCH 076/136] fix: leave application dashboard - total leaves allocated considering cancelled leaves - optional plural for leave category labels - show dashboard only once from date is set, else it fetches all allocations till date and generates incorrect balance - change pending leaves to 'Leaves Pending Approval' for better context - update labels in Salary Slip Leave Details table (cherry picked from commit 942511cfffe9e294e3baaf50206d3781ba59c8bf) --- .../doctype/leave_application/leave_application.js | 3 ++- .../doctype/leave_application/leave_application.py | 7 ++++--- .../leave_application_dashboard.html | 12 ++++++------ erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +- .../salary_slip_leave/salary_slip_leave.json | 13 +++++++------ 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 9e8cb5516f3..85997a4087f 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", { make_dashboard: function(frm) { var leave_details; let lwps; - if (frm.doc.employee) { + if (frm.doc.employee && frm.doc.from_date) { frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", async: false, @@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", { }, to_date: function(frm) { + frm.trigger("make_dashboard"); frm.trigger("half_day_datepicker"); frm.trigger("calculate_total_days"); }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index db7560d78aa..5a551d70394 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -522,6 +522,7 @@ def get_leave_details(employee, date): 'to_date': ('>=', date), 'employee': employee, 'leave_type': allocation.leave_type, + 'docstatus': 1 }, 'SUM(total_leaves_allocated)') or 0 remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, @@ -529,13 +530,13 @@ def get_leave_details(employee, date): end_date = allocation.to_date leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1 - leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date) + leaves_pending = get_leaves_pending_approval_for_period(employee, d, allocation.from_date, end_date) leave_allocation[d] = { "total_leaves": total_allocated_leaves, "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), "leaves_taken": leaves_taken, - "pending_leaves": leaves_pending, + "leaves_pending_approval": leaves_pending, "remaining_leaves": remaining_leaves} #is used in set query @@ -633,7 +634,7 @@ def get_leave_allocation_records(employee, date, leave_type=None): })) return allocated_leaves -def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): +def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_date): ''' Returns leaves that are pending approval ''' leaves = frappe.get_all("Leave Application", filters={ diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 9f667a68356..e755322efda 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -4,11 +4,11 @@ {{ __("Leave Type") }} - {{ __("Total Allocated Leave") }} - {{ __("Expired Leave") }} - {{ __("Used Leave") }} - {{ __("Pending Leave") }} - {{ __("Available Leave") }} + {{ __("Total Allocated Leave(s)") }} + {{ __("Expired Leave(s)") }} + {{ __("Used Leave(s)") }} + {{ __("Leave(s) Pending Approval") }} + {{ __("Available Leave(s)") }} @@ -18,7 +18,7 @@ {%= value["total_leaves"] %} {%= value["expired_leaves"] %} {%= value["leaves_taken"] %} - {%= value["pending_leaves"] %} + {%= value["leaves_pending_approval"] %} {%= value["remaining_leaves"] %} {% } %} diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 422bb0e1bbc..6238f9c10e1 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1390,7 +1390,7 @@ class SalarySlip(TransactionBase): 'total_allocated_leaves': flt(leave_values.get('total_leaves')), 'expired_leaves': flt(leave_values.get('expired_leaves')), 'used_leaves': flt(leave_values.get('leaves_taken')), - 'pending_leaves': flt(leave_values.get('pending_leaves')), + 'pending_leaves': flt(leave_values.get('leaves_pending_approval')), 'available_leaves': flt(leave_values.get('remaining_leaves')) }) diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json index 7ac453b3c3d..60ed4539385 100644 --- a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json +++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json @@ -26,7 +26,7 @@ "fieldname": "total_allocated_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Total Allocated Leave", + "label": "Total Allocated Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -34,7 +34,7 @@ "fieldname": "expired_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Expired Leave", + "label": "Expired Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -42,7 +42,7 @@ "fieldname": "used_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Used Leave", + "label": "Used Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -50,7 +50,7 @@ "fieldname": "pending_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Pending Leave", + "label": "Leave(s) Pending Approval", "no_copy": 1, "read_only": 1 }, @@ -58,7 +58,7 @@ "fieldname": "available_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Available Leave", + "label": "Available Leave(s)", "no_copy": 1, "read_only": 1 } @@ -66,7 +66,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-19 10:47:48.546724", + "modified": "2022-02-28 14:01:32.327204", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip Leave", @@ -74,5 +74,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From d9ec973096f1c3cdc8ee4c04925334b2a309dcad Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 14:26:49 +0530 Subject: [PATCH 077/136] fix: earned leave policy assignment test (cherry picked from commit aaa1ae94f255b5acc4dbb197c81c7cf9f5fd4f31) --- .../test_leave_policy_assignment.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 8d7b27ee5af..08680425a02 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import add_months, get_first_day, get_last_day, getdate +from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -94,9 +94,12 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } + + # second last day of the month + # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency + frappe.flags.current_date = add_days(get_last_day(getdate()), -1) leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency leaves_allocated = frappe.db.get_value("Leave Allocation", { "leave_policy_assignment": leave_policy_assignments[0] }, "total_leaves_allocated") From 758e095ffde9860d9f411c736ea4d0b22a4335c2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 15:27:24 +0530 Subject: [PATCH 078/136] test: fix test `test_leave_balance_near_allocaton_expiry` (cherry picked from commit a58dfecb23876120e180e2984009a9cb2d07b720) --- erpnext/hr/doctype/leave_application/leave_application.py | 5 +---- .../hr/doctype/leave_application/test_leave_application.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 5a551d70394..cad1b475793 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -672,10 +672,7 @@ def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date) - return { - 'leave_balance': leave_balance, - 'leave_balance_for_consumption': remaining_leaves - } + return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves) def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 2e93c41a7f7..b4547280448 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -516,8 +516,10 @@ class TestLeaveApplication(unittest.TestCase): leave_type.insert() create_carry_forwarded_allocation(employee, leave_type) + details = get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True) - self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) + self.assertEqual(details.leave_balance_for_consumption, 21) + self.assertEqual(details.leave_balance, 30) def test_earned_leaves_creation(self): From 4cd89c3dac423eadcfad164dfa6b304e25f94dac Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 28 Feb 2022 17:30:09 +0530 Subject: [PATCH 079/136] test: get leave details for leave application dashboard (cherry picked from commit 3f3b1766c2159fbe5733615e5a00a4698d279937) # Conflicts: # erpnext/hr/doctype/leave_application/test_leave_application.py --- .../leave_application/leave_application.py | 12 ++-- .../test_leave_application.py | 60 +++++++++++++++++-- .../doctype/salary_slip/test_salary_slip.py | 9 +-- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index cad1b475793..7fecc0c67fd 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -540,16 +540,14 @@ def get_leave_details(employee, date): "remaining_leaves": remaining_leaves} #is used in set query - lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) - lwps = [lwp.name for lwp in lwps] + lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name") - ret = { - 'leave_allocation': leave_allocation, - 'leave_approver': get_leave_approver(employee), - 'lwps': lwps + return { + "leave_allocation": leave_allocation, + "leave_approver": get_leave_approver(employee), + "lwps": lwp } - return ret @frappe.whitelist() def get_leave_balance_on(employee, leave_type, date, to_date=None, diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index b4547280448..abfc5dfdca4 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -17,12 +17,14 @@ from frappe.utils import ( ) from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_application.leave_application import ( LeaveDayBlockedError, NotAnOptionalHoliday, OverlapError, get_leave_balance_on, + get_leave_details, ) from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, @@ -33,7 +35,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_leave_application, ) -test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] +test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"] _test_records = [ { @@ -72,12 +74,13 @@ _test_records = [ class TestLeaveApplication(unittest.TestCase): def setUp(self): for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: - frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec + frappe.db.delete(dt) frappe.set_user("Administrator") set_leave_approver() - frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") + frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"}) + self.holiday_list = make_holiday_list() def tearDown(self): frappe.db.rollback() @@ -119,6 +122,7 @@ class TestLeaveApplication(unittest.TestCase): for d in ('2018-01-01', '2018-01-02', '2018-01-03'): self.assertTrue(getdate(d) in dates) + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') def test_attendance_for_include_holidays(self): # Case 1: leave type with 'Include holidays within leaves as leaves' enabled frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1) @@ -131,22 +135,29 @@ class TestLeaveApplication(unittest.TestCase): date = getdate() make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) - holiday_list = make_holiday_list() employee = get_employee() +<<<<<<< HEAD original_holiday_list = employee.holiday_list frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) +======= + first_sunday = get_first_sunday(self.holiday_list) +>>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), leave_type.name) leave_application.reload() self.assertEqual(leave_application.total_leave_days, 4) self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) leave_application.cancel() +<<<<<<< HEAD frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) +======= + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') +>>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) def test_attendance_update_for_exclude_holidays(self): # Case 2: leave type with 'Include holidays within leaves as leaves' disabled frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) @@ -159,11 +170,14 @@ class TestLeaveApplication(unittest.TestCase): date = getdate() make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) - holiday_list = make_holiday_list() employee = get_employee() +<<<<<<< HEAD original_holiday_list = employee.holiday_list frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) +======= + first_sunday = get_first_sunday(self.holiday_list) +>>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) # already marked attendance on a holiday should be deleted in this case config = { @@ -327,17 +341,22 @@ class TestLeaveApplication(unittest.TestCase): application.half_day_date = "2013-01-05" application.insert() + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') def test_optional_leave(self): leave_period = get_leave_period() today = nowdate() holiday_list = 'Test Holiday List for Optional Holiday' employee = get_employee() +<<<<<<< HEAD default_holiday_list = make_holiday_list() original_holiday_list = employee.holiday_list frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list) first_sunday = get_first_sunday(default_holiday_list) +======= + first_sunday = get_first_sunday(self.holiday_list) +>>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) optional_leave_date = add_days(first_sunday, 1) if not frappe.db.exists('Holiday List', holiday_list): @@ -715,6 +734,35 @@ class TestLeaveApplication(unittest.TestCase): employee.leave_approver = "" employee.save() + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_get_leave_details_for_dashboard(self): + employee = get_employee() + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + # ALLOCATION = 30 + allocation = make_allocation_record(employee=employee.name, from_date=year_start, to_date=year_end) + + # USED LEAVES = 4 + first_sunday = get_first_sunday(self.holiday_list) + leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application.reload() + + # LEAVES PENDING APPROVAL = 1 + leave_application = make_leave_application(employee.name, add_days(first_sunday, 5), add_days(first_sunday, 5), + '_Test Leave Type', submit=False) + leave_application.status = 'Open' + leave_application.save() + + details = get_leave_details(employee.name, allocation.from_date) + leave_allocation = details['leave_allocation']['_Test Leave Type'] + self.assertEqual(leave_allocation['total_leaves'], 30) + self.assertEqual(leave_allocation['leaves_taken'], 4) + self.assertEqual(leave_allocation['expired_leaves'], 0) + self.assertEqual(leave_allocation['leaves_pending_approval'], 1) + self.assertEqual(leave_allocation['remaining_leaves'], 26) + def create_carry_forwarded_allocation(employee, leave_type): # initial leave allocation diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index bd89bf4e489..bdf69397266 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1062,7 +1062,7 @@ def create_additional_salary(employee, payroll_period, amount): }).submit() return salary_date -def make_leave_application(employee, from_date, to_date, leave_type, company=None): +def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True): leave_application = frappe.get_doc(dict( doctype = 'Leave Application', employee = employee, @@ -1070,11 +1070,12 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non from_date = from_date, to_date = to_date, company = company or erpnext.get_default_company() or "_Test Company", - docstatus = 1, status = "Approved", leave_approver = 'test@example.com' - )) - leave_application.submit() + )).insert() + + if submit: + leave_application.submit() return leave_application From 51b17cfbc09bbef1589d5d223b98f9fb2a904b8f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Mar 2022 13:36:08 +0530 Subject: [PATCH 080/136] fix: add type hints for employee leave balance report (cherry picked from commit 430bf004588d4c5b759ad4ef8e95377f4473b4a4) --- .../employee_leave_balance.py | 28 +++++++++---------- .../test_employee_leave_balance.py | 24 ++++++++-------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 5c18d11721d..3324ede1dd7 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -3,6 +3,7 @@ from itertools import groupby +from typing import Dict, List, Tuple import frappe from frappe import _ @@ -14,10 +15,9 @@ from erpnext.hr.doctype.leave_application.leave_application import ( get_leaves_for_period, ) +Filters = frappe._dict -def execute(filters=None): - filters = frappe._dict(filters or {}) - +def execute(filters: Filters = None) -> Tuple: if filters.to_date <= filters.from_date: frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) @@ -26,8 +26,9 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts -def get_columns(): - columns = [{ + +def get_columns() -> List[Dict]: + return [{ 'label': _('Leave Type'), 'fieldtype': 'Link', 'fieldname': 'leave_type', @@ -72,9 +73,8 @@ def get_columns(): 'width': 150, }] - return columns -def get_data(filters): +def get_data(filters: Filters) -> List: leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name') conditions = get_conditions(filters) @@ -128,7 +128,7 @@ def get_data(filters): return data -def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves): +def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float) -> float: # allocation boundary condition # opening balance is the closing leave balance 1 day before the filter start date opening_balance_date = add_days(filters.from_date, -1) @@ -146,7 +146,7 @@ def get_opening_balance(employee, leave_type, filters, carry_forwarded_leaves): return opening_balance -def get_conditions(filters): +def get_conditions(filters: Filters) -> Dict: conditions={ 'status': 'Active', } @@ -162,7 +162,7 @@ def get_conditions(filters): return conditions -def get_department_leave_approver_map(department=None): +def get_department_leave_approver_map(department: str = None): # get current department and all its child department_list = frappe.get_list('Department', filters={'disabled': 0}, @@ -190,7 +190,7 @@ def get_department_leave_approver_map(department=None): return approvers -def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): +def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str, leave_type: str) -> Tuple[float, float, float]: new_allocation = 0 expired_leaves = 0 carry_forwarded_leaves = 0 @@ -219,7 +219,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): return new_allocation, expired_leaves, carry_forwarded_leaves -def get_leave_ledger_entries(from_date, to_date, employee, leave_type): +def get_leave_ledger_entries(from_date: str, to_date: str, employee: str, leave_type: str) -> List[Dict]: ledger = frappe.qb.DocType('Leave Ledger Entry') records = ( frappe.qb.from_(ledger) @@ -243,7 +243,7 @@ def get_leave_ledger_entries(from_date, to_date, employee, leave_type): return records -def get_chart_data(data): +def get_chart_data(data: List) -> Dict: labels = [] datasets = [] employee_data = data @@ -263,7 +263,7 @@ def get_chart_data(data): return chart -def get_dataset_for_chart(employee_data, datasets, labels): +def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List: leaves = [] employee_data = sorted(employee_data, key=lambda k: k['employee_name']) diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py index aecf0a4d4e5..b2ed72c04d7 100644 --- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -59,11 +59,11 @@ class TestEmployeeLeaveBalance(unittest.TestCase): leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') leave_application.reload() - filters = { + filters = frappe._dict({ 'from_date': allocation1.from_date, 'to_date': allocation2.to_date, 'employee': self.employee_id - } + }) report = execute(filters) @@ -93,30 +93,30 @@ class TestEmployeeLeaveBalance(unittest.TestCase): leave_application.reload() # Case 1: opening balance for first alloc boundary - filters = { + filters = frappe._dict({ 'from_date': self.year_start, 'to_date': self.year_end, 'employee': self.employee_id - } + }) report = execute(filters) self.assertEqual(report[1][0].opening_balance, 0) # Case 2: opening balance after leave application date - filters = { + filters = frappe._dict({ 'from_date': add_days(leave_application.to_date, 1), 'to_date': self.year_end, 'employee': self.employee_id - } + }) report = execute(filters) self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 - filters = { + filters = frappe._dict({ 'from_date': add_days(self.year_end, -3), 'to_date': self.year_end, 'employee': self.employee_id - } + }) report = execute(filters) self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) @@ -139,22 +139,22 @@ class TestEmployeeLeaveBalance(unittest.TestCase): carry_forward=True, leave_type=leave_type.name) # Case 1: carry forwarded leaves considered in opening balance for second alloc - filters = { + filters = frappe._dict({ 'from_date': add_days(self.mid_year, 1), 'to_date': self.year_end, 'employee': self.employee_id - } + }) report = execute(filters) # available leaves from old alloc opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days self.assertEqual(report[1][0].opening_balance, opening_balance) # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc - filters = { + filters = frappe._dict({ 'from_date': add_days(self.mid_year, 2), 'to_date': self.year_end, 'employee': self.employee_id - } + }) report = execute(filters) # available leaves from old alloc opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days) From a30ec8bef664b1d6badda951cd0f89d290ff4939 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Mar 2022 10:25:49 +0530 Subject: [PATCH 081/136] feat: split ledger entries for applications created across allocations - fix: ledger entry for expiring cf leaves not considering holidays (cherry picked from commit c0f1e269e4982052b265cb209f5b9cea7ca5eded) --- .../leave_application/leave_application.py | 139 +++++++++++++----- 1 file changed, 101 insertions(+), 38 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 7fecc0c67fd..de45c86bb38 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -138,21 +138,36 @@ class LeaveApplication(Document): def validate_dates_across_allocation(self): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): return - def _get_leave_allocation_record(date): - allocation = frappe.db.sql("""select name from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 - and %s between from_date and to_date""", (self.employee, self.leave_type, date)) - return allocation and allocation[0][0] + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + + if not (alloc_on_from_date or alloc_on_to_date): + frappe.throw(_("Application period cannot be outside leave allocation period")) + + elif alloc_on_from_date.name != alloc_on_to_date.name: + frappe.throw(_("Application period cannot be across two allocation records")) + + def get_allocation_based_on_application_dates(self): + """Returns allocation name, from and to dates for application dates""" + def _get_leave_allocation_record(date): + LeaveAllocation = frappe.qb.DocType("Leave Allocation") + allocation = ( + frappe.qb.from_(LeaveAllocation) + .select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date) + .where( + (LeaveAllocation.employee == self.employee) + & (LeaveAllocation.leave_type == self.leave_type) + & (LeaveAllocation.docstatus == 1) + & ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date)) + ) + ).run(as_dict=True) + + return allocation and allocation[0] allocation_based_on_from_date = _get_leave_allocation_record(self.from_date) allocation_based_on_to_date = _get_leave_allocation_record(self.to_date) - if not (allocation_based_on_from_date or allocation_based_on_to_date): - frappe.throw(_("Application period cannot be outside leave allocation period")) - - elif allocation_based_on_from_date != allocation_based_on_to_date: - frappe.throw(_("Application period cannot be across two allocation records")) + return allocation_based_on_from_date, allocation_based_on_to_date def validate_back_dated_application(self): future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` @@ -434,49 +449,97 @@ class LeaveApplication(Document): expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type, self.to_date, self.from_date) - lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") if expiry_date: self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp) else: - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): + # required only if negative balance is allowed for leave type + # else will be stopped in validation itself + self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp) + else: + raise_exception = False if frappe.flags.in_patch else True + args = dict( + leaves=self.total_leave_days * -1, + from_date=self.from_date, + to_date=self.to_date, + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + ) + create_leave_ledger_entry(self, args, submit) - args = dict( - leaves=self.total_leave_days * -1, + def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool: + if ((alloc_on_from_date and not alloc_on_to_date) + or (not alloc_on_from_date and alloc_on_to_date) + or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)): + return True + return False + + def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp): + """Creates separate ledger entries for application period falling into separate allocations""" + # for creating separate ledger entries existing allocation periods should be consecutive + if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date: + frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format( + get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date))) + + raise_exception = False if frappe.flags.in_patch else True + leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type, + self.from_date, alloc_on_from_date.to_date, self.half_day, self.half_day_date) + leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type, + add_days(alloc_on_from_date.to_date, 1), self.to_date, self.half_day, self.half_day_date) + + args = dict( + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + ) + + if leaves_in_first_alloc: + args.update(dict( from_date=self.from_date, + to_date=alloc_on_from_date.to_date, + leaves=leaves_in_first_alloc * -1 + )) + create_leave_ledger_entry(self, args, submit) + + if leaves_in_second_alloc: + args.update(dict( + from_date=add_days(alloc_on_from_date.to_date, 1), to_date=self.to_date, + leaves=leaves_in_second_alloc * -1 + )) + create_leave_ledger_entry(self, args, submit) + + def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): + """Splits leave application into two ledger entries to consider expiry of allocation""" + raise_exception = False if frappe.flags.in_patch else True + + leaves = get_number_of_leave_days(self.employee, self.leave_type, + self.from_date, expiry_date, self.half_day, self.half_day_date) + + if leaves: + args = dict( + from_date=self.from_date, + to_date=expiry_date, + leaves=leaves * -1, is_lwp=lwp, holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' ) create_leave_ledger_entry(self, args, submit) - def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): - ''' splits leave application into two ledger entries to consider expiry of allocation ''' - - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False - - args = dict( - from_date=self.from_date, - to_date=expiry_date, - leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, - is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' - ) - create_leave_ledger_entry(self, args, submit) - if getdate(expiry_date) != getdate(self.to_date): start_date = add_days(expiry_date, 1) - args.update(dict( - from_date=start_date, - to_date=self.to_date, - leaves=date_diff(self.to_date, expiry_date) * -1 - )) - create_leave_ledger_entry(self, args, submit) + leaves = get_number_of_leave_days(self.employee, self.leave_type, + start_date, self.to_date, self.half_day, self.half_day_date) + + if leaves: + args.update(dict( + from_date=start_date, + to_date=self.to_date, + leaves=leaves * -1 + )) + create_leave_ledger_entry(self, args, submit) def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date): From bfedac47e6a356b4d795054e2df0833ca90eb5bf Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Mar 2022 13:49:07 +0530 Subject: [PATCH 082/136] fix: clearer validation/warning messages for insufficient balance in leave application (cherry picked from commit a504ffcc4ccc1a445814c45f52a234238a8538e5) --- .../leave_application/leave_application.py | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index de45c86bb38..10244138318 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -143,8 +143,7 @@ class LeaveApplication(Document): if not (alloc_on_from_date or alloc_on_to_date): frappe.throw(_("Application period cannot be outside leave allocation period")) - - elif alloc_on_from_date.name != alloc_on_to_date.name: + elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): frappe.throw(_("Application period cannot be across two allocation records")) def get_allocation_based_on_application_dates(self): @@ -285,12 +284,28 @@ class LeaveApplication(Document): leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption") if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption): - if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Insufficient leave balance for Leave Type {0}") - .format(frappe.bold(self.leave_type)), title=_("Warning"), indicator="orange") - else: - frappe.throw(_("Insufficient leave balance for Leave Type {0}") - .format(self.leave_type), InsufficientLeaveBalanceError, title=_("Insufficient Balance")) + self.show_insufficient_balance_message(leave_balance_for_consumption) + + def show_insufficient_balance_message(self, leave_balance_for_consumption): + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + if leave_balance_for_consumption != self.leave_balance: + msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type)) + msg += "

" + msg += _("Actual leave balance is {0} but only {1} leave(s) can be consumed between {2} (Application Date) and {3} (Allocation Expiry).").format( + frappe.bold(self.leave_balance), frappe.bold(leave_balance_for_consumption), + frappe.bold(formatdate(self.from_date)), + frappe.bold(formatdate(alloc_on_from_date.to_date))) + msg += "
" + msg += _("Remaining leaves would be compensated in the next allocation.") + else: + msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type)) + + frappe.msgprint(msg, title=_("Warning"), indicator="orange") + else: + frappe.throw(_("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)), + exc=InsufficientLeaveBalanceError, title=_("Insufficient Balance")) def validate_leave_overlap(self): if not self.name: @@ -471,6 +486,7 @@ class LeaveApplication(Document): create_leave_ledger_entry(self, args, submit) def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool: + """Checks if application dates fall in separate allocations""" if ((alloc_on_from_date and not alloc_on_to_date) or (not alloc_on_from_date and alloc_on_to_date) or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)): From b63cedc7d74ad05a1dd7a61cd7ba173477abaf13 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Mar 2022 15:48:57 +0530 Subject: [PATCH 083/136] test: leave application validations (cherry picked from commit 6755d6e6f5bb17d5cad58f3bf064796239879211) --- .../leave_application/leave_application.py | 4 +- .../test_leave_application.py | 94 ++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 10244138318..2256b53064a 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -38,6 +38,8 @@ class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass class InsufficientLeaveBalanceError(frappe.ValidationError): pass +class LeaveAcrossAllocationsError(frappe.ValidationError): + pass from frappe.model.document import Document @@ -144,7 +146,7 @@ class LeaveApplication(Document): if not (alloc_on_from_date or alloc_on_to_date): frappe.throw(_("Application period cannot be outside leave allocation period")) elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): - frappe.throw(_("Application period cannot be across two allocation records")) + frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError) def get_allocation_based_on_application_dates(self): """Returns allocation name, from and to dates for application dates""" diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index abfc5dfdca4..b055df6ab67 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -20,6 +20,8 @@ from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_application.leave_application import ( + InsufficientLeaveBalanceError, + LeaveAcrossAllocationsError, LeaveDayBlockedError, NotAnOptionalHoliday, OverlapError, @@ -82,8 +84,16 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"}) self.holiday_list = make_holiday_list() + if not frappe.db.exists("Leave Type", "_Test Leave Type"): + frappe.get_doc(dict( + leave_type_name="_Test Leave Type", + doctype="Leave Type", + include_holiday=True + )).insert() + def tearDown(self): frappe.db.rollback() + frappe.set_user("Administrator") def _clear_roles(self): frappe.db.sql("""delete from `tabHas Role` where parent in @@ -98,6 +108,81 @@ class TestLeaveApplication(unittest.TestCase): application.to_date = "2013-01-05" return application + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_validate_application_across_allocations(self): + # Test validation for application dates when negative balance is disabled + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=False + )).insert() + + employee = get_employee() + date = getdate() + first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) + + leave_application = frappe.get_doc(dict( + doctype='Leave Application', + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 4), + company="_Test Company", + status="Approved", + leave_approver = 'test@example.com' + )) + # Application period cannot be outside leave allocation period + self.assertRaises(frappe.ValidationError, leave_application.insert) + + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + + leave_application = frappe.get_doc(dict( + doctype='Leave Application', + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, -10), + to_date=add_days(first_sunday, 1), + company="_Test Company", + status="Approved", + leave_approver = 'test@example.com' + )) + + # Application period cannot be across two allocation records + self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert) + + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_insufficient_leave_balance_validation(self): + # CASE 1: Validation when allow negative is disabled + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=False + )).insert() + + employee = get_employee() + date = getdate() + first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) + + # allocate 2 leaves, apply for more + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date), leaves=2) + leave_application = frappe.get_doc(dict( + doctype='Leave Application', + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 3), + company="_Test Company", + status="Approved", + leave_approver = 'test@example.com' + )) + self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert) + + # CASE 2: Allows creating application with a warning message when allow negative is enabled + frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True) + make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name) + def test_overwrite_attendance(self): '''check attendance is automatically created on leave approval''' make_allocation_record() @@ -591,7 +676,14 @@ class TestLeaveApplication(unittest.TestCase): # test to not consider current leave in leave balance while submitting def test_current_leave_on_submit(self): employee = get_employee() - leave_type = 'Sick leave' + + leave_type = 'Sick Leave' + if not frappe.db.exists('Leave Type', leave_type): + frappe.get_doc(dict( + leave_type_name=leave_type, + doctype='Leave Type' + )).insert() + allocation = frappe.get_doc(dict( doctype = 'Leave Allocation', employee = employee.name, From 75833243aebf12467a0931468762dab982b5bcb0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Mar 2022 16:18:23 +0530 Subject: [PATCH 084/136] fix: boundary determination for separate ledger entries (cherry picked from commit 70239158b976ede73e55cc2d34f622752c020666) --- .../leave_application/leave_application.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 2256b53064a..0fa34ac6f33 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -503,10 +503,18 @@ class LeaveApplication(Document): get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date))) raise_exception = False if frappe.flags.in_patch else True + + if alloc_on_from_date: + first_alloc_end = alloc_on_from_date.to_date + second_alloc_start = add_days(alloc_on_from_date.to_date, 1) + else: + first_alloc_end = add_days(alloc_on_to_date.from_date, -1) + second_alloc_start = alloc_on_to_date.from_date + leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type, - self.from_date, alloc_on_from_date.to_date, self.half_day, self.half_day_date) + self.from_date, first_alloc_end, self.half_day, self.half_day_date) leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type, - add_days(alloc_on_from_date.to_date, 1), self.to_date, self.half_day, self.half_day_date) + second_alloc_start, self.to_date, self.half_day, self.half_day_date) args = dict( is_lwp=lwp, @@ -516,14 +524,14 @@ class LeaveApplication(Document): if leaves_in_first_alloc: args.update(dict( from_date=self.from_date, - to_date=alloc_on_from_date.to_date, + to_date=first_alloc_end, leaves=leaves_in_first_alloc * -1 )) create_leave_ledger_entry(self, args, submit) if leaves_in_second_alloc: args.update(dict( - from_date=add_days(alloc_on_from_date.to_date, 1), + from_date=second_alloc_start, to_date=self.to_date, leaves=leaves_in_second_alloc * -1 )) From fab0153e341449faef58357a16d875e669ab3e97 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Mar 2022 16:35:25 +0530 Subject: [PATCH 085/136] test: separate leave ledger entries for leave applications across allocations (cherry picked from commit 97b7b5012e911f7095810f29bfc328c02e6e34a1) --- .../test_leave_application.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index b055df6ab67..e0f607477b2 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -183,6 +183,57 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True) make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name) + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_separate_leave_ledger_entry_for_boundary_applications(self): + # When application falls in 2 different allocations and Allow Negative is enabled + # creates separate leave ledger entries + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=True + )).insert() + + employee = get_employee() + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end) + # application across allocations + + # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation + application = make_leave_application(employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name) + + # 2 separate leave ledger entries + ledgers = frappe.db.get_all("Leave Ledger Entry", { + "transaction_type": "Leave Application", + "transaction_name": application.name + }, ["leaves", "from_date", "to_date"], order_by="from_date") + self.assertEqual(len(ledgers), 2) + + self.assertEqual(ledgers[0].from_date, application.from_date) + self.assertEqual(ledgers[0].to_date, add_days(year_start, -1)) + + self.assertEqual(ledgers[1].from_date, year_start) + self.assertEqual(ledgers[1].to_date, application.to_date) + + # CASE 2: from date has an allocation, to date has no allocation + application = make_leave_application(employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name) + + # 2 separate leave ledger entries + ledgers = frappe.db.get_all("Leave Ledger Entry", { + "transaction_type": "Leave Application", + "transaction_name": application.name + }, ["leaves", "from_date", "to_date"], order_by="from_date") + self.assertEqual(len(ledgers), 2) + + self.assertEqual(ledgers[0].from_date, application.from_date) + self.assertEqual(ledgers[0].to_date, year_end) + + self.assertEqual(ledgers[1].from_date, add_days(year_end, 1)) + self.assertEqual(ledgers[1].to_date, application.to_date) + def test_overwrite_attendance(self): '''check attendance is automatically created on leave approval''' make_allocation_record() From c66a66e0902195b9fff502d8df13c4b1f1178356 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Mar 2022 15:41:14 +0530 Subject: [PATCH 087/136] fix: conflicts (cherry picked from commit b2c549a464a86d6f693156c87bf9674a02429a0e) --- .../hr/doctype/leave_application/test_leave_application.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index e0f607477b2..1898a0808dd 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -344,8 +344,6 @@ class TestLeaveApplication(unittest.TestCase): # attendance on non-holiday updated self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave") - frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) - def test_block_list(self): self._clear_roles() @@ -541,8 +539,6 @@ class TestLeaveApplication(unittest.TestCase): # check leave balance is reduced self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9) - frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) - def test_leaves_allowed(self): employee = get_employee() leave_period = get_leave_period() From 9449d11a4cc3817e1a0ace5cfefc5d808cd1fdbd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Mar 2022 15:30:01 +0530 Subject: [PATCH 088/136] fix: simplify insufficient leave balance message (cherry picked from commit 8173e6a8ea8b2659fca6acfcd19a06bde6b89869) --- erpnext/hr/doctype/leave_application/leave_application.py | 7 +------ .../employee_leave_balance/employee_leave_balance.py | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0fa34ac6f33..095d0eae8c2 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -295,12 +295,7 @@ class LeaveApplication(Document): if leave_balance_for_consumption != self.leave_balance: msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type)) msg += "

" - msg += _("Actual leave balance is {0} but only {1} leave(s) can be consumed between {2} (Application Date) and {3} (Allocation Expiry).").format( - frappe.bold(self.leave_balance), frappe.bold(leave_balance_for_consumption), - frappe.bold(formatdate(self.from_date)), - frappe.bold(formatdate(alloc_on_from_date.to_date))) - msg += "
" - msg += _("Remaining leaves would be compensated in the next allocation.") + msg += _("Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation.") else: msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type)) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 3324ede1dd7..66c1d25d593 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -3,7 +3,7 @@ from itertools import groupby -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple import frappe from frappe import _ @@ -17,7 +17,7 @@ from erpnext.hr.doctype.leave_application.leave_application import ( Filters = frappe._dict -def execute(filters: Filters = None) -> Tuple: +def execute(filters: Optional[Filters] = None) -> Tuple: if filters.to_date <= filters.from_date: frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) @@ -162,7 +162,7 @@ def get_conditions(filters: Filters) -> Dict: return conditions -def get_department_leave_approver_map(department: str = None): +def get_department_leave_approver_map(department: Optional[str] = None): # get current department and all its child department_list = frappe.get_list('Department', filters={'disabled': 0}, From 70febe468bca1947e4fc05a7a92337f18a83999b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 13 Mar 2022 20:02:51 +0530 Subject: [PATCH 089/136] fix: add more type hints (cherry picked from commit 558650bc3ac7293818202b307996831a416d7554) --- .../leave_application/leave_application.py | 41 +++++++++++++------ .../employee_leave_balance_summary.py | 1 - .../test_employee_leave_balance_summary.py | 8 ++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 095d0eae8c2..518d79aa34b 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1,7 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from typing import Dict +from typing import Dict, Optional, Tuple import frappe from frappe import _ @@ -148,7 +148,7 @@ class LeaveApplication(Document): elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError) - def get_allocation_based_on_application_dates(self): + def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]: """Returns allocation name, from and to dates for application dates""" def _get_leave_allocation_record(date): LeaveAllocation = frappe.qb.DocType("Leave Allocation") @@ -288,7 +288,7 @@ class LeaveApplication(Document): if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption): self.show_insufficient_balance_message(leave_balance_for_consumption) - def show_insufficient_balance_message(self, leave_balance_for_consumption): + def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None: alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): @@ -482,7 +482,7 @@ class LeaveApplication(Document): ) create_leave_ledger_entry(self, args, submit) - def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool: + def is_separate_ledger_entry_required(self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None) -> bool: """Checks if application dates fall in separate allocations""" if ((alloc_on_from_date and not alloc_on_to_date) or (not alloc_on_from_date and alloc_on_to_date) @@ -563,7 +563,7 @@ class LeaveApplication(Document): create_leave_ledger_entry(self, args, submit) -def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date): +def get_allocation_expiry_for_cf_leaves(employee: str, leave_type: str, to_date: str, from_date: str) -> str: ''' Returns expiry of carry forward allocation in leave ledger entry ''' expiry = frappe.get_all("Leave Ledger Entry", filters={ @@ -574,10 +574,14 @@ def get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, from_date 'to_date': ['between', (from_date, to_date)], 'docstatus': 1 },fields=['to_date']) - return expiry[0]['to_date'] if expiry else None + return expiry[0]['to_date'] if expiry else '' + @frappe.whitelist() -def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None): +def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_date: str, half_day: Optional[int] = None, + half_day_date: Optional[str] = None, holiday_list: Optional[str] = None) -> float: + """Returns number of leave days between 2 dates after considering half day and holidays + (Based on the include_holiday setting in Leave Type)""" number_of_days = 0 if cint(half_day) == 1: if from_date == to_date: @@ -594,6 +598,7 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list)) return number_of_days + @frappe.whitelist() def get_leave_details(employee, date): allocation_records = get_leave_allocation_records(employee, date) @@ -634,8 +639,8 @@ def get_leave_details(employee, date): @frappe.whitelist() -def get_leave_balance_on(employee, leave_type, date, to_date=None, - consider_all_leaves_in_the_allocation_period=False, for_consumption=False): +def get_leave_balance_on(employee: str, leave_type: str, date: str, to_date: str = None, + consider_all_leaves_in_the_allocation_period: bool = False, for_consumption: bool = False): ''' Returns leave balance till date :param employee: employee name @@ -716,8 +721,9 @@ def get_leave_allocation_records(employee, date, leave_type=None): })) return allocated_leaves -def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_date): - ''' Returns leaves that are pending approval ''' + +def get_leaves_pending_approval_for_period(employee: str, leave_type: str, from_date: str, to_date: str) -> float: + ''' Returns leaves that are pending for approval ''' leaves = frappe.get_all("Leave Application", filters={ "employee": employee, @@ -730,7 +736,8 @@ def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_d }, fields=['SUM(total_leave_days) as leaves'])[0] return leaves['leaves'] if leaves['leaves'] else 0.0 -def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]: + +def get_remaining_leaves(allocation: Dict, leaves_taken: float, date: str, cf_expiry: str) -> Dict[str, float]: '''Returns a dict of leave_balance and leave_balance_for_consumption leave_balance returns the available leave balance leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry @@ -756,7 +763,8 @@ def get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) -> Dict[str, remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date) return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves) -def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): + +def get_leaves_for_period(employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True) -> float: leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 @@ -811,6 +819,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): "leave_type": leave_type }, as_dict=1) + @frappe.whitelist() def get_holidays(employee, from_date, to_date, holiday_list = None): '''get holidays between two dates for the given employee''' @@ -827,6 +836,7 @@ def is_lwp(leave_type): lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type) return lwp and cint(lwp[0][0]) or 0 + @frappe.whitelist() def get_events(start, end, filters=None): from frappe.desk.reportview import get_filters_cond @@ -855,6 +865,7 @@ def get_events(start, end, filters=None): return events + def add_department_leaves(events, start, end, employee, company): department = frappe.db.get_value("Employee", employee, "department") @@ -935,6 +946,7 @@ def add_block_dates(events, start, end, employee, company): }) cnt+=1 + def add_holidays(events, start, end, employee, company): applicable_holiday_list = get_holiday_list_for_employee(employee, company) if not applicable_holiday_list: @@ -951,6 +963,7 @@ def add_holidays(events, start, end, employee, company): "name": holiday.name }) + @frappe.whitelist() def get_mandatory_approval(doctype): mandatory = "" @@ -963,6 +976,7 @@ def get_mandatory_approval(doctype): return mandatory + def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): query = """ select employee, leave_type, from_date, to_date, total_leave_days @@ -998,6 +1012,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): return leave_days + @frappe.whitelist() def get_leave_approver(employee): leave_approver, department = frappe.db.get_value("Employee", diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index e484d9e3a11..936184a9c0d 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -12,7 +12,6 @@ from erpnext.hr.report.employee_leave_balance.employee_leave_balance import ( def execute(filters=None): - filters = frappe._dict(filters or {}) leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") columns = get_columns(leave_types) diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py index 9b953de0dc2..b76858d8438 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py @@ -64,11 +64,11 @@ class TestEmployeeLeaveBalance(unittest.TestCase): leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') leave_application2.reload() - filters = { + filters = frappe._dict({ 'date': self.date, 'company': '_Test Company', 'employee': self.employee_id - } + }) report = execute(filters) @@ -100,11 +100,11 @@ class TestEmployeeLeaveBalance(unittest.TestCase): # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 - filters = { + frappe._dict({ 'date': add_days(self.year_end, -3), 'company': '_Test Company', 'employee': self.employee_id - } + }) report = execute(filters) expected_data = [[ From d50dbca844e504fd7b2bf154476561cea0fdb7c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 13 Mar 2022 20:30:18 +0530 Subject: [PATCH 090/136] fix: flaky tests (cherry picked from commit d61c43758893895943e36242e3add3eb2502ac82) --- .../hr/doctype/leave_application/test_leave_application.py | 2 +- .../test_employee_leave_balance_summary.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 1898a0808dd..3b742611bf0 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -281,7 +281,7 @@ class TestLeaveApplication(unittest.TestCase): first_sunday = get_first_sunday(self.holiday_list) >>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) - leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), leave_type.name) + leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application.reload() self.assertEqual(leave_application.total_leave_days, 4) self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py index b76858d8438..6f16a8d58cb 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py @@ -65,7 +65,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase): leave_application2.reload() filters = frappe._dict({ - 'date': self.date, + 'date': add_days(leave_application2.to_date, 1), 'company': '_Test Company', 'employee': self.employee_id }) @@ -100,7 +100,7 @@ class TestEmployeeLeaveBalance(unittest.TestCase): # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 - frappe._dict({ + filters = frappe._dict({ 'date': add_days(self.year_end, -3), 'company': '_Test Company', 'employee': self.employee_id From 465c117708ba44bc7ef97d6f22edd796e3eaaf55 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 13 Mar 2022 21:05:06 +0530 Subject: [PATCH 091/136] fix: conflicts --- .../test_leave_application.py | 26 ------------------- .../doctype/salary_slip/test_salary_slip.py | 5 ---- 2 files changed, 31 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 3b742611bf0..aaf0e4f1ce6 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -272,14 +272,7 @@ class TestLeaveApplication(unittest.TestCase): make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) employee = get_employee() -<<<<<<< HEAD - original_holiday_list = employee.holiday_list - frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) - - first_sunday = get_first_sunday(holiday_list) -======= first_sunday = get_first_sunday(self.holiday_list) ->>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application.reload() @@ -288,12 +281,7 @@ class TestLeaveApplication(unittest.TestCase): leave_application.cancel() -<<<<<<< HEAD - frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) - -======= @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') ->>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) def test_attendance_update_for_exclude_holidays(self): # Case 2: leave type with 'Include holidays within leaves as leaves' disabled frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) @@ -307,13 +295,7 @@ class TestLeaveApplication(unittest.TestCase): make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) employee = get_employee() -<<<<<<< HEAD - original_holiday_list = employee.holiday_list - frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) - first_sunday = get_first_sunday(holiday_list) -======= first_sunday = get_first_sunday(self.holiday_list) ->>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) # already marked attendance on a holiday should be deleted in this case config = { @@ -482,15 +464,7 @@ class TestLeaveApplication(unittest.TestCase): holiday_list = 'Test Holiday List for Optional Holiday' employee = get_employee() -<<<<<<< HEAD - default_holiday_list = make_holiday_list() - original_holiday_list = employee.holiday_list - frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list) - first_sunday = get_first_sunday(default_holiday_list) - -======= first_sunday = get_first_sunday(self.holiday_list) ->>>>>>> 3f3b1766c2 (test: get leave details for leave application dashboard) optional_leave_date = add_days(first_sunday, 1) if not frappe.db.exists('Holiday List', holiday_list): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index bdf69397266..c8e381e58eb 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1095,14 +1095,9 @@ def setup_test(): def make_holiday_list(list_name=None, from_date=None, to_date=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) -<<<<<<< HEAD - holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") - if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): -======= name = list_name or "Salary Slip Test Holiday List" holiday_list = frappe.db.exists("Holiday List", name) if not holiday_list: ->>>>>>> c050ce49c2 (test: employee leave balance report) holiday_list = frappe.get_doc({ "doctype": "Holiday List", "holiday_list_name": name, From 404a82121d05850f5314b6d8a71f1ee9046368de Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 15 Feb 2022 16:35:04 +0530 Subject: [PATCH 092/136] fix: if an item code is too long, truncate before setting BOM name (cherry picked from commit d9c91748f4e8eb37ec1c76ee5718c38de8ce9f65) --- erpnext/manufacturing/doctype/bom/bom.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index cde25a2bc22..e5c046c493a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -121,7 +121,21 @@ class BOM(WebsiteGenerator): else: idx = 1 - name = 'BOM-' + self.item + ('-%.3i' % idx) + prefix = self.doctype + suffix = "%.3i" % idx + bom_name = prefix + "-" + self.item + "-" + suffix + + if len(bom_name) <= 140: + name = bom_name + else: + # since max characters for name is 140, remove enough characters from the + # item name to fit the prefix, suffix and the separators + truncated_length = 140 - (len(prefix) + len(suffix) + 2) + truncated_item_name = self.item[:truncated_length] + # if a partial word is found after truncate, remove the extra characters + truncated_item_name = truncated_item_name.rsplit(" ", 1)[0] + name = prefix + "-" + truncated_item_name + "-" + suffix + if frappe.db.exists("BOM", name): conflicting_bom = frappe.get_doc("BOM", name) From 25df914bc62a1e8361b0894f516117419898b120 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 16 Feb 2022 12:35:34 +0530 Subject: [PATCH 093/136] test: bom for item_code that is >VARCHAR_LEN (cherry picked from commit e2c99e02a95a87021786a0666e97e174a3f65a44) --- erpnext/manufacturing/doctype/bom/test_bom.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index bfafacdfb57..08ac50fa6ee 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -385,6 +385,15 @@ class TestBOM(ERPNextTestCase): self.assertEqual(bom.transfer_material_against, "Work Order") bom.delete() + def test_bom_name_length(self): + """ test >140 char names""" + bom_tree = { + "x" * 140 : { + " ".join(["abc"] * 35): {} + } + } + create_nested_bom(bom_tree, prefix="") + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From 6a031a87da9f8c241e97b2831f58699912509008 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 16 Feb 2022 15:55:25 +0530 Subject: [PATCH 094/136] fix: improve bom autoname logic (cherry picked from commit 7f2670941ca7f2e41a37a8ea3ed186a0fa04b57c) --- erpnext/manufacturing/doctype/bom/bom.py | 38 ++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e5c046c493a..14562c2824b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import functools +import re from collections import deque from operator import itemgetter from typing import List @@ -103,27 +104,34 @@ class BOM(WebsiteGenerator): ) def autoname(self): - names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item) + existing_boms = frappe.get_all("BOM", filters={"item": self.item}) + if existing_boms: + existing_bom_names = [bom.name for bom in existing_boms] - if names: - # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1 + # split by "/" and "-" + delimiters = ["/", "-"] + pattern = "|".join(map(re.escape, delimiters)) + bom_parts = [re.split(pattern, bom_name) for bom_name in existing_bom_names] - # split by item - names = [name.split(self.item, 1) for name in names] - names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)] + # filter out BOMs that do not follow the following formats: + # - BOM/ITEM/001 + # - BOM/ITEM/001-1 + # - BOM-ITEM-001 + # - BOM-ITEM-001-1 + valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) - # split by (-) if cancelled - if names: - names = [cint(name.split('-')[-1]) for name in names] - idx = max(names) + 1 + # extract the current index from the BOM parts + if valid_bom_parts: + indexes = [cint(part[-1]) for part in valid_bom_parts] + index = max(indexes) + 1 else: - idx = 1 + index = 1 else: - idx = 1 + index = 1 prefix = self.doctype - suffix = "%.3i" % idx - bom_name = prefix + "-" + self.item + "-" + suffix + suffix = "%.3i" % index # convert index to string (1 -> "001") + bom_name = f"{prefix}-{self.item}-{suffix}" if len(bom_name) <= 140: name = bom_name @@ -134,7 +142,7 @@ class BOM(WebsiteGenerator): truncated_item_name = self.item[:truncated_length] # if a partial word is found after truncate, remove the extra characters truncated_item_name = truncated_item_name.rsplit(" ", 1)[0] - name = prefix + "-" + truncated_item_name + "-" + suffix + name = f"{prefix}-{truncated_item_name}-{suffix}" if frappe.db.exists("BOM", name): conflicting_bom = frappe.get_doc("BOM", name) From 1a2a03ed98d84561794a232f62fe28a9141d79bb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 26 Feb 2022 11:36:44 +0530 Subject: [PATCH 095/136] refactor: split versioning code for testability (cherry picked from commit 6b58d534030df956f9983c003741ad69263aa287) --- erpnext/manufacturing/doctype/bom/bom.py | 46 ++++++++++--------- erpnext/manufacturing/doctype/bom/test_bom.py | 21 +++++++++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 14562c2824b..2c34e83a5b9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -104,28 +104,9 @@ class BOM(WebsiteGenerator): ) def autoname(self): - existing_boms = frappe.get_all("BOM", filters={"item": self.item}) + existing_boms = frappe.get_all("BOM", filters={"item": self.item}, pluck="name") if existing_boms: - existing_bom_names = [bom.name for bom in existing_boms] - - # split by "/" and "-" - delimiters = ["/", "-"] - pattern = "|".join(map(re.escape, delimiters)) - bom_parts = [re.split(pattern, bom_name) for bom_name in existing_bom_names] - - # filter out BOMs that do not follow the following formats: - # - BOM/ITEM/001 - # - BOM/ITEM/001-1 - # - BOM-ITEM-001 - # - BOM-ITEM-001-1 - valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) - - # extract the current index from the BOM parts - if valid_bom_parts: - indexes = [cint(part[-1]) for part in valid_bom_parts] - index = max(indexes) + 1 - else: - index = 1 + index = self.get_next_version_index(existing_boms) else: index = 1 @@ -156,6 +137,29 @@ class BOM(WebsiteGenerator): self.name = name + @staticmethod + def get_next_version_index(existing_boms: List[str]) -> int: + # split by "/" and "-" + delimiters = ["/", "-"] + pattern = "|".join(map(re.escape, delimiters)) + bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms] + + # filter out BOMs that do not follow the following formats: + # - BOM/ITEM/001 + # - BOM/ITEM/001-1 + # - BOM-ITEM-001 + # - BOM-ITEM-001-1 + valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) + + # extract the current index from the BOM parts + if valid_bom_parts: + indexes = [cint(part[-1]) for part in valid_bom_parts] + index = max(indexes) + 1 + else: + index = 1 + + return index + def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 08ac50fa6ee..f43329121f3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -394,6 +394,27 @@ class TestBOM(ERPNextTestCase): } create_nested_bom(bom_tree, prefix="") + def test_version_index(self): + + bom = frappe.new_doc("BOM") + + version_index_test_cases = [ + (1, []), + (1, ["BOM#XYZ"]), + (2, ["BOM/ITEM/001"]), + (2, ["BOM/ITEM/001", "BOM/ITEM/001-1"]), + (2, ["BOM-ITEM-001",]), + (2, ["BOM-ITEM-001", "BOM-ITEM-001-1"]), + (3, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-001-1"]), + (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]), + (2, ["BOM-ITEM-001", "BOM-ITEM-001-1", "BOM-ITEM-001-2"]), + ] + + for expected_index, existing_boms in version_index_test_cases: + with self.subTest(): + self.assertEqual(expected_index, bom.get_next_version_index(existing_boms), + msg=f"Incorrect index for {existing_boms}") + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From c8ca92e4063249a04574a77f078aae1369e16b73 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 2 Mar 2022 15:25:06 +0530 Subject: [PATCH 096/136] fix: cancelled document check (cherry picked from commit 67d8a7ba86c3131b19b4077055524f69d334314e) --- erpnext/manufacturing/doctype/bom/bom.py | 15 +++++++++------ erpnext/manufacturing/doctype/bom/test_bom.py | 7 ++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 2c34e83a5b9..a6eef6049e3 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -104,7 +104,13 @@ class BOM(WebsiteGenerator): ) def autoname(self): - existing_boms = frappe.get_all("BOM", filters={"item": self.item}, pluck="name") + # ignore amended documents while calculating current index + existing_boms = frappe.get_all( + "BOM", + filters={"item": self.item, "amended_from": ["is", "not set"]}, + pluck="name" + ) + if existing_boms: index = self.get_next_version_index(existing_boms) else: @@ -144,15 +150,12 @@ class BOM(WebsiteGenerator): pattern = "|".join(map(re.escape, delimiters)) bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms] - # filter out BOMs that do not follow the following formats: - # - BOM/ITEM/001 - # - BOM/ITEM/001-1 - # - BOM-ITEM-001 - # - BOM-ITEM-001-1 + # filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001 valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) # extract the current index from the BOM parts if valid_bom_parts: + # handle cancelled and submitted documents indexes = [cint(part[-1]) for part in valid_bom_parts] index = max(indexes) + 1 else: diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index f43329121f3..ac52ba2ce95 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -402,12 +402,9 @@ class TestBOM(ERPNextTestCase): (1, []), (1, ["BOM#XYZ"]), (2, ["BOM/ITEM/001"]), - (2, ["BOM/ITEM/001", "BOM/ITEM/001-1"]), - (2, ["BOM-ITEM-001",]), - (2, ["BOM-ITEM-001", "BOM-ITEM-001-1"]), - (3, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-001-1"]), + (2, ["BOM-ITEM-001"]), + (3, ["BOM-ITEM-001", "BOM-ITEM-002"]), (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]), - (2, ["BOM-ITEM-001", "BOM-ITEM-001-1", "BOM-ITEM-001-2"]), ] for expected_index, existing_boms in version_index_test_cases: From fc257fa58dea8a50eef725dc167093336ab5b5d7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Mar 2022 16:48:53 +0530 Subject: [PATCH 097/136] test: actual bom naming test (cherry picked from commit 94d0f8d4e79aa87d66f73bc7056cf5faf9114588) --- erpnext/manufacturing/doctype/bom/test_bom.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index ac52ba2ce95..d0e79aa9a81 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -412,6 +412,42 @@ class TestBOM(ERPNextTestCase): self.assertEqual(expected_index, bom.get_next_version_index(existing_boms), msg=f"Incorrect index for {existing_boms}") + def test_bom_versioning(self): + bom_tree = { + frappe.generate_hash(length=10) : { + frappe.generate_hash(length=10): {} + } + } + bom = create_nested_bom(bom_tree, prefix="") + self.assertEqual(int(bom.name.split("-")[-1]), 1) + original_bom_name = bom.name + + bom.cancel() + bom.reload() + self.assertEqual(bom.name, original_bom_name) + + # create a new amendment + amendment = frappe.copy_doc(bom) + amendment.docstatus = 0 + amendment.amended_from = bom.name + + amendment.save() + amendment.submit() + amendment.reload() + + self.assertNotEqual(amendment.name, bom.name) + # `origname-001-1` version + self.assertEqual(int(amendment.name.split("-")[-1]), 1) + self.assertEqual(int(amendment.name.split("-")[-2]), 1) + + # create a new version + version = frappe.copy_doc(amendment) + version.docstatus = 0 + version.amended_from = None + version.save() + self.assertNotEqual(amendment.name, version.name) + self.assertEqual(int(version.name.split("-")[-1]), 2) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From d198c488a4071d13d0891d87f790562febd44b3e Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 14 Mar 2022 12:05:34 +0530 Subject: [PATCH 098/136] fix: max_qty validation condition --- erpnext/manufacturing/doctype/work_order/work_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 47fe3296cf1..0bfa7a286b5 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -635,12 +635,12 @@ class WorkOrder(Document): if self.production_plan and self.production_plan_item: qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1) - allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings", + allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0) max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0) - if max_qty < 1: + if not max_qty > 0: frappe.throw(_("Cannot produce more item for {0}") .format(self.production_item), OverProductionError) elif self.qty > max_qty: From 1cee08f3e9882eefbfcf26387230622e058a5fe2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Mar 2022 12:31:13 +0530 Subject: [PATCH 099/136] fix: cannot create purchase order from sales order (cherry picked from commit 58804b8436e782b520d39ec9e3633a724bf8e75f) --- erpnext/selling/doctype/sales_order/sales_order.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 69c85a32533..c15c917f828 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -693,12 +693,12 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( get_ordered_qty: function(item, so) { let ordered_qty = item.ordered_qty; - if (so.packed_items) { + if (so.packed_items && so.packed_items.length) { // calculate ordered qty based on packed items in case of product bundle let packed_items = so.packed_items.filter( (pi) => pi.parent_detail_docname == item.name ); - if (packed_items) { + if (packed_items && packed_items.length) { ordered_qty = packed_items.reduce( (sum, pi) => sum + flt(pi.ordered_qty), 0 From aadc2a8849b98cf1c5187cab36ffa95b57a6468b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 13 Mar 2022 13:16:45 +0530 Subject: [PATCH 100/136] test: negative fifo test (cherry picked from commit 91fd9d917aa7ef0f7ec23c956f7511094dd900ee) --- erpnext/stock/doctype/item/test_item.py | 5 +++- .../test_stock_ledger_entry.py | 29 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 68e545ba140..baaf51b47a0 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -29,7 +29,10 @@ from erpnext.tests.utils import ERPNextTestCase, change_settings test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] -def make_item(item_code, properties=None): +def make_item(item_code=None, properties=None): + if not item_code: + item_code = frappe.generate_hash(length=16) + if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 6d113ba4eb6..1ae2132dc2e 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -5,6 +5,7 @@ import json import frappe from frappe.core.page.permission_manager.permission_manager import reset +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today from erpnext.stock.doctype.delivery_note.test_delivery_note import ( @@ -22,10 +23,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.tests.utils import ERPNextTestCase -class TestStockLedgerEntry(ERPNextTestCase): +class TestStockLedgerEntry(FrappeTestCase): def setUp(self): items = create_items() reset('Stock Entry') @@ -443,6 +443,31 @@ class TestStockLedgerEntry(ERPNextTestCase): {"incoming_rate": sum(rates) * 10} ], sle_filters={"item_code": packed.name}) + @change_settings("Stock Settings", {"allow_negative_stock": 1}) + def test_negative_fifo_valuation(self): + """ + When stock goes negative discard FIFO queue. + Only pervailing valuation rate should be used for making transactions in such cases. + """ + item = make_item(properties={"allow_negative_stock": 1}).name + warehouse = "_Test Warehouse - _TC" + + receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10) + consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15) + + self.assertSLEs(consume1, [ + {"stock_value": -5 * 10, "stock_queue": [[-5, 10]]} + ]) + + consume2 = make_stock_entry(item_code=item, source=warehouse, qty=5) + self.assertSLEs(consume2, [ + {"stock_value": -10 * 10, "stock_queue": [[-10, 10]]} + ]) + + receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15) + self.assertSLEs(receipt2, [ + {"stock_queue": [[5, 15]], "stock_value_difference": 175} + ]) def create_repack_entry(**args): args = frappe._dict(args) From 7aca949a332c8759080cfc214fba1cacef19884a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 13 Mar 2022 19:39:39 +0530 Subject: [PATCH 101/136] fix(ux): skip items without batch series (cherry picked from commit 941ea1ec74af347ceb186352cc5d301a9af63e48) --- .../doctype/work_order/test_work_order.py | 26 +++++++++++++++++-- .../doctype/work_order/work_order.py | 8 ++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 975216d1bd9..28226290e2f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase, change_settings, timeout from frappe.utils import add_days, add_months, cint, flt, now, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError @@ -21,10 +22,9 @@ 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(ERPNextTestCase): +class TestWorkOrder(FrappeTestCase): def setUp(self): self.warehouse = '_Test Warehouse 2 - _TC' self.item = '_Test Item' @@ -937,6 +937,28 @@ class TestWorkOrder(ERPNextTestCase): frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") + @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1}) + def test_auto_batch_creation(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + fg_item = frappe.generate_hash(length=20) + child_item = frappe.generate_hash(length=20) + + bom_tree = {fg_item: {child_item: {}}} + + create_nested_bom(bom_tree, prefix="") + + item = frappe.get_doc("Item", fg_item) + item.has_batch_no = 1 + item.create_new_batch = 0 + item.save() + + try: + make_wo_order_test_record(item=fg_item) + except frappe.MandatoryError: + self.fail("Batch generation causing failing in Work Order") + + def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 47fe3296cf1..e2cbc3d3c2f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -333,6 +333,14 @@ class WorkOrder(Document): if not self.batch_size: self.batch_size = total_qty + batch_auto_creation = frappe.get_cached_value("Item", self.production_item, "create_new_batch") + if not batch_auto_creation: + frappe.msgprint( + _("Batch not created for item {} since it does not have a batch series.") + .format(frappe.bold(self.production_item)), + alert=True, indicator="orange") + return + while total_qty > 0: qty = self.batch_size if self.batch_size >= total_qty: From a1c9d1c67fd544a37acc134f7fd6cafd0da8d0ec Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 23 Aug 2021 11:05:07 +0530 Subject: [PATCH 102/136] fix: pos return payment mode issue (#26872) --- erpnext/controllers/taxes_and_totals.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index a642b8a083e..78914160e72 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -637,7 +637,8 @@ class calculate_taxes_and_totals(object): self.doc.precision("outstanding_amount")) if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): - self.update_paid_amount_for_return(total_amount_to_pay) + self.set_total_amount_to_default_mop(total_amount_to_pay) + self.calculate_paid_amount() def calculate_paid_amount(self): @@ -717,7 +718,7 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) - def update_paid_amount_for_return(self, total_amount_to_pay): + def set_total_amount_to_default_mop(self, total_amount_to_pay): default_mode_of_payment = frappe.db.get_value('POS Payment Method', {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) @@ -729,8 +730,6 @@ class calculate_taxes_and_totals(object): 'default': 1 }) - self.calculate_paid_amount() - def get_itemised_tax_breakup_html(doc): if not doc.taxes: return From 98a67967a38c4e5b7b1977c4257bd0b23c57eded Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 14 Mar 2022 14:40:00 +0530 Subject: [PATCH 103/136] fix: Search query of payroll entry reference in Journal Entry (#30225) --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 4ef29848bc6..0dc694f5a8b 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -671,7 +671,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte where reference_type="Payroll Entry") order by name limit %(start)s, %(page_len)s""" .format(key=searchfield), { - 'txt': "%%%s%%" % frappe.db.escape(txt), + 'txt': "%%%s%%" % txt, 'start': start, 'page_len': page_len }) From 468964a3f7f7a0e1c9c4c2b458c44072a8a2ef49 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 14 Mar 2022 14:43:15 +0530 Subject: [PATCH 104/136] ci: Revert fail on merge conflict label This reverts commit d32f229483cf4681a0873bfc9a4c5d97abecffca. --- .github/workflows/patch.yml | 6 ------ .github/workflows/server-tests.yml | 7 ------- 2 files changed, 13 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 54b381d7f89..30ca22aedc5 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -5,7 +5,6 @@ on: paths-ignore: - '**.js' - '**.md' - types: [opened, unlabeled, synchronize, reopened] workflow_dispatch: @@ -30,11 +29,6 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - - name: Check for merge conficts label - if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }} - run: | - echo "Remove merge conflicts and remove conflict label to run CI" - exit 1 - name: Clone uses: actions/checkout@v2 diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 1c9743c5700..c62622eecec 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -5,7 +5,6 @@ on: paths-ignore: - '**.js' - '**.md' - types: [opened, unlabeled, synchronize, reopened] workflow_dispatch: push: branches: [ develop ] @@ -40,12 +39,6 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - - name: Check for merge conficts label - if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }} - run: | - echo "Remove merge conflicts and remove conflict label to run CI" - exit 1 - - name: Clone uses: actions/checkout@v2 From 2e79c4fd598cdff8d2e1ad3dd0230b2b39577f2c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 15:17:46 +0530 Subject: [PATCH 105/136] fix(ux): remove get item buttons from submitted production plan (#30224) (cherry picked from commit 1af13ca4bfe99def6800c477ab79a01946ee8aa2) fix(patch): remove dead links to ProdPlan Item (cherry picked from commit d3e90ed8c81c347040a9e225ab829b3359afd621) Co-authored-by: Ankush Menat --- .../production_plan/production_plan.json | 10 +++-- erpnext/patches.txt | 1 + ...remove_unknown_links_to_prod_plan_items.py | 37 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 56cf2b4f08a..acc1b0017d0 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -189,7 +189,7 @@ "label": "Select Items to Manufacture" }, { - "depends_on": "get_items_from", + "depends_on": "eval:doc.get_items_from && doc.docstatus == 0", "fieldname": "get_items", "fieldtype": "Button", "label": "Get Finished Goods for Manufacture" @@ -197,6 +197,7 @@ { "fieldname": "po_items", "fieldtype": "Table", + "label": "Assembly Items", "no_copy": 1, "options": "Production Plan Item", "reqd": 1 @@ -350,6 +351,7 @@ "hide_border": 1 }, { + "depends_on": "get_items_from", "fieldname": "sub_assembly_items", "fieldtype": "Table", "label": "Sub Assembly Items", @@ -357,6 +359,7 @@ "options": "Production Plan Sub Assembly Item" }, { + "depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0", "fieldname": "get_sub_assembly_items", "fieldtype": "Button", "label": "Get Sub Assembly Items" @@ -376,7 +379,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-06 18:35:59.642232", + "modified": "2022-03-14 03:56:23.209247", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", @@ -397,5 +400,6 @@ } ], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d53df7e4f63..9370087da43 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,3 +352,4 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs +erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items diff --git a/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py new file mode 100644 index 00000000000..0284097e281 --- /dev/null +++ b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py @@ -0,0 +1,37 @@ +import frappe + + +def execute(): + """ + Remove "production_plan_item" field where linked field doesn't exist in tha table. + """ + frappe.reload_doc("manufacturing", "doctype", "production_plan_item") + + work_order = frappe.qb.DocType("Work Order") + pp_item = frappe.qb.DocType("Production Plan Item") + + broken_work_orders = ( + frappe.qb + .from_(work_order) + .left_join(pp_item).on(work_order.production_plan_item == pp_item.name) + .select(work_order.name) + .where( + (work_order.docstatus == 0) + & (work_order.production_plan_item.notnull()) + & (work_order.production_plan_item.like("new-production-plan%")) + & (pp_item.name.isnull()) + ) + ).run() + + if not broken_work_orders: + return + + broken_work_order_names = [d[0] for d in broken_work_orders] + + (frappe.qb + .update(work_order) + .set(work_order.production_plan_item, None) + .where(work_order.name.isin(broken_work_order_names)) + ).run() + + From 1fd96f379fabd8ff6a9a66134a27f6ea078f7140 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 28 Feb 2022 16:55:46 +0530 Subject: [PATCH 106/136] test(refactor): use FrappeTestCase (cherry picked from commit b0d1e6db54ea73af5a2b31d32cd5ba360d0dd7b1) --- .../test_opening_invoice_creation_tool.py | 4 +- .../shopping_cart/test_shopping_cart.py | 2 +- .../variant_selector/test_variant_selector.py | 6 +- .../blanket_order/test_blanket_order.py | 4 +- erpnext/manufacturing/doctype/bom/test_bom.py | 4 +- .../bom_update_tool/test_bom_update_tool.py | 4 +- .../doctype/job_card/test_job_card.py | 4 +- .../production_plan/test_production_plan.py | 4 +- .../doctype/routing/test_routing.py | 4 +- .../doctype/workstation/test_workstation.py | 4 +- .../selling/doctype/customer/test_customer.py | 5 +- .../test_party_specific_item.py | 6 +- .../doctype/quotation/test_quotation.py | 5 +- .../doctype/sales_order/test_sales_order.py | 4 +- ...st_payment_terms_status_for_sales_order.py | 4 +- ...t_pending_so_items_for_purchase_request.py | 4 +- .../report/sales_analytics/test_analytics.py | 4 +- erpnext/stock/doctype/batch/test_batch.py | 4 +- erpnext/stock/doctype/bin/test_bin.py | 4 +- .../delivery_note/test_delivery_note.py | 4 +- .../delivery_trip/test_delivery_trip.py | 5 +- erpnext/stock/doctype/item/test_item.py | 4 +- .../item_alternative/test_item_alternative.py | 4 +- .../item_attribute/test_item_attribute.py | 5 +- .../doctype/item_price/test_item_price.py | 4 +- .../test_landed_cost_voucher.py | 4 +- .../material_request/test_material_request.py | 4 +- .../doctype/packed_item/test_packed_item.py | 4 +- .../doctype/packing_slip/test_packing_slip.py | 2 +- .../stock/doctype/pick_list/test_pick_list.py | 5 +- .../purchase_receipt/test_purchase_receipt.py | 4 +- .../doctype/putaway_rule/test_putaway_rule.py | 4 +- .../test_quality_inspection.py | 4 +- .../stock/doctype/serial_no/test_serial_no.py | 5 +- .../stock/doctype/shipment/test_shipment.py | 4 +- .../doctype/stock_entry/test_stock_entry.py | 4 +- .../test_stock_reconciliation.py | 4 +- .../stock_settings/test_stock_settings.py | 5 +- .../stock/doctype/warehouse/test_warehouse.py | 4 +- .../report/stock_ageing/test_stock_ageing.py | 6 +- .../stock_analytics/test_stock_analytics.py | 5 +- erpnext/tests/utils.py | 77 ------------------- 42 files changed, 87 insertions(+), 164 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3eaf6a28f37..77d54a605e5 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( create_dimension, @@ -10,11 +11,10 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] -class TestOpeningInvoiceCreationTool(ERPNextTestCase): +class TestOpeningInvoiceCreationTool(FrappeTestCase): @classmethod def setUpClass(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index b15bac68234..51d37059570 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -17,7 +17,7 @@ from erpnext.e_commerce.shopping_cart.cart import ( request_for_quotation, update_cart, ) -from erpnext.tests.utils import change_settings, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address class TestShoppingCart(unittest.TestCase): diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index 967be838e67..45234b8dbe2 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -1,6 +1,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.controllers.item_variant import create_variant from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( @@ -9,11 +10,10 @@ from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings imp from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Item"] -class TestVariantSelector(ERPNextTestCase): +class TestVariantSelector(FrappeTestCase): @classmethod def setUpClass(cls): @@ -118,4 +118,4 @@ class TestVariantSelector(ERPNextTestCase): self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R") self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R") self.assertEqual(price_info["price_list_rate"], 100.0) - self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") \ No newline at end of file + self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index eff2344e85c..d4d337d8412 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -1,15 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, today from erpnext import get_company_currency -from erpnext.tests.utils import ERPNextTestCase from .blanket_order import make_order -class TestBlanketOrder(ERPNextTestCase): +class TestBlanketOrder(FrappeTestCase): def setUp(self): frappe.flags.args = frappe._dict() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index d0e79aa9a81..98314d9fade 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -7,6 +7,7 @@ from functools import partial import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -17,11 +18,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.tests.test_subcontracting import set_backflush_based_on -from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('BOM') -class TestBOM(ERPNextTestCase): +class TestBOM(FrappeTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 12576cbf322..b4c625d6108 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -2,15 +2,15 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item -from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('BOM') -class TestBOMUpdateTool(ERPNextTestCase): +class TestBOMUpdateTool(FrappeTestCase): def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index bb5004ba86f..33425d23142 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import random_string from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError @@ -11,10 +12,9 @@ from erpnext.manufacturing.doctype.job_card.job_card import ( from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestJobCard(ERPNextTestCase): +class TestJobCard(FrappeTestCase): def setUp(self): make_bom_for_jc_tests() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2359815813d..b2a41ff5b2a 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1,6 +1,7 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now_datetime, nowdate from erpnext.controllers.item_variant import create_variant @@ -16,10 +17,9 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.utils import ERPNextTestCase -class TestProductionPlan(ERPNextTestCase): +class TestProductionPlan(FrappeTestCase): def setUp(self): for item in ['Test Production Item 1', 'Subassembly Item 1', 'Raw Material Item 1', 'Raw Material Item 2']: diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 8bd60ea4aca..696d9bca144 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -2,14 +2,14 @@ # See license.txt import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestCase -class TestRouting(ERPNextTestCase): +class TestRouting(FrappeTestCase): @classmethod def setUpClass(cls): cls.item_code = "Test Routing Item - A" diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c298c0a8dbb..dd51017bb75 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -2,6 +2,7 @@ # See license.txt import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_bom @@ -10,13 +11,12 @@ from erpnext.manufacturing.doctype.workstation.workstation import ( WorkstationHolidayError, check_if_within_operating_hours, ) -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') make_test_records('Workstation') -class TestWorkstation(ERPNextTestCase): +class TestWorkstation(FrappeTestCase): def test_validate_timings(self): check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7802a3fea44..3da38a34522 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -4,12 +4,13 @@ import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding -from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] test_dependencies = ['Payment Term', 'Payment Terms Template'] @@ -18,7 +19,7 @@ test_records = frappe.get_test_records('Customer') from six import iteritems -class TestCustomer(ERPNextTestCase): +class TestCustomer(FrappeTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index b951044f332..9b672b4b5d3 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -1,12 +1,10 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.controllers.queries import item_query -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ['Item', 'Customer', 'Supplier'] @@ -18,7 +16,7 @@ def create_party_specific_item(**args): psi.based_on_value = args.get('based_on_value') psi.insert() -class TestPartySpecificItem(ERPNextTestCase): +class TestPartySpecificItem(FrappeTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") self.supplier = frappe.get_last_doc("Supplier") diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 4357201d23d..a749d9e1f1f 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_months, flt, getdate, nowdate -from erpnext.tests.utils import ERPNextTestCase - test_dependencies = ["Product Bundle"] -class TestQuotation(ERPNextTestCase): +class TestQuotation(FrappeTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) self.assertFalse(quotation.get('payment_schedule')) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index dd3a007c411..9d093b205e9 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,6 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate @@ -21,10 +22,9 @@ from erpnext.selling.doctype.sales_order.sales_order import ( ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestSalesOrder(ERPNextTestCase): +class TestSalesOrder(FrappeTestCase): @classmethod def setUpClass(cls): diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index cad41e1dc03..f7f8a5dbce3 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,6 +1,7 @@ import datetime import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice @@ -9,12 +10,11 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s execute, ) from erpnext.stock.doctype.item.test_item import create_item -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] -class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): +class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): def create_payment_terms_template(self): # create template for 50-50 payments template = None diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index d62915fc66d..16162acc8f3 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -2,6 +2,7 @@ # For license information, please see license.txt +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_material_request @@ -9,10 +10,9 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import ( execute, ) -from erpnext.tests.utils import ERPNextTestCase -class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase): +class TestPendingSOItemsForPurchaseRequest(FrappeTestCase): def test_result_for_partial_material_request(self): so = make_sales_order() mr=make_material_request(so.name) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index f56cce2dfdc..564f48fef3b 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -3,13 +3,13 @@ import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.sales_analytics.sales_analytics import execute -from erpnext.tests.utils import ERPNextTestCase -class TestAnalytics(ERPNextTestCase): +class TestAnalytics(FrappeTestCase): def test_sales_analytics(self): frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 0a663c2a188..8d7a2cf8d8c 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -3,15 +3,15 @@ import frappe from frappe.exceptions import ValidationError +from frappe.tests.utils import FrappeTestCase from frappe.utils import cint, flt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty from erpnext.stock.get_item_details import get_item_details -from erpnext.tests.utils import ERPNextTestCase -class TestBatch(ERPNextTestCase): +class TestBatch(FrappeTestCase): def test_item_has_batch_enabled(self): self.assertRaises(ValidationError, frappe.get_doc({ "doctype": "Batch", diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index 250126c6b98..ec0d8a88e3f 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -2,13 +2,13 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.utils import _create_bin -from erpnext.tests.utils import ERPNextTestCase -class TestBin(ERPNextTestCase): +class TestBin(FrappeTestCase): def test_concurrent_inserts(self): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4d26397f482..fc3dce1ee90 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,6 +6,7 @@ import json import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt, nowdate, nowtime from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -35,10 +36,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ) from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.tests.utils import ERPNextTestCase -class TestDeliveryNote(ERPNextTestCase): +class TestDeliveryNote(FrappeTestCase): def test_over_billing_against_dn(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index 321f48b2c59..dcdff4a0f1e 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, now_datetime, nowdate import erpnext @@ -12,10 +13,10 @@ from erpnext.stock.doctype.delivery_trip.delivery_trip import ( make_expense_claim, notify_customers, ) -from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address -class TestDeliveryTrip(ERPNextTestCase): +class TestDeliveryTrip(FrappeTestCase): def setUp(self): super().setUp() driver = create_driver() diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index baaf51b47a0..a50ced006cc 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,6 +6,7 @@ import json import frappe from frappe.test_runner import make_test_objects +from frappe.tests.utils import FrappeTestCase, change_settings from erpnext.controllers.item_variant import ( InvalidItemAttributeValueError, @@ -24,7 +25,6 @@ from erpnext.stock.doctype.item.item import ( ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_item_details -from erpnext.tests.utils import ERPNextTestCase, change_settings test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] @@ -55,7 +55,7 @@ def make_item(item_code=None, properties=None): return item -class TestItem(ERPNextTestCase): +class TestItem(FrappeTestCase): def setUp(self): super().setUp() frappe.flags.attribute_values = None diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 3976af4e88c..501c1c1ad3c 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -4,6 +4,7 @@ import json import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import ( @@ -18,10 +19,9 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.utils import ERPNextTestCase -class TestItemAlternative(ERPNextTestCase): +class TestItemAlternative(FrappeTestCase): def setUp(self): super().setUp() make_items() diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py index 0b7ca257151..055c22e0c5d 100644 --- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -6,11 +6,12 @@ import frappe test_records = frappe.get_test_records('Item Attribute') +from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError -from erpnext.tests.utils import ERPNextTestCase -class TestItemAttribute(ERPNextTestCase): +class TestItemAttribute(FrappeTestCase): def setUp(self): super().setUp() if frappe.db.exists("Item Attribute", "_Test_Length"): diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index f81770e487d..6ceba3f8d3f 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -4,13 +4,13 @@ import frappe from frappe.test_runner import make_test_records_for_doctype +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem from erpnext.stock.get_item_details import get_price_list_rate_for, process_args -from erpnext.tests.utils import ERPNextTestCase -class TestItemPrice(ERPNextTestCase): +class TestItemPrice(FrappeTestCase): def setUp(self): super().setUp() frappe.db.sql("delete from `tabItem Price`") diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 1ea0596d333..6dc4fee5697 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -4,6 +4,7 @@ import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -15,10 +16,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, make_purchase_receipt, ) -from erpnext.tests.utils import ERPNextTestCase -class TestLandedCostVoucher(ERPNextTestCase): +class TestLandedCostVoucher(FrappeTestCase): def test_landed_cost_voucher(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 705ef27b37a..866f3ab2d57 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -6,6 +6,7 @@ import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, today from erpnext.stock.doctype.item.test_item import create_item @@ -15,10 +16,9 @@ from erpnext.stock.doctype.material_request.material_request import ( make_supplier_quotation, raise_work_orders, ) -from erpnext.tests.utils import ERPNextTestCase -class TestMaterialRequest(ERPNextTestCase): +class TestMaterialRequest(FrappeTestCase): def test_make_purchase_order(self): mr = frappe.copy_doc(test_records[0]).insert() diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 2521ac9fe72..94268a8ef37 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,6 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_to_date, nowdate from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle @@ -9,10 +10,9 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestPackedItem(ERPNextTestCase): +class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index 5eb6b7399ae..bc405b20995 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -4,7 +4,7 @@ import unittest # test_records = frappe.get_test_records('Packing Slip') -from erpnext.tests.utils import ERPNextTestCase +from frappe.tests.utils import FrappeTestCase class TestPackingSlip(unittest.TestCase): diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 41e3150f0d7..f3b6b89784a 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -6,16 +6,17 @@ from frappe import _dict test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] +from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( EmptyStockReconciliationItemsError, ) -from erpnext.tests.utils import ERPNextTestCase -class TestPickList(ERPNextTestCase): +class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): try: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a737d873a96..c8a8fce7d63 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -7,6 +7,7 @@ import unittest from collections import defaultdict import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today from six import iteritems @@ -18,10 +19,9 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestPurchaseReceipt(ERPNextTestCase): +class TestPurchaseReceipt(FrappeTestCase): def setUp(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index ff1c19a8275..4e8d71fe5e4 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.item.test_item import make_item @@ -9,10 +10,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.get_item_details import get_conversion_factor -from erpnext.tests.utils import ERPNextTestCase -class TestPutawayRule(ERPNextTestCase): +class TestPutawayRule(FrappeTestCase): def setUp(self): if not frappe.db.exists("Item", "_Rice"): make_item("_Rice", { diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 308c62875d5..601ca054b53 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.controllers.stock_controller import ( @@ -13,12 +14,11 @@ from erpnext.controllers.stock_controller import ( from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase # test_records = frappe.get_test_records('Quality Inspection') -class TestQualityInspection(ERPNextTestCase): +class TestQualityInspection(FrappeTestCase): def setUp(self): super().setUp() create_item("_Test Item with QA") diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index f8cea717251..057a7d4c01f 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -18,11 +18,12 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse test_dependencies = ["Item"] test_records = frappe.get_test_records('Serial No') +from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.serial_no.serial_no import * -from erpnext.tests.utils import ERPNextTestCase -class TestSerialNo(ERPNextTestCase): +class TestSerialNo(FrappeTestCase): def tearDown(self): frappe.db.rollback() diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index afe821845ae..317abb6d03e 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -4,12 +4,12 @@ from datetime import date, timedelta import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment -from erpnext.tests.utils import ERPNextTestCase -class TestShipment(ERPNextTestCase): +class TestShipment(FrappeTestCase): def test_shipment_from_delivery_note(self): delivery_note = create_test_delivery_note() delivery_note.submit() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 3c34d4795cb..6a3b21d81c8 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.permissions import add_user_permission, remove_user_permission +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import flt, nowdate, nowtime from six import iteritems @@ -29,7 +30,6 @@ 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 ERPNextTestCase, change_settings def get_sle(**args): @@ -43,7 +43,7 @@ def get_sle(**args): order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, values, as_dict=1) -class TestStockEntry(ERPNextTestCase): +class TestStockEntry(FrappeTestCase): def tearDown(self): frappe.db.rollback() frappe.set_user("Administrator") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 86af0a0cf3b..d3e63713847 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,6 +6,7 @@ import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance @@ -19,10 +20,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestStockReconciliation(ERPNextTestCase): +class TestStockReconciliation(FrappeTestCase): @classmethod def setUpClass(cls): create_batch_or_serial_no_items() diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py index 072b54b8205..13496718ead 100644 --- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py @@ -4,11 +4,10 @@ import unittest import frappe - -from erpnext.tests.utils import ERPNextTestCase +from frappe.tests.utils import FrappeTestCase -class TestStockSettings(ERPNextTestCase): +class TestStockSettings(FrappeTestCase): def setUp(self): super().setUp() frappe.db.set_value("Stock Settings", None, "clean_description_html", 0) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 26db2642e4b..cdb771935b0 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -3,17 +3,17 @@ import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import cint import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('Warehouse') -class TestWarehouse(ERPNextTestCase): +class TestWarehouse(FrappeTestCase): def setUp(self): super().setUp() if not frappe.get_value('Item', '_Test Item'): diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 562d178c329..ca963b74863 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -2,12 +2,12 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data -from erpnext.tests.utils import ERPNextTestCase -class TestStockAgeing(ERPNextTestCase): +class TestStockAgeing(FrappeTestCase): def setUp(self) -> None: self.filters = frappe._dict( company="_Test Company", @@ -610,4 +610,4 @@ def generate_item_and_item_wh_wise_slots(filters, sle): item_wh_wise_slots = FIFOSlots(filters, sle).generate() filters.show_warehouse_wise_stock = False - return item_wise_slots, item_wh_wise_slots \ No newline at end of file + return item_wise_slots, item_wh_wise_slots diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py index 32df5859375..f6c98f914d2 100644 --- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py @@ -1,14 +1,13 @@ import datetime -import unittest from frappe import _dict +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges -from erpnext.tests.utils import ERPNextTestCase -class TestStockAnalyticsReport(ERPNextTestCase): +class TestStockAnalyticsReport(FrappeTestCase): def test_get_period_date_ranges(self): filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 1568b142022..0a9a5f84e11 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -1,10 +1,6 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # 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 import frappe @@ -13,22 +9,6 @@ from frappe.core.doctype.report.report import get_report_module_dotted_path ReportFilters = Dict[str, Any] ReportName = NewType("ReportName", str) - -class ERPNextTestCase(unittest.TestCase): - """A sane default test class for ERPNext tests.""" - - - @classmethod - def setUpClass(cls) -> None: - frappe.db.commit() - return super().setUpClass() - - @classmethod - def tearDownClass(cls) -> None: - frappe.db.rollback() - return super().tearDownClass() - - def create_test_contact_and_address(): frappe.db.sql('delete from tabContact') frappe.db.sql('delete from `tabContact Email`') @@ -81,43 +61,6 @@ def create_test_contact_and_address(): contact_two.insert() -@contextmanager -def change_settings(doctype, settings_dict): - """ A context manager to ensure that settings are changed before running - function and restored after running it regardless of exceptions occured. - This is useful in tests where you want to make changes in a function but - don't retain those changes. - import and use as decorator to cover full function or using `with` statement. - - example: - @change_settings("Stock Settings", {"item_naming_by": "Naming Series"}) - def test_case(self): - ... - """ - - try: - settings = frappe.get_doc(doctype) - # remember setting - previous_settings = copy.deepcopy(settings_dict) - for key in previous_settings: - previous_settings[key] = getattr(settings, key) - - # change setting - for key, value in settings_dict.items(): - setattr(settings, key, value) - settings.save() - # singles are cached by default, clear to avoid flake - frappe.db.value_cache[settings] = {} - yield # yield control to calling function - - finally: - # restore settings - settings = frappe.get_doc(doctype) - for key, value in previous_settings.items(): - setattr(settings, key, value) - settings.save() - - def execute_script_report( report_name: ReportName, module: str, @@ -152,23 +95,3 @@ 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 b22bdc5ff762d41242c742bb3f6fda64eb98fd2e Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 14 Mar 2022 16:28:23 +0530 Subject: [PATCH 107/136] test: add test for planned_qty --- .../doctype/production_plan/test_production_plan.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2359815813d..f8067052565 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -605,6 +605,17 @@ class TestProductionPlan(ERPNextTestCase): ] self.assertFalse(pp.all_items_completed()) + def test_production_plan_planned_qty(self): + pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55) + pln.make_work_order() + work_order = frappe.db.get_value('Work Order', {'production_plan': pln.name}, 'name') + wo_doc = frappe.get_doc('Work Order', work_order) + wo_doc.update({ + 'wip_warehouse': 'Work In Progress - _TC', + 'fg_warehouse': 'Finished Goods - _TC' + }) + wo_doc.submit() + self.assertEqual(wo_doc.qty, 0.55) def create_production_plan(**args): """ From 017624c270465a049afb5dd0c7b749ab9e33fdfc Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Mar 2022 15:57:24 +0530 Subject: [PATCH 108/136] fix: Add missing currency option in Supplier Quotation's `rounded_total` field (cherry picked from commit dcd88ddc877f3e11afa3129cd6fa81c78f06b36d) --- .../buying/doctype/supplier_quotation/supplier_quotation.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 023c95d697d..933949f5163 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -635,6 +635,7 @@ "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", + "options": "currency", "read_only": 1 }, { @@ -810,7 +811,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-11 06:43:20.924080", + "modified": "2022-03-14 15:53:02.550969", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", @@ -875,6 +876,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title" } \ No newline at end of file From d5b142e5caaf5733864f641dd6006b7ebdf8b258 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Mar 2022 16:15:12 +0530 Subject: [PATCH 109/136] chore: Re-arrange fields for consistency with base currency LHS (cherry picked from commit a579a211fd2ad10ce08157c447d0e81988a68dce) --- .../buying/doctype/supplier_quotation/supplier_quotation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 933949f5163..567e41fb61f 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -72,8 +72,8 @@ "section_break_46", "base_grand_total", "base_rounding_adjustment", - "base_in_words", "base_rounded_total", + "base_in_words", "column_break4", "grand_total", "rounding_adjustment", @@ -811,7 +811,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-14 15:53:02.550969", + "modified": "2022-03-14 16:13:20.284572", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", From 4768f4f2788c7590af4c7e59cb677ff3c1551d67 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 14 Mar 2022 16:29:00 +0530 Subject: [PATCH 110/136] fix(ux): negative stock warning (cherry picked from commit 02d64a32c231715e251363d0ec2c6bd52b559c1c) --- .../doctype/stock_settings/stock_settings.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 6167becdaac..66da215dbbe 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -13,6 +13,25 @@ frappe.ui.form.on('Stock Settings', { frm.set_query("default_warehouse", filters); frm.set_query("sample_retention_warehouse", filters); + }, + allow_negative_stock: function(frm) { + if (!frm.doc.allow_negative_stock) { + return; + } + + let msg = __("Using negative stock disables FIFO/Moving average valuation when inventory is negative."); + msg += " "; + msg += __("This is considered dangerous from accounting point of view.") + msg += "
"; + msg += ("Do you still want to enable negative inventory?"); + + frappe.confirm( + msg, + () => {}, + () => { + frm.set_value("allow_negative_stock", 0); + } + ); } }); From f23810749ddde39a3657ebe828ae5f1bd58c0d53 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 13 Mar 2022 18:13:12 +0530 Subject: [PATCH 111/136] fix: Do not consider cancelled entries (cherry picked from commit 6308e1be91fef76023d17c232519d51069ce31a1) --- .../report/trial_balance_for_party/trial_balance_for_party.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index d843dfd3ce3..70effce5684 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -107,6 +107,7 @@ def get_opening_balances(filters): select party, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` where company=%(company)s + and is_cancelled=0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') {account_filter} @@ -133,6 +134,7 @@ def get_balances_within_period(filters): select party, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where company=%(company)s + and is_cancelled = 0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and posting_date >= %(from_date)s and posting_date <= %(to_date)s and ifnull(is_opening, 'No') = 'No' From 34d6031601fb12f7f03df5f81386114c362b39b5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:30:41 +0530 Subject: [PATCH 112/136] fix(pos): do not reset mode of payments in case of consolidation (backport #30198) (#30218) --- .../test_pos_invoice_merge_log.py | 12 ++++++++++-- erpnext/controllers/taxes_and_totals.py | 7 ++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 89f7f18b42c..8909da96fcf 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -83,7 +83,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv_cn = make_sales_return(pos_inv.name) pos_inv_cn.set("payments", []) pos_inv_cn.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -100 + }) + pos_inv_cn.append('payments', { + 'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': -200 }) pos_inv_cn.paid_amount = -300 pos_inv_cn.submit() @@ -98,7 +101,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv_cn.load_from_db() self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) - self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) + consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice) + self.assertEqual(consolidated_credit_note.is_return, 1) + self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, 'Cash') + self.assertEqual(consolidated_credit_note.payments[0].amount, -100) + self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, 'Bank Draft') + self.assertEqual(consolidated_credit_note.payments[1].amount, -200) finally: frappe.set_user("Administrator") diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 78914160e72..40833b9300f 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -636,7 +636,12 @@ class calculate_taxes_and_totals(object): self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), self.doc.precision("outstanding_amount")) - if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): + if ( + self.doc.doctype == 'Sales Invoice' + and self.doc.get('is_pos') + and self.doc.get('is_return') + and not self.doc.get('is_consolidated') + ): self.set_total_amount_to_default_mop(total_amount_to_pay) self.calculate_paid_amount() From 767a02cd15c3feef952de026154d0b262cecf685 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:33:42 +0530 Subject: [PATCH 113/136] fix: cannot create multicurrency sales order with product bundles (#30222) --- erpnext/stock/doctype/packed_item/packed_item.json | 3 ++- erpnext/stock/doctype/packed_item/packed_item.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index d6e2e9ce2d7..e94c34d7adc 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -223,6 +223,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", + "options": "currency", "print_hide": 1, "read_only": 1 }, @@ -239,7 +240,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-22 12:57:45.325488", + "modified": "2022-03-10 15:42:00.265915", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 07c2f1f0dd3..f9c00c59bac 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -185,7 +185,8 @@ def update_packed_item_price_data(pi_row, item_data, doc): row_data.update({ "company": doc.get("company"), "price_list": doc.get("selling_price_list"), - "currency": doc.get("currency") + "currency": doc.get("currency"), + "conversion_rate": doc.get("conversion_rate"), }) rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") From d8de8caa1e284480deee729486b5e74143d5fd9a Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Mon, 14 Mar 2022 17:46:28 +0530 Subject: [PATCH 114/136] fix: BOM - clear Quality Inspection Template according to Inspection Quality Required Squashed commit of the following: commit b73fa210b234d2c8067db2c32f94f362b89afe5a Author: Florian HENRY Date: Mon Mar 14 08:30:24 2022 +0100 add json tes commit 984d015a7d9aceb6ea64be7ed9f1cc0caa356714 Author: Florian HENRY Date: Mon Mar 14 08:30:06 2022 +0100 better test commit 42061146658598da02eda7cb781bd2cf44c8ca34 Author: Florian HENRY Date: Fri Mar 11 16:12:57 2022 +0100 update test commit 6259c0957566600b044fcd4a5e14e69cdff71020 Author: Florian HENRY Date: Fri Mar 11 13:59:13 2022 +0100 update test commit cbc4ad9566f6f7f01dd622651458fea309d80954 Merge: faa44b3fdd 94d0f8d4e7 Author: Florian HENRY Date: Fri Mar 11 12:46:48 2022 +0100 Merge branch 'develop' of https://github.com/frappe/erpnext into dev_fix_30190 commit faa44b3fdd983adfc04bbf04a7ebfef114501bb3 Author: Florian HENRY Date: Fri Mar 11 12:03:35 2022 +0100 fix: clear Quality Inspection Template according to Inspection Quality Required --- erpnext/manufacturing/doctype/bom/bom.py | 5 +++++ erpnext/manufacturing/doctype/bom/test_bom.py | 19 +++++++++++++++++++ .../test_records.json | 12 ++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 erpnext/stock/doctype/quality_inspection_template/test_records.json diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a6eef6049e3..e9bb345c574 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -170,6 +170,7 @@ class BOM(WebsiteGenerator): frappe.throw(_("Please select a Company first."), title=_("Mandatory")) self.clear_operations() + self.clear_inspection() self.validate_main_item() self.validate_currency() self.set_conversion_rate() @@ -416,6 +417,10 @@ class BOM(WebsiteGenerator): if not self.with_operations: self.set('operations', []) + def clear_inspection(self): + if not self.inspection_required: + self.set('quality_inspection_template', None) + def validate_main_item(self): """ Validate main FG item""" item = self.get_item_det(self.item) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 98314d9fade..ad893a43ec3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -26,6 +26,9 @@ class TestBOM(FrappeTestCase): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') + if not frappe.get_value('Quality Inspection Template', '_Test Quality Inspection Template'): + make_test_records('Quality Inspection_Template') + def test_get_items(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict items_dict = get_bom_items_as_dict(bom=get_default_bom(), @@ -448,6 +451,22 @@ class TestBOM(FrappeTestCase): self.assertNotEqual(amendment.name, version.name) self.assertEqual(int(version.name.split("-")[-1]), 2) + def test_clear_inpection_quality(self): + + bom = frappe.copy_doc(test_records[2]) + bom.is_active = 0 + bom.quality_inspection_template = "_Test Quality Inspection Template" + bom.inspection_required = 1 + bom.save() + + self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template') + + bom.inspection_required = 0 + bom.save() + bom.load_from_db() + + self.assertEqual(bom.quality_inspection_template, None) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json new file mode 100644 index 00000000000..a1b6415b54e --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json @@ -0,0 +1,12 @@ +[ + { + "quality_inspection_template_name" : "_Test Quality Inspection Template", + "item_quality_inspection_parameter" : [ + { + "specification": "_Test Param", + "doctype": "Item Quality Inspection Parameter", + "parentfield": "item_quality_inspection_parameter" + } + ] + } +] From 1dbb6545001e0c20d3dd2e20b78acfe44694946e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 14 Mar 2022 17:43:28 +0530 Subject: [PATCH 115/136] test: refactor BOM quality template test --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/manufacturing/doctype/bom/test_bom.py | 17 ++++++----------- .../test_records.json | 1 + 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e9bb345c574..797115abb27 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -419,7 +419,7 @@ class BOM(WebsiteGenerator): def clear_inspection(self): if not self.inspection_required: - self.set('quality_inspection_template', None) + self.quality_inspection_template = None def validate_main_item(self): """ Validate main FG item""" diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index ad893a43ec3..e9fb4862a05 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -6,7 +6,6 @@ from collections import deque from functools import partial import frappe -from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt @@ -20,15 +19,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.tests.test_subcontracting import set_backflush_based_on test_records = frappe.get_test_records('BOM') +test_dependencies = ["Item", "Quality Inspection Template"] class TestBOM(FrappeTestCase): - def setUp(self): - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') - - if not frappe.get_value('Quality Inspection Template', '_Test Quality Inspection Template'): - make_test_records('Quality Inspection_Template') - def test_get_items(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict items_dict = get_bom_items_as_dict(bom=get_default_bom(), @@ -453,17 +446,19 @@ class TestBOM(FrappeTestCase): def test_clear_inpection_quality(self): - bom = frappe.copy_doc(test_records[2]) - bom.is_active = 0 + bom = frappe.copy_doc(test_records[2], ignore_no_copy=True) + bom.docstatus = 0 + bom.is_default = 0 bom.quality_inspection_template = "_Test Quality Inspection Template" bom.inspection_required = 1 bom.save() + bom.reload() self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template') bom.inspection_required = 0 bom.save() - bom.load_from_db() + bom.reload() self.assertEqual(bom.quality_inspection_template, None) diff --git a/erpnext/stock/doctype/quality_inspection_template/test_records.json b/erpnext/stock/doctype/quality_inspection_template/test_records.json index a1b6415b54e..980f49a80aa 100644 --- a/erpnext/stock/doctype/quality_inspection_template/test_records.json +++ b/erpnext/stock/doctype/quality_inspection_template/test_records.json @@ -1,6 +1,7 @@ [ { "quality_inspection_template_name" : "_Test Quality Inspection Template", + "doctype": "Quality Inspection Template", "item_quality_inspection_parameter" : [ { "specification": "_Test Param", From fa32fc3c832196004154055ac1b2334d6202c261 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 13 Mar 2022 14:45:19 +0530 Subject: [PATCH 116/136] fix: show status in job card list view in Draft mode --- erpnext/manufacturing/doctype/job_card/job_card_list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 8017209e7de..7f60bdc6d92 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,4 +1,5 @@ frappe.listview_settings['Job Card'] = { + has_indicator_for_draft: true, get_indicator: function(doc) { if (doc.status === "Work In Progress") { return [__("Work In Progress"), "orange", "status,=,Work In Progress"]; From 7b8723445ee695e88590bb833f60a0c4731106fa Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Mon, 14 Mar 2022 19:34:18 +0530 Subject: [PATCH 117/136] fix: job card - sub operations table status misbehaviour on pause / resume Job Card --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3c406156ebd..fae09b320a4 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -48,7 +48,7 @@ class JobCard(Document): self.validate_work_order() def set_sub_operations(self): - if self.operation: + if not self.sub_operations and self.operation: self.sub_operations = [] for row in frappe.get_all('Sub Operation', filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): From b0e178a29d00d1213246cf8d19a40e538a768487 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 13 Mar 2022 17:48:37 +0530 Subject: [PATCH 118/136] fix: Do not update ignore prcing rule check implicitly (cherry picked from commit 96b5cedcf8fdf4242daffd95ca3f40fea3a02199) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 1 - erpnext/selling/doctype/sales_order/sales_order.py | 1 - erpnext/stock/doctype/delivery_note/delivery_note.py | 1 - erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 - 4 files changed, 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2e7d3063ccb..2c6654285ff 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -402,7 +402,6 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 57c67424f7d..603b64d22fa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -566,7 +566,6 @@ def make_project(source_name, target_doc=None): @frappe.whitelist() def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index e7ab101d5a8..dc6fd84e1a4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -445,7 +445,6 @@ def make_sales_invoice(source_name, target_doc=None): invoiced_qty_map = get_invoiced_qty_map(source_name) def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index db2656e8c59..32ffba392a9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -724,7 +724,6 @@ def make_purchase_invoice(source_name, target_doc=None): frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) - doc.ignore_pricing_rule = 1 doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) doc.run_method("onload") doc.run_method("set_missing_values") From 8b077106f9e69e4e6fb0084a68f805fbf3626fef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Mar 2022 16:13:35 +0530 Subject: [PATCH 119/136] fix: KSA E-Invoice QR Code showing wrong VAT amount (cherry picked from commit b37559c535ce5cc4589a08a1af95311b360ce810) --- erpnext/regional/saudi_arabia/utils.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 674ea83cc65..80b7264adce 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -93,7 +93,7 @@ def create_qr_code(doc, method=None): tlv_array.append(''.join([tag, length, value])) # VAT Amount - vat_amount = str(doc.total_taxes_and_charges) + vat_amount = str(get_vat_amount(doc)) tag = bytes([5]).hex() length = bytes([len(vat_amount)]).hex() @@ -130,6 +130,22 @@ def create_qr_code(doc, method=None): doc.db_set('ksa_einv_qr', _file.file_url) doc.notify_update() +def get_vat_amount(doc): + vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company}) + vat_accounts = [] + vat_amount = 0 + + if vat_settings: + vat_settings_doc = frappe.get_doc('KSA VAT Setting', vat_settings) + + for row in vat_settings_doc.get('ksa_vat_sales_accounts'): + vat_accounts.append(row.account) + + for tax in doc.get('taxes'): + if tax.account_head in vat_accounts: + vat_amount += tax.tax_amount + + return vat_amount def delete_qr_code_file(doc, method=None): region = get_region(doc.company) From fc298770e9959a77e1c0a32d2df73cb0d4d79918 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Mar 2022 16:27:04 +0530 Subject: [PATCH 120/136] fix: Linting Issue (cherry picked from commit 3cc2e53b0822a221a76b900406eaff4b160da532) --- 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 80b7264adce..2859bbc3c96 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -133,7 +133,7 @@ def create_qr_code(doc, method=None): def get_vat_amount(doc): vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company}) vat_accounts = [] - vat_amount = 0 + vat_amount = 0 if vat_settings: vat_settings_doc = frappe.get_doc('KSA VAT Setting', vat_settings) From c5de90320fc1ea8326e080822c2db0ccf90afee3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Mar 2022 17:17:01 +0530 Subject: [PATCH 121/136] fix: Itemised tax rate updation (cherry picked from commit be56efad262cc8233d7bc462aa08d7f02e35ddcb) --- erpnext/regional/united_arab_emirates/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 891e75e0033..c18af93b2c8 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -28,9 +28,12 @@ def update_itemised_tax_data(doc): elif row.item_code and itemised_tax.get(row.item_code): tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) - row.tax_rate = flt(tax_rate, row.precision("tax_rate")) - row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) - row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + meta = frappe.get_meta(row.doctype) + + if meta.has_field('tax_rate'): + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) + row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) + row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) def get_account_currency(account): """Helper function to get account currency.""" From ec92551f7c940282d7663cb139ea0da71d5eb076 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:35:49 +0530 Subject: [PATCH 122/136] Update erpnext/regional/saudi_arabia/utils.py Co-authored-by: Saqib Ansari (cherry picked from commit e33d4713cda199f4d74b0d2fdb540dd77b81a381) --- 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 2859bbc3c96..60953ca6d83 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -136,7 +136,7 @@ def get_vat_amount(doc): vat_amount = 0 if vat_settings: - vat_settings_doc = frappe.get_doc('KSA VAT Setting', vat_settings) + vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings) for row in vat_settings_doc.get('ksa_vat_sales_accounts'): vat_accounts.append(row.account) From 0133b2a145f251e351cdf657eefa10aae9533020 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 11:41:38 +0530 Subject: [PATCH 123/136] fix: salary slip amount rounding errors (backport #30248) (#30251) Co-authored-by: Rucha Mahabal --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 6238f9c10e1..ade6cc77636 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -999,7 +999,7 @@ class SalarySlip(TransactionBase): # apply rounding if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"): - amount, additional_amount = rounded(amount), rounded(additional_amount) + amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0) return amount, additional_amount @@ -1276,9 +1276,9 @@ class SalarySlip(TransactionBase): def set_base_totals(self): self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate) self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate) - self.rounded_total = rounded(self.net_pay) + self.rounded_total = rounded(self.net_pay or 0) self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate) - self.base_rounded_total = rounded(self.base_net_pay) + self.base_rounded_total = rounded(self.base_net_pay or 0) self.set_net_total_in_words() #calculate total working hours, earnings based on hourly wages and totals From 6ef08545b6f88ab0bedcf9ce63c1040297d0a3b1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 12:24:08 +0530 Subject: [PATCH 124/136] fix: Leave Policy Assignment creation patch (backport #30215) (#30252) Co-authored-by: Rucha Mahabal --- ...t_based_on_employee_current_leave_policy.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py index 55125431b52..6f9031fc500 100644 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -6,14 +6,14 @@ import frappe def execute(): + frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment') + frappe.reload_doc('hr', 'doctype', 'employee_grade') + employee_with_assignment = [] + leave_policy = [] + if "leave_policy" in frappe.db.get_table_columns("Employee"): employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1) - employee_with_assignment = [] - leave_policy =[] - - #for employee - for employee in employees_with_leave_policy: alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) if not alloc: @@ -22,12 +22,10 @@ def execute(): employee_with_assignment.append(employee.name) leave_policy.append(employee.leave_policy) - - if "default_leave_policy" in frappe.db.get_table_columns("Employee"): + if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"): employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1) #for whole employee Grade - for grade in employee_grade_with_leave_policy: employees = get_employee_with_grade(grade.name) for employee in employees: @@ -47,13 +45,13 @@ def execute(): allocation_exists=True) def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): + if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2: + return filters = {"employee":employee, "leave_policy": leave_policy} if leave_period: filters["leave_period"] = leave_period - frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment') - if not frappe.db.exists("Leave Policy Assignment" , filters): lpa = frappe.new_doc("Leave Policy Assignment") lpa.employee = employee From 2c5e76d5426535c66e7ddd0087a39a432ac64ba3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:28:21 +0530 Subject: [PATCH 125/136] fix(pos): loyalty points in case of returned pos invoice (#30257) --- .../doctype/pos_invoice/pos_invoice.py | 4 ++-- .../doctype/sales_invoice/sales_invoice.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 9d585411582..466b2833fe1 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -54,7 +54,7 @@ class POSInvoice(SalesInvoice): def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program - if self.loyalty_program: + if not self.is_return and self.loyalty_program: self.make_loyalty_point_entry() elif self.is_return and self.return_against and self.loyalty_program: against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) @@ -88,7 +88,7 @@ class POSInvoice(SalesInvoice): def on_cancel(self): # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() - if self.loyalty_program: + if not self.is_return and self.loyalty_program: self.delete_loyalty_point_entry() elif self.is_return and self.return_against and self.loyalty_program: against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c0430e1b9de..a563646e90b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1422,12 +1422,19 @@ class SalesInvoice(SellingController): frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name) def get_returned_amount(self): - returned_amount = frappe.db.sql(""" - select sum(grand_total) - from `tabSales Invoice` - where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s - """, self.name) - return abs(flt(returned_amount[0][0])) if returned_amount else 0 + from frappe.query_builder.functions import Coalesce, Sum + doc = frappe.qb.DocType(self.doctype) + returned_amount = ( + frappe.qb.from_(doc) + .select(Sum(doc.grand_total)) + .where( + (doc.docstatus == 1) + & (doc.is_return == 1) + & (Coalesce(doc.return_against, '') == self.name) + ) + ).run() + + return abs(returned_amount[0][0]) if returned_amount[0][0] else 0 # redeem the loyalty points. def apply_loyalty_points(self): From a56fd20ab4d8c5e10787d8725f5a73a0758f466f Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Mar 2022 16:27:52 +0530 Subject: [PATCH 126/136] fix: Sub-Categpry Routing in Item Group Page Listing pills - Use absolute route even 3-4 sub-category levels down - Remove scroll from category pills due to accessibility issues - Arrange sub-category pills alphabetically (cherry picked from commit 8264d6b0bcef5d9c7900f652c4554f95572aa202) --- erpnext/e_commerce/product_ui/views.js | 2 +- erpnext/public/scss/shopping_cart.scss | 9 +++------ erpnext/setup/doctype/item_group/item_group.py | 3 ++- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js index 99b91afac17..cc51718c47f 100644 --- a/erpnext/e_commerce/product_ui/views.js +++ b/erpnext/e_commerce/product_ui/views.js @@ -501,7 +501,7 @@ erpnext.ProductView = class { categories.forEach(category => { sub_group_html += ` - +
${ category.name }
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index b743504a527..ff2482d3f64 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -569,15 +569,12 @@ body.product-page { } .scroll-categories { - white-space: nowrap; - overflow-x: auto; - .category-pill { - margin: 0px 4px; display: inline-block; - padding: 6px 12px; - background-color: #ecf5fe; width: fit-content; + padding: 6px 12px; + margin-bottom: 8px; + background-color: #ecf5fe; font-size: 14px; border-radius: 18px; color: var(--blue-500); diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 5bcd0e4e21e..91b2f4f974f 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -133,7 +133,8 @@ def get_child_groups_for_website(item_group_name, immediate=False, include_self= return frappe.get_all( "Item Group", filters=filters, - fields=["name", "route"] + fields=["name", "route"], + order_by="name" ) def get_child_item_groups(item_group_name): From 8663a776dd2b2a12e5d56b7becfad87e2ff47ba2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Mar 2022 20:14:46 +0530 Subject: [PATCH 127/136] fix: incorrect debit credit amount in presentation currency (cherry picked from commit 83a5fae5913bb05d949c3a3f5fb24bd6b1d19d95) --- erpnext/accounts/report/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 89cc0a8c8cc..fa8d69d7df8 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -93,10 +93,10 @@ def convert_to_presentation_currency(gl_entries, currency_info, company): account_currency = entry['account_currency'] if len(account_currencies) == 1 and account_currency == presentation_currency: - if entry.get('debit'): + if debit_in_account_currency: entry['debit'] = debit_in_account_currency - if entry.get('credit'): + if credit_in_account_currency: entry['credit'] = credit_in_account_currency else: date = currency_info['report_date'] From 510668f78aef647ed65482c412a93db16cd9e665 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 15 Mar 2022 17:56:42 +0530 Subject: [PATCH 128/136] test: foreign_account_balance_after_exchange_rate_revaluation (cherry picked from commit 12d99ed69a8a71cf92c38cd0e251965b2be9fe0a) --- .../general_ledger/test_general_ledger.py | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 erpnext/accounts/report/general_ledger/test_general_ledger.py diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py new file mode 100644 index 00000000000..2373c8c2a67 --- /dev/null +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -0,0 +1,134 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.general_ledger.general_ledger import execute + + +class TestGeneralLedger(FrappeTestCase): + + def test_foreign_account_balance_after_exchange_rate_revaluation(self): + """ + Checks the correctness of balance after exchange rate revaluation + """ + # create a new account with USD currency + account_name = "Test USD Account for Revalutation" + company = "_Test Company" + account = frappe.get_doc({ + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "inter_company_account": 0, + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + "doctype": "Account" + }) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set("accounts", [ + { + "account": account.name, + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ]) + jv.save() + jv.submit() + # create a JV to credit 900 USD at 100 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set("accounts", [ + { + "account": account.name, + "debit_in_account_currency": 0, + "credit_in_account_currency": 900, + "exchange_rate": 100, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 90000, + "credit_in_account_currency": 0, + "cost_center": "_Test Cost Center - _TC", + }, + ]) + jv.save() + jv.submit() + + # create an exchange rate revaluation entry at 77 exchange rate + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + revaluation.set("accounts", [ + { + "account": account.name, + "account_currency": "USD", + "new_exchange_rate": 77, + "new_balance_in_base_currency": 7700, + "balance_in_base_currency": -15000, + "balance_in_account_currency": 100, + "current_exchange_rate": -150 + } + ]) + revaluation.save() + revaluation.submit() + + # post journal entry to revaluate + frappe.db.set_value('Company', company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC") + revaluation_jv = revaluation.make_jv_entry() + revaluation_jv = frappe.get_doc(revaluation_jv) + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # check the balance of the account + balance = frappe.db.sql( + """ + select sum(debit_in_account_currency) - sum(credit_in_account_currency) + from `tabGL Entry` + where account = %s + group by account + """, account.name) + + self.assertEqual(balance[0][0], 100) + + # check if general ledger shows correct balance + columns, data = execute(frappe._dict({ + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + })) + + self.assertEqual(data[1]["account"], account.name) + self.assertEqual(data[1]["debit"], 1000) + self.assertEqual(data[1]["credit"], 0) + self.assertEqual(data[2]["debit"], 0) + self.assertEqual(data[2]["credit"], 900) + self.assertEqual(data[3]["debit"], 100) + self.assertEqual(data[3]["credit"], 100) \ No newline at end of file From 29fa902b33d1aea72e5efad8c2e77c64af121af5 Mon Sep 17 00:00:00 2001 From: Samuel Danieli <23150094+scdanieli@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:03:30 +0100 Subject: [PATCH 129/136] feat: add German translations (cherry picked from commit bbe5739547b221816adb05a0b24c397dd00fedfd) --- erpnext/translations/de.csv | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 6e70bc579e7..4f30eb250c2 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1538,7 +1538,7 @@ Marketing,Marketing, Marketing Expenses,Marketingkosten, Marketplace,Marktplatz, Marketplace Error,Marktplatzfehler, -Masters,Stämme, +Masters,Stammdaten, Match Payments with Invoices,Zahlungen und Rechnungen abgleichen, Match non-linked Invoices and Payments.,Nicht verknüpfte Rechnungen und Zahlungen verknüpfen, Material,Material, @@ -1617,7 +1617,7 @@ More Information,Mehr Informationen, More than one selection for {0} not allowed,Mehr als eine Auswahl für {0} ist nicht zulässig, More...,Mehr..., Motion Picture & Video,Film & Fernsehen, -Move,Bewegen, +Move,Verschieben, Move Item,Element verschieben, Multi Currency,Unterschiedliche Währungen, Multiple Item prices.,Mehrere verschiedene Artikelpreise, @@ -1939,7 +1939,7 @@ Pharmaceutical,Arzneimittel, Pharmaceuticals,Pharmaprodukte, Physician,Arzt, Piecework,Akkordarbeit, -Pincode,Postleitzahl (PLZ), +Pincode,Postleitzahl, Place Of Supply (State/UT),Ort der Lieferung (Staat / UT), Place Order,Bestellung aufgeben, Plan Name,Planname, @@ -2534,7 +2534,7 @@ Sales Orders,Kundenaufträge, Sales Partner,Vertriebspartner, Sales Pipeline,Vertriebspipeline, Sales Price List,Verkaufspreisliste, -Sales Return,Rücklieferung, +Sales Return,Retoure, Sales Summary,Verkaufszusammenfassung, Sales Tax Template,Umsatzsteuer-Vorlage, Sales Team,Verkaufsteam, @@ -9838,3 +9838,8 @@ Enable European Access,Ermöglichen Sie den europäischen Zugang, Creating Purchase Order ...,Bestellung anlegen ..., "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Wählen Sie einen Lieferanten aus den Standardlieferanten der folgenden Artikel aus. Bei der Auswahl erfolgt eine Bestellung nur für Artikel, die dem ausgewählten Lieferanten gehören.", Row #{}: You must select {} serial numbers for item {}.,Zeile # {}: Sie müssen {} Seriennummern für Artikel {} auswählen., +{} To Deliver,{} Zu liefern, +{} To Receive,{} Zu erhalten, +{} Available,{} Verfügbar, +Report an Issue,Ein Problem melden, +User Forum,Anwenderforum, From f27fd605f5edd4462869ba367768d16b8a491d88 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Mar 2022 11:55:43 +0530 Subject: [PATCH 130/136] fix: Do not update ignore prcing rule check implicitly (cherry picked from commit 1f79b47a1797762cd57972f6f3b28688d558a0cc) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 1 - .../buying/doctype/supplier_quotation/supplier_quotation.py | 1 - erpnext/selling/doctype/quotation/quotation.py | 3 +-- erpnext/selling/doctype/sales_order/sales_order.py | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a563646e90b..615ebc5218c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1714,7 +1714,6 @@ def make_maintenance_schedule(source_name, target_doc=None): @frappe.whitelist() def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index d65ab94a6d3..34a4f26a087 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -109,7 +109,6 @@ def get_list_context(context=None): @frappe.whitelist() def make_purchase_order(source_name, target_doc=None): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("get_schedule_dates") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index f191d9323ee..ed8d93e7962 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -151,7 +151,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): if source.referral_sales_partner: target.sales_partner=source.referral_sales_partner target.commission_rate=frappe.get_value('Sales Partner', source.referral_sales_partner, 'commission_rate') - target.ignore_pricing_rule = 1 target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -226,7 +225,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if customer: target.customer = customer.name target.customer_name = customer.customer_name - target.ignore_pricing_rule = 1 + target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 603b64d22fa..6f329e037fa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -641,7 +641,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.set_advances() def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.flags.ignore_permissions = True target.run_method("set_missing_values") target.run_method("set_po_nos") From cde3c685ee98ac7d450d0a22cb756eb63d7362b1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 15 Mar 2022 19:09:26 +0530 Subject: [PATCH 131/136] revert: "fix: updated packed_items getting fetched on Sales Return / Credit Note (#28607)" This reverts commit 20216fa9f1e98e612c36a89e497de3e92238db7b. (cherry picked from commit b781e8b7d18f41db986b3126b98aa484db4fc851) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 8 ++------ .../stock/doctype/delivery_note/delivery_note.py | 9 ++------- .../doctype/delivery_note/test_delivery_note.py | 12 ++---------- .../stock_ledger_entry/test_stock_ledger_entry.py | 8 +++----- erpnext/stock/utils.py | 13 ------------- 5 files changed, 9 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 615ebc5218c..f1e64c7d020 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -49,7 +49,6 @@ from erpnext.stock.doctype.serial_no.serial_no import ( get_serial_nos, update_serial_nos_after_submit, ) -from erpnext.stock.utils import calculate_mapped_packed_items_return form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -759,11 +758,8 @@ class SalesInvoice(SellingController): def update_packing_list(self): if cint(self.update_stock) == 1: - if cint(self.is_return) and self.return_against: - calculate_mapped_packed_items_return(self) - else: - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self) + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) else: self.set('packed_items', []) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index dc6fd84e1a4..b10a7015dec 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -17,7 +17,6 @@ from erpnext.stock.doctype.serial_no.serial_no import ( get_delivery_note_serial_no, update_serial_nos_after_submit, ) -from erpnext.stock.utils import calculate_mapped_packed_items_return form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -132,12 +131,8 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - # Keeps mapped packed_items in case product bundle is updated. - if self.is_return and self.return_against: - calculate_mapped_packed_items_return(self) - else: - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list - make_packing_list(self) + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) if self._action != 'submit' and not self.is_return: set_batch_nos(self, 'warehouse', throw=True) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index fc3dce1ee90..9dc112fda85 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -386,7 +386,8 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(actual_qty, 25) # return bundled item - dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2) + dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1, + return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") @@ -827,15 +828,6 @@ class TestDeliveryNote(FrappeTestCase): dn.reload() self.assertTrue(dn.items[0].serial_no) -def create_return_delivery_note(**args): - args = frappe._dict(args) - from erpnext.controllers.sales_and_purchase_return import make_return_doc - doc = make_return_doc("Delivery Note", args.source_name, None) - doc.items[0].rate = args.rate - doc.items[0].qty = args.qty - doc.submit() - return doc - def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 1ae2132dc2e..ad78fba347b 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -8,10 +8,7 @@ from frappe.core.page.permission_manager.permission_manager import reset from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today -from erpnext.stock.doctype.delivery_note.test_delivery_note import ( - create_delivery_note, - create_return_delivery_note, -) +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( create_landed_cost_voucher, @@ -258,7 +255,8 @@ class TestStockLedgerEntry(FrappeTestCase): self.assertEqual(outgoing_rate, 100) # Return Entry: Qty = -2, Rate = 150 - return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2) + return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150, + company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") # check incoming rate for Return entry incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index b8bdf39301e..b843d52b1bf 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -422,19 +422,6 @@ def is_reposting_item_valuation_in_progress(): if reposting_in_progress: frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) - -def calculate_mapped_packed_items_return(return_doc): - parent_items = set([item.parent_item for item in return_doc.packed_items]) - against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against) - - for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items): - if original_bundle.item_code in parent_items: - for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items): - if returned_packed_item.parent_item == original_bundle.item_code: - returned_packed_item.parent_detail_docname = returned_bundle.name - returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty - - def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: """Check if there are pending reposting job till the specified posting date.""" From f3af5dd3845d96d3cc09f6a52fc60d3343846e6e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 16 Mar 2022 12:55:35 +0530 Subject: [PATCH 132/136] test: packed item return scenarios (cherry picked from commit b46d6e3c058ad546e7c87740f8dcb094ed1e8269) --- .../doctype/packed_item/test_packed_item.py | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 94268a8ef37..5f1b9542d6a 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -17,15 +17,25 @@ class TestPackedItem(FrappeTestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() + cls.warehouse = "_Test Warehouse - _TC" cls.bundle = "_Test Product Bundle X" cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] + + cls.bundle2 = "_Test Product Bundle Y" + cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"] + make_item(cls.bundle, {"is_stock_item": 0}) - for item in cls.bundle_items: + make_item(cls.bundle2, {"is_stock_item": 0}) + for item in cls.bundle_items + cls.bundle2_items: make_item(item, {"is_stock_item": 1}) make_item("_Test Normal Stock Item", {"is_stock_item": 1}) make_product_bundle(cls.bundle, cls.bundle_items, qty=2) + make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2) + + for item in cls.bundle_items + cls.bundle2_items: + make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100) def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." @@ -156,3 +166,104 @@ class TestPackedItem(FrappeTestCase): credit_after_reposting = sum(gle.credit for gle in gles) self.assertNotEqual(credit_before_repost, credit_after_reposting) self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost) + + def assertReturns(self, original, returned): + self.assertEqual(len(original), len(returned)) + + sort_function = lambda p: (p.parent_item, p.item_code, p.qty) + + for sent, returned in zip( + sorted(original, key=sort_function), + sorted(returned, key=sort_function) + ): + self.assertEqual(sent.item_code, returned.item_code) + self.assertEqual(sent.parent_item, returned.parent_item) + self.assertEqual(sent.qty, -1 * returned.qty) + + def test_returning_full_bundles(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_list = [ + { + "item_code": self.bundle, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + }, + { + "item_code": self.bundle2, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + } + ] + so = make_sales_order(item_list=item_list, warehouse=self.warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + # create return + dn_ret = make_sales_return(dn.name) + dn_ret.save() + dn_ret.submit() + self.assertReturns(dn.packed_items, dn_ret.packed_items) + + def test_returning_partial_bundles(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_list = [ + { + "item_code": self.bundle, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + }, + { + "item_code": self.bundle2, + "warehouse": self.warehouse, + "qty": 1, + "rate": 100, + } + ] + so = make_sales_order(item_list=item_list, warehouse=self.warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + # create return + dn_ret = make_sales_return(dn.name) + # remove bundle 2 + dn_ret.items.pop() + + dn_ret.save() + dn_ret.submit() + dn_ret.reload() + + self.assertTrue(all(d.parent_item == self.bundle for d in dn_ret.packed_items)) + + expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle] + self.assertReturns(expected_returns, dn_ret.packed_items) + + + def test_returning_partial_bundle_qty(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty = 2) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + # create return + dn_ret = make_sales_return(dn.name) + # halve the qty + dn_ret.items[0].qty = -1 + dn_ret.save() + dn_ret.submit() + + expected_returns = dn.packed_items + for d in expected_returns: + d.qty /= 2 + self.assertReturns(expected_returns, dn_ret.packed_items) From 64b646050e7ead6cc90afa1b5bed347c93410cb2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 19:51:22 +0530 Subject: [PATCH 133/136] refactor: removed unrequired code and test for standalone delivery note serial return (#30276) (#30278) (cherry picked from commit 2a0ca7c91a8f788e765d7465b82e9f6238960d4a) Co-authored-by: Noah Jacob --- erpnext/stock/doctype/delivery_note/delivery_note.py | 8 +------- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 5 ----- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index b10a7015dec..93d2357cfa9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -13,10 +13,7 @@ from frappe.utils import cint, flt from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos -from erpnext.stock.doctype.serial_no.serial_no import ( - get_delivery_note_serial_no, - update_serial_nos_after_submit, -) +from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -218,9 +215,6 @@ class DeliveryNote(SellingController): # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() - if self.is_return: - update_serial_nos_after_submit(self, "items") - self.make_gl_entries() self.repost_future_sle_and_gle() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 9dc112fda85..82f4e7dd294 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -823,11 +823,6 @@ class TestDeliveryNote(FrappeTestCase): automatically_fetch_payment_terms(enable=0) - def test_standalone_serial_no_return(self): - dn = create_delivery_note(item_code="_Test Serialized Item With Series", is_return=True, qty=-1) - dn.reload() - self.assertTrue(dn.items[0].serial_no) - def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) From 0becc1e2313cae72e85553114b208ed17324325e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 23:01:38 +0530 Subject: [PATCH 134/136] refactor: call repost directly during tests (#30277) (#30279) enqueue(now=frappe.flags.in_test) is always true in test, this change avoids confusion. (cherry picked from commit 76187d175f67bb36e4a433b58b62373bb0ae3104) Co-authored-by: Ankush Menat --- .../repost_item_valuation.py | 16 +++++++++++++--- 1 file changed, 13 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 4b83e5e8532..0dd867f4156 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -45,11 +45,21 @@ class RepostItemValuation(Document): self.db_set('status', self.status) def on_submit(self): - if not frappe.flags.in_test or self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts: + """During tests reposts are executed immediately. + + Exceptions: + 1. "Repost Item Valuation" document has self.flags.dont_run_in_test + 2. global flag frappe.flags.dont_execute_stock_reposts is set + + These flags are useful for asserting real time behaviour like quantity updates. + """ + + if not frappe.flags.in_test: + return + if self.flags.dont_run_in_test or frappe.flags.dont_execute_stock_reposts: return - frappe.enqueue(repost, timeout=1800, queue='long', - job_name='repost_sle', now=frappe.flags.in_test, doc=self) + repost(self) @frappe.whitelist() def restart_reposting(self): From 15413cf8355b780c7597ffc04934f798e522263e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Mar 2022 12:52:02 +0530 Subject: [PATCH 135/136] fix: Non Profit fixes (#30280) --- .../non_profit/doctype/donation/donation.json | 20 +++++++++++-------- .../non_profit/doctype/donation/donation.py | 6 +++--- .../doctype/donation/test_donation.py | 4 ++-- erpnext/non_profit/doctype/member/member.py | 4 ++++ .../doctype/membership/membership.json | 19 ++++++++++++------ .../doctype/membership/membership.py | 2 +- erpnext/patches.txt | 1 + .../patches/v13_0/rename_non_profit_fields.py | 17 ++++++++++++++++ .../tax_exemption_80g_certificate.js | 2 +- .../tax_exemption_80g_certificate.json | 18 ++++++++--------- .../tax_exemption_80g_certificate.py | 14 ++----------- .../test_tax_exemption_80g_certificate.py | 6 +++--- .../tax_exemption_80g_certificate_detail.json | 16 ++++++++------- .../80g_certificate_for_donation.json | 4 ++-- 14 files changed, 79 insertions(+), 54 deletions(-) create mode 100644 erpnext/patches/v13_0/rename_non_profit_fields.py diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json index 6759569d54d..d2bcd17a20e 100644 --- a/erpnext/non_profit/doctype/donation/donation.json +++ b/erpnext/non_profit/doctype/donation/donation.json @@ -17,7 +17,8 @@ "paid", "amount", "mode_of_payment", - "razorpay_payment_id", + "column_break_12", + "payment_id", "amended_from" ], "fields": [ @@ -73,12 +74,6 @@ "label": "Mode of Payment", "options": "Mode of Payment" }, - { - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "Razorpay Payment ID", - "read_only": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -108,12 +103,21 @@ "options": "Donation", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "payment_id", + "fieldtype": "Data", + "label": "Payment ID" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-03-11 10:53:11.269005", + "modified": "2022-03-16 17:18:45.611741", "modified_by": "Administrator", "module": "Non Profit", "name": "Donation", diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 617979ef745..85f5a2652e9 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -102,7 +102,7 @@ def capture_razorpay_donations(*args, **kwargs): if not donor: donor = create_donor(payment) - donation = create_donation(donor, payment) + donation = create_razorpay_donation(donor, payment) donation.run_method('create_payment_entry') except Exception as e: @@ -114,7 +114,7 @@ def capture_razorpay_donations(*args, **kwargs): return { 'status': 'Success' } -def create_donation(donor, payment): +def create_razorpay_donation(donor, payment): if not frappe.db.exists('Mode of Payment', payment.method): create_mode_of_payment(payment.method) @@ -128,7 +128,7 @@ def create_donation(donor, payment): 'date': getdate(), 'amount': flt(payment.amount) / 100, # Convert to rupees from paise 'mode_of_payment': payment.method, - 'razorpay_payment_id': payment.id + 'payment_id': payment.id }).insert(ignore_mandatory=True) donation.submit() diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py index 5fa731a6aa3..4e39adbcf26 100644 --- a/erpnext/non_profit/doctype/donation/test_donation.py +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -5,7 +5,7 @@ import unittest import frappe -from erpnext.non_profit.doctype.donation.donation import create_donation +from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation class TestDonation(unittest.TestCase): @@ -30,7 +30,7 @@ class TestDonation(unittest.TestCase): 'method': 'Debit Card', 'id': 'pay_MeXAmsgeKOhq7O' }) - donation = create_donation(donor, payment) + donation = create_razorpay_donation(donor, payment) self.assertTrue(donation.name) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 4d80e57eccf..7639c2de68f 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -100,10 +100,13 @@ def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" + customer.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group") + customer.territory = frappe.db.get_single_value("Selling Settings", "territory") customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) try: + frappe.db.savepoint("contact_creation") contact = frappe.new_doc("Contact") contact.first_name = user_details.fullname if user_details.mobile: @@ -129,6 +132,7 @@ def create_customer(user_details, member=None): return customer.name except Exception as e: + frappe.db.rollback(save_point="contact_creation") frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) pass diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 11d32f9c2b4..df7f723c944 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -21,9 +21,11 @@ "paid", "currency", "amount", + "column_break_16", "invoice", "razorpay_details_section", "subscription_id", + "column_break_19", "payment_id" ], "fields": [ @@ -106,20 +108,17 @@ { "fieldname": "razorpay_details_section", "fieldtype": "Section Break", - "hidden": 1, "label": "Razorpay Details" }, { "fieldname": "subscription_id", "fieldtype": "Data", - "label": "Subscription ID", - "read_only": 1 + "label": "Subscription ID" }, { "fieldname": "payment_id", "fieldtype": "Data", - "label": "Payment ID", - "read_only": 1 + "label": "Payment ID" }, { "fieldname": "invoice", @@ -140,11 +139,19 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-19 14:33:44.925122", + "modified": "2022-03-16 17:37:28.672916", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 297a2dccb65..2809c8da1a7 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -104,7 +104,7 @@ class Membership(Document): return invoice def validate_membership_type_and_settings(self, plan, settings): - settings_link = get_link_to_form("Membership Type", self.membership_type) + settings_link = get_link_to_form("Non Profit Settings", "Non Profit Settings") if not settings.membership_debit_account: frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9370087da43..ccafb6bb564 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -353,3 +353,4 @@ erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items +erpnext.patches.v13_0.rename_non_profit_fields \ No newline at end of file diff --git a/erpnext/patches/v13_0/rename_non_profit_fields.py b/erpnext/patches/v13_0/rename_non_profit_fields.py new file mode 100644 index 00000000000..b6fc0a72c10 --- /dev/null +++ b/erpnext/patches/v13_0/rename_non_profit_fields.py @@ -0,0 +1,17 @@ + +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.table_exists("Donation"): + frappe.reload_doc("non_profit", "doctype", "Donation") + + rename_field("Donation", "razorpay_payment_id", "payment_id") + + if frappe.db.table_exists("Tax Exemption 80G Certificate"): + frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate") + frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate Detail") + + rename_field("Tax Exemption 80G Certificate", "razorpay_payment_id", "payment_id") + rename_field("Tax Exemption 80G Certificate Detail", "razorpay_payment_id", "payment_id") \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js index 54cde9c0cf4..5f840daba67 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js @@ -36,7 +36,7 @@ frappe.ui.form.on('Tax Exemption 80G Certificate', { 'date_of_donation': '', 'amount': 0, 'mode_of_payment': '', - 'razorpay_payment_id': '' + 'payment_id': '' }); } }, diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json index 9eee722f420..9b182ad4969 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json @@ -38,7 +38,7 @@ "amount", "column_break_27", "mode_of_payment", - "razorpay_payment_id" + "payment_id" ], "fields": [ { @@ -201,13 +201,6 @@ "options": "Mode of Payment", "read_only": 1 }, - { - "fetch_from": "donation.razorpay_payment_id", - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "RazorPay Payment ID", - "read_only": 1 - }, { "fetch_from": "donation.date", "fieldname": "date_of_donation", @@ -266,11 +259,18 @@ "hidden": 1, "label": "Title", "print_hide": 1 + }, + { + "fetch_from": "donation.payment_id", + "fieldname": "payment_id", + "fieldtype": "Data", + "label": "Payment ID", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-22 00:03:34.215633", + "modified": "2022-03-16 17:21:39.831059", "modified_by": "Administrator", "module": "Regional", "name": "Tax Exemption 80G Certificate", 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 0f0897841b4..dc3ee6f28e2 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 @@ -6,29 +6,19 @@ import frappe from frappe import _ from frappe.contacts.doctype.address.address import get_company_address from frappe.model.document import Document -from frappe.utils import flt, get_link_to_form, getdate +from frappe.utils import flt, get_link_to_form from erpnext.accounts.utils import get_fiscal_year class TaxExemption80GCertificate(Document): def validate(self): - self.validate_date() self.validate_duplicates() self.validate_company_details() self.set_company_address() self.calculate_total() self.set_title() - def validate_date(self): - if self.recipient == 'Member': - if getdate(self.date): - fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - - if not (fiscal_year.year_start_date <= getdate(self.date) \ - <= fiscal_year.year_end_date): - frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year))) - def validate_duplicates(self): if self.recipient == 'Donor': certificate = frappe.db.exists(self.doctype, { @@ -96,7 +86,7 @@ class TaxExemption80GCertificate(Document): 'date': doc.from_date, 'amount': doc.amount, 'invoice_id': doc.invoice, - 'razorpay_payment_id': doc.payment_id, + 'payment_id': doc.payment_id, 'membership': doc.name }) total += flt(doc.amount) diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py index 6fa3b85d061..4e328931ec1 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import getdate from erpnext.accounts.utils import get_fiscal_year -from erpnext.non_profit.doctype.donation.donation import create_donation +from erpnext.non_profit.doctype.donation.donation import create_razorpay_donation from erpnext.non_profit.doctype.donation.test_donation import ( create_donor, create_donor_type, @@ -39,11 +39,11 @@ class TestTaxExemption80GCertificate(unittest.TestCase): donor = create_donor() create_mode_of_payment() payment = frappe._dict({ - 'amount': 100, + 'amount': 100, # rzp sends data in paise 'method': 'Debit Card', 'id': 'pay_MeXAmsgeKOhq7O' }) - donation = create_donation(donor, payment) + donation = create_razorpay_donation(donor, payment) args = frappe._dict({ 'recipient': 'Donor', diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json index dfa817dd271..c863aab3285 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json @@ -9,7 +9,7 @@ "amount", "invoice_id", "column_break_4", - "razorpay_payment_id", + "payment_id", "membership" ], "fields": [ @@ -35,26 +35,28 @@ "options": "Sales Invoice", "reqd": 1 }, - { - "fieldname": "razorpay_payment_id", - "fieldtype": "Data", - "label": "Razorpay Payment ID" - }, { "fieldname": "membership", "fieldtype": "Link", + "in_list_view": 1, "label": "Membership", "options": "Membership" }, { "fieldname": "column_break_4", "fieldtype": "Column Break" + }, + { + "fieldname": "payment_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Payment ID" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-15 16:35:10.777587", + "modified": "2022-03-17 11:55:24.621708", "modified_by": "Administrator", "module": "Regional", "name": "Tax Exemption 80G Certificate Detail", diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json index a8da0bd2097..2343b4ec5cc 100644 --- a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json +++ b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json @@ -10,10 +10,10 @@ "docstatus": 0, "doctype": "Print Format", "font": "Default", - "html": "{% if letter_head and not no_letterhead -%}\n
{{ letter_head }}
\n{%- endif %}\n\n
\n

{{ doc.company }} 80G Donor Certificate

\n
\n

\n\n
\n

{{ _(\"Certificate No. : \") }} {{ doc.name }}

\n

\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n

\n

\n \n
\n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n

\n \n

\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", + "html": "{% if letter_head and not no_letterhead -%}\n
{{ letter_head }}
\n{%- endif %}\n\n
\n

{{ doc.company }} 80G Donor Certificate

\n
\n

\n\n
\n

{{ _(\"Certificate No. : \") }} {{ doc.name }}

\n

\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n

\n

\n \n
\n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.payment_id -%}\n bearing Payment ID {{ doc.payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n

\n \n

\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", "idx": 0, "line_breaks": 0, - "modified": "2021-02-22 00:20:08.516600", + "modified": "2022-03-16 17:25:33.420509", "modified_by": "Administrator", "module": "Regional", "name": "80G Certificate for Donation", From 3b1033c315772d7b7b558f384efc3d3c88b5cd27 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 15:39:02 +0530 Subject: [PATCH 136/136] test: fix holiday list creation causing flaky tests (backport #30260) (#30288) Co-authored-by: Rucha Mahabal --- .../hr/doctype/attendance/test_attendance.py | 26 +++++++++--------- .../test_leave_application.py | 7 ++++- .../doctype/salary_slip/test_salary_slip.py | 27 ++++++++++--------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index 6095413771c..585059ff479 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate +from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate from erpnext.hr.doctype.attendance.attendance import ( get_month_map, @@ -16,6 +16,13 @@ from erpnext.hr.doctype.leave_application.test_leave_application import get_firs test_records = frappe.get_test_records('Attendance') class TestAttendance(FrappeTestCase): + def setUp(self): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + + from_date = get_year_start(getdate()) + to_date = get_year_ending(getdate()) + self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) + def test_mark_absent(self): employee = make_employee("test_mark_absent@example.com") date = nowdate() @@ -31,12 +38,9 @@ class TestAttendance(FrappeTestCase): employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) frappe.db.delete('Attendance', {'employee': employee}) + frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list) - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - holiday_list = make_holiday_list() - frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) - - first_sunday = get_first_sunday(holiday_list, for_date=first_day) + first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) mark_attendance(employee, first_day, 'Present') month_name = get_month_name(first_day) @@ -58,11 +62,9 @@ class TestAttendance(FrappeTestCase): employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) frappe.db.delete('Attendance', {'employee': employee}) - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - holiday_list = make_holiday_list() - frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) + frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list) - first_sunday = get_first_sunday(holiday_list, for_date=first_day) + first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) mark_attendance(employee, first_day, 'Present') month_name = get_month_name(first_day) @@ -87,9 +89,7 @@ class TestAttendance(FrappeTestCase): relieving_date=relieving_date) frappe.db.delete('Attendance', {'employee': employee}) - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - holiday_list = make_holiday_list() - frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) + frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list) attendance_date = add_days(first_day, 2) mark_attendance(employee, attendance_date, 'Present') diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index aaf0e4f1ce6..af633e776cd 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -82,7 +82,11 @@ class TestLeaveApplication(unittest.TestCase): set_leave_approver() frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"}) - self.holiday_list = make_holiday_list() + frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "") + + from_date = get_year_start(getdate()) + to_date = get_year_ending(getdate()) + self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) if not frappe.db.exists("Leave Type", "_Test Leave Type"): frappe.get_doc(dict( @@ -316,6 +320,7 @@ class TestLeaveApplication(unittest.TestCase): leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company) leave_application.reload() + # holiday should be excluded while marking attendance self.assertEqual(leave_application.total_leave_days, 3) self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index c8e381e58eb..4e5f00de600 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -415,7 +415,7 @@ class TestSalarySlip(unittest.TestCase): def test_email_salary_slip(self): frappe.db.sql("delete from `tabEmail Queue`") - make_employee("test_email_salary_slip@salary.com") + make_employee("test_email_salary_slip@salary.com", company="_Test Company") ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email") ss.company = "_Test Company" ss.save() @@ -1096,18 +1096,19 @@ def setup_test(): def make_holiday_list(list_name=None, from_date=None, to_date=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) name = list_name or "Salary Slip Test Holiday List" - holiday_list = frappe.db.exists("Holiday List", name) - if not holiday_list: - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": name, - "from_date": from_date or fiscal_year[1], - "to_date": to_date or fiscal_year[2], - "weekly_off": "Sunday" - }).insert() - holiday_list.get_weekly_off_dates() - holiday_list.save() - holiday_list = holiday_list.name + + frappe.delete_doc_if_exists("Holiday List", name, force=True) + + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": name, + "from_date": from_date or fiscal_year[1], + "to_date": to_date or fiscal_year[2], + "weekly_off": "Sunday" + }).insert() + holiday_list.get_weekly_off_dates() + holiday_list.save() + holiday_list = holiday_list.name return holiday_list