From ab4a96dadd530211c253ad99425dc0a86f5f4cc2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Feb 2022 13:07:51 +0530 Subject: [PATCH 001/148] 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 2ba649da07fd..158ff4d3437a 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 a8b5a0e28bd1..0af7ca9c8bd5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -371,20 +371,37 @@ def get_average_buying_rate(self, row, item_code): return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") + + 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: - 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) - - 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.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/148] 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 76c560ad247e..0730ffd77e5b 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 0af7ca9c8bd5..3c8e3eb898e7 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 @@ def process(self): 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 @@ def get_average_rate_based_on_group_by(self): 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 @@ def get_average_rate_based_on_group_by(self): 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 @@ def set_average_gross_profit(self, new_row): 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 @@ def get_last_purchase_rate(self, item_code, row): 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/148] 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 3c8e3eb898e7..2ec64ed57595 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -172,7 +172,7 @@ def process(self): 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 @@ def get_returned_invoice_items(self): 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/148] 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 2ec64ed57595..f6c8c63d8a52 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -284,8 +284,8 @@ def skip_row(self, row): 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/148] 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 4211bd0169d1..26078d632985 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 7811d56a7581..50926d772686 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 df3aadfb18d6..54a03b92b5eb 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 @@ def set_missing_values(self): 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 @@ def get_values_on_submit(self, loan_details): 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 @@ def make_gl_entries(self, cancel=0, adv_adj=0): 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 93ef21704208..766602de8669 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 a6e526a0490c..0610da14d4ce 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 @@ def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_deta 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 @@ def make_gl_entries(self, cancel=0, adv_adj=0): 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 @@ def make_gl_entries(self, cancel=0, adv_adj=0): 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 @@ def make_gl_entries(self, cancel=0, adv_adj=0): 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 @@ def make_gl_entries(self, cancel=0, adv_adj=0): "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 75ed332f4b68..bb799af36eab 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/148] 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 6aaf9aa33aad..f7bc17cb3a80 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 000000000000..440f912be21a --- /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/148] 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 26078d632985..f3351ddcba4c 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 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 44cea31ed381..464d645a9944 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -48,7 +48,8 @@ def update_allocations(self): 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 766602de8669..480e010b49a5 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/148] 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 6c401fb8f3bd..b72d2669775d 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/148] 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 464d645a9944..b8e8d9702201 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/148] fix: Resolve conflicts --- erpnext/patches.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f7bc17cb3a80..d53df7e4f630 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/148] 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 b1eaaf8b5872..b1e373e21815 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 + + if joining_date and joining_date.month == month_map[month]: + start_day = joining_date.day - length = len(dates_of_month) - month_start, month_end = dates_of_month[0], dates_of_month[length-1] + if relieving_date and relieving_date.month == month_map[month]: + end_day = relieving_date.day + 1 + 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 = [ + 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 a770d70ffa93..118cc987efb3 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 e70c5116bed5..422bb0e1bbc6 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -308,28 +308,59 @@ def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=N 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 + + 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 self.total_working_days - 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 4249fa76c718..20060f479acc 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 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 @@ 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 @@ def setUp(self): 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,13 +88,77 @@ def test_payment_days_based_on_attendance(self): 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 - def test_payment_days_based_on_leave_application(self): no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) - # Payroll based on attendance - frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + 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() 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 @@ def test_payment_days_based_on_leave_application(self): 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 @@ def test_payment_days_in_salary_slip_based_on_timesheet(self): 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 @@ def test_payment_days_in_salary_slip_based_on_timesheet(self): 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 @@ def test_component_amount_dependent_on_another_payment_days_based_component(self ) 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 @@ def test_component_amount_dependent_on_another_payment_days_based_component(self 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 @@ def test_salary_slip_with_holidays_included(self): 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 @@ def test_salary_slip_with_holidays_excluded(self): 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 @@ def test_employee_salary_slip_read_permission(self): 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/148] 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 08d1dcea7dc0..d19047479184 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -116,7 +116,7 @@ def calculate_item_values(self): 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 474cf56fc1bc..0ff00c097627 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/148] 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 d19047479184..692af71a57a7 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -116,11 +116,11 @@ def calculate_item_values(self): 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/148] 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 692af71a57a7..36e4f0a759df 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -113,17 +113,24 @@ def calculate_item_values(self): 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/148] 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 47a97c47fe5a..dd7fb66f983b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -834,7 +834,7 @@ def raise_exceptions(self): 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/148] 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 0ff00c097627..474cf56fc1bc 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/148] 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 40ab0c50deb4..41dfa226a56c 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 @@ def process_merging_into_sales_invoice(self, data): 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 @@ def process_merging_into_credit_note(self, data): # 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 5930aa097f74..89f7f18b42cc 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 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 @@ def test_consolidation_round_off_error_2(self): 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 42da6b7708f6..409677f3c26c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -272,6 +272,9 @@ def on_submit(self): 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 08d1dcea7dc0..bcaf7a145a24 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -270,7 +270,8 @@ def calculate_shipping_charges(self): 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 @@ def calculate_taxes(self): 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 @@ def calculate_total_net_weight(self): 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 + + 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 - self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, - self.doc.currency, self.doc.precision("rounded_total")) + self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, + self.doc.currency, self.doc.precision("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")) + #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"]) + 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/148] 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 f98e5f12c2d5..60a8f92cc34a 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 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")) - - 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) + query = query.where(mr_item.item_code == filters.get("item_code")) - 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 000000000000..f3c751c5c3c1 --- /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 383b0ae806e7..705ef27b37a5 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 @@ def test_multi_uom_for_purchase(self): 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 3fc357e8d4fe..562d178c3292 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/148] 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 118cc987efb3..c74967d213e4 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 @@ 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 @@ def test_unmarked_days_excluding_holidays(self): 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 39356bdcf182..38b8eef0c9b9 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 3396a2193cf1..c80f301962c4 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.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 @@ def test_project_profitability(self): 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/148] 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 70250f5bcf85..ef5f4bcb0ff7 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") + + 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) + ) + ) - 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 + 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/148] 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 440f912be21a..0b26f55e002a 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 @@ 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/148] 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 b7987543f2bb..86f24c2427c3 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/148] 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 86f24c2427c3..b2eaecb58687 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.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/148] 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 4a245e1f7781..56e1eb57b817 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/148] 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 885d874720d5..f570c2faec66 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/148] 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 5062c1c807a0..973c8371ea23 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/148] 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 e7b4ca2de38e..59f02e361145 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 a12a3bd4417477c88734c9a09a3e99962157ac79 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 15:38:38 +0530 Subject: [PATCH 028/148] fix: dont fetch entire barcode table in get_item_details (#30131) (#30135) (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 e7b4ca2de38e..59f02e361145 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 76376d7ad7097962a368a63cced491b7cebf6bfe Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Mar 2022 15:41:58 +0530 Subject: [PATCH 029/148] chore: bump version --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 59ac78cd09a8..90a9a21edd32 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ from erpnext.hooks import regional_overrides -__version__ = '13.22.0' +__version__ = '13.22.1' def get_default_company(user=None): '''Get default company for user''' From 856534c8a08f34915ebe27f798c25bd88068d556 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Mar 2022 15:42:06 +0530 Subject: [PATCH 030/148] 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 973c8371ea23..82854ba2a6c7 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 031/148] 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 82854ba2a6c7..80b95db88689 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 032/148] 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 3ad1da4a92f9..b363e0f56dd8 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 6d44b2cb9772..ad03c4b286cb 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -14,6 +14,8 @@ def __init__(self, item_group=None): 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 @@ def get_field_filters(self): 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 1a2ddeb02512..27e37ce6fe51 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -46,10 +46,10 @@ def query(self, attributes=None, fields=None, search_term=None, start=0, item_gr 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 @@ def query(self, attributes=None, fields=None, search_term=None, start=0, item_gr 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 @@ def build_fields_filters(self, filters): # `=` 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 @@ def build_search_filters(self, search_term): 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 @@ def get_cart_items(self): 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 3e0680f4f519..a090c8d76c56 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 7695affde604..5bcd0e4e21e6 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -112,7 +112,7 @@ def validate_item_group_defaults(self): 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 033/148] 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 f0f7918d00ee..6549ba692afc 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 @@ 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 @@ def setUpClass(cls): 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 @@ def test_products_in_multiple_item_groups(self): 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 @@ def test_item_group_with_sub_groups(self): # 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 a090c8d76c56..50f923d87e0b 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 034/148] 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 c9efe3d08481..cae34c93f7eb 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 035/148] =?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 adc77e8921c6..5d6e8dff3fa2 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 036/148] 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 c9efe3d08481..461a67dbfc6d 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 52996e934753..5c03b98873b6 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 037/148] 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 461a67dbfc6d..c81b611a4188 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 038/148] 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 000000000000..b196fb5b9895 --- /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 039/148] 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 daa24207a558..c81b611a4188 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 040/148] 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 874fb630f875..6e042422101f 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 041/148] 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 c0b262395d8b..bca24c8dcd48 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 042/148] 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 df8714914228..c78227e4c86a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -219,7 +219,9 @@ def validate_credit_limit_on_change(self): 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 043/148] 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 95ba8f7df28a..dd3c23a0145f 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -39,16 +39,13 @@

{{ title }}

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 044/148] 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 088c190f4518..29f2e98e7799 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 045/148] 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 b196fb5b9895..952af8117e22 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 046/148] 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 f8d191cc3f87..82705a9cea46 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 047/148] 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 1ce09f0152c8..2a2eafbb3917 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 7bc875ac12f3..5ab2c4acfdd8 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 048/148] 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 907964720ffd..f1862b31e7ed 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 000000000000..f7a1a858ab8c --- /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 049/148] 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 f1862b31e7ed..de34a879db76 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 050/148] 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 c74967d213e4..6095413771cd 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -25,7 +25,9 @@ def test_mark_absent(self): 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 @@ def test_unmarked_days(self): 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 @@ def test_unmarked_days(self): 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 @@ def test_unmarked_days_excluding_holidays(self): 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 @@ def test_unmarked_days_as_per_joining_and_relieving_dates(self): 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 38b8eef0c9b9..c9f377dfaca1 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 @@ def test_attendance_for_include_holidays(self): 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 @@ def test_attendance_for_include_holidays(self): 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 @@ def test_attendance_update_for_exclude_holidays(self): 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 @@ def test_attendance_update_for_exclude_holidays(self): 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 @@ def test_attendance_update_for_exclude_holidays(self): # 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 @@ def test_optional_leave(self): 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 @@ def test_optional_leave(self): # 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 051/148] 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 fff9f079744d..7ab568895698 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 ba3a36685df1..bdbdaa0e45aa 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 @@ _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 @@ def tearDown(self): 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 @@ def test_add_item_variant_without_web_item_to_cart(self): # 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 052/148] 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 bdbdaa0e45aa..72d6962708db 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 053/148] 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 72d6962708db..b15bac682349 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 @@ 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 054/148] 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 087a7883e090..cc64b5caa523 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 055/148] 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 c912101a4acd..68e545ba140e 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -370,23 +370,24 @@ def test_make_item_variant_with_numeric_values(self): 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 c6baa46c5eb9..4b83e5e85324 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 056/148] 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 5ab2c4acfdd8..efcaa90198c3 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -393,6 +393,7 @@ def before_rename(self, old_name, new_name, merge=False): 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 @@ def after_rename(self, old_name, new_name, merge): 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 @@ def recalculate_bin_qty(self, new_name): 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 057/148] 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 b97dcab632f8..cde25a2bc22b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -192,12 +192,13 @@ def get_routing(self): 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 ec617f3aaa93..9877b2882afc 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 058/148] 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 dd7fb66f983b..4b9686cb2f6a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -843,11 +843,13 @@ def update_bin(self): 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 059/148] 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 f3c751c5c3c1..a533da00e3aa 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 @@ 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 + 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 @@ def test_ordered_received_material_requests(self): 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 060/148] 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 b13d6d3d05a2..a737d873a96f 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 @@ def test_closed_purchase_receipt(self): 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 061/148] 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 792e7d21a78a..7e5129911e46 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -71,8 +71,7 @@ def apply(self, doc): 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 e218c0cd0314..a642b8a083e5 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -37,6 +37,8 @@ def calculate(self): 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 @@ def _calculate(self): 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 @@ def calculate_shipping_charges(self): 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 2b80efd6e33b..8068879810f0 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 1102fe96fc43..dd3a007c411a 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 @@ def test_so_back_updated_from_wo_via_mr(self): 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 062/148] 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 df3c5f10c1bc..721aabbdb8e7 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 update_item(source_doc, target_doc, source_parent): 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 update_item(source_doc, target_doc, source_parent): 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 update_item(source_doc, target_doc, source_parent): 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 update_item(source_doc, target_doc, source_parent): 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 00836fc81574..c8b8a0c01659 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -779,3 +779,6 @@ def update_details(source_doc, target_doc, source_parent): }, 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 afaa8b02a894..db2656e8c596 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 063/148] 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 721aabbdb8e7..7d4ef587526b 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 update_item(source_doc, target_doc, source_parent): 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 update_item(source_doc, target_doc, source_parent): 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 064/148] 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 55a4c956a67b..7ebc4eed751b 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 b54a90eed355..6d4b4a19bd24 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 065/148] 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 69ab1738bc64..ad0fc63e47b6 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 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): 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 066/148] 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 409677f3c26c..c0430e1b9de5 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.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 @@ def on_submit(self): # 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 ad0fc63e47b6..5e4a6d619cbe 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 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): 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 c8b8a0c01659..e7ab101d5a8c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -13,7 +13,10 @@ 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 @@ def on_submit(self): # 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 bd18e788ba62..4d26397f4823 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 @@ def test_payment_terms_are_fetched_when_creating_sales_invoice(self): 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 e300d46db839..350d5fec50fc 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 067/148] 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 6b31bcc05fc7..05b0c3c8f09a 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 @@ def get_itc_details(self): 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 @@ def get_inward_nil_exempt(self, state): 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 @@ def get_inward_nil_exempt(self, state): } } + 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 @@ def get_missing_field_invoices(self): 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 068/148] 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 f3351ddcba4c..317fcc02b501 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 b8e8d9702201..e3fc1dc73f82 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 069/148] 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 | 137 +++++++++++++----- 1 file changed, 102 insertions(+), 35 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 b375b18b0793..6280ef323b78 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 @@ 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 +def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): 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 + # 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): - new_allocation += record.leaves + 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 070/148] 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 6280ef323b78..5172fb8fc28f 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 071/148] 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 ef5f4bcb0ff7..68b345631cf2 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(remaining_leaves, end_date): 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 5c5299ea7eb3..a5923e0021ca 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 072/148] 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 68b345631cf2..55968eb107a6 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -30,11 +30,13 @@ 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 @@ def validate_balance_leaves(self): 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 @@ def create_leave_ledger_entry(self, submit=True): 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 @@ def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, su 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 073/148] 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 55968eb107a6..db7560d78aae 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 @@ 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 074/148] 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 5172fb8fc28f..3f0337e508c8 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 075/148] 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 c9239edb720a..aed901aa6da3 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 c9f377dfaca1..2e93c41a7f71 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 @@ def test_leave_balance_near_allocaton_expiry(self): 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 3f0337e508c8..3a5f2fe9629f 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 @@ 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 000000000000..05316f165f3e --- /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 20060f479acc..bd89bf4e4899 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 076/148] 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 3a5f2fe9629f..5c18d11721dc 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 @@ 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 05316f165f3e..f844be5ff112 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 077/148] 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 f844be5ff112..aecf0a4d4e57 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 @@ def setUp(self): 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 @@ def test_employee_leave_balance(self): # 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 @@ def test_opening_balance_on_alloc_boundary_dates(self): 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 936184a9c0de..e484d9e3a112 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 @@ 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 000000000000..9b953de0dc27 --- /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 078/148] 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 9e8cb5516f3b..85997a4087f4 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 db7560d78aae..5a551d703946 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 9f667a68356a..e755322efda2 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 422bb0e1bbc6..6238f9c10e1e 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1390,7 +1390,7 @@ def add_leave_balances(self): '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 7ac453b3c3dd..60ed45393852 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 079/148] 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 8d7b27ee5aff..08680425a02e 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 @@ def test_earned_leave_allocation(self): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + # 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 = 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 080/148] 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 5a551d703946..cad1b4757930 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(remaining_leaves, end_date): 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 2e93c41a7f71..b4547280448e 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 @@ def test_leave_balance_near_allocaton_expiry(self): 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 081/148] 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 cad1b4757930..7fecc0c67fd7 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 b4547280448e..abfc5dfdca43 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 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 @@ 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 @@ 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 @@ def test_overwrite_attendance(self): 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 @@ def test_attendance_for_include_holidays(self): 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 @@ def test_attendance_update_for_exclude_holidays(self): 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 @@ def test_overlap_with_half_day_3(self): 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 @@ def test_leave_approver_perms(self): 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 bd89bf4e4899..bdf693972662 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 082/148] 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 5c18d11721dc..3324ede1dd76 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 @@ 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 aecf0a4d4e57..b2ed72c04d7e 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 @@ def test_employee_leave_balance(self): 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 @@ def test_opening_balance_on_alloc_boundary_dates(self): 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 @@ def test_opening_balance_considers_carry_forwarded_leaves(self): 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 083/148] 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 | 135 +++++++++++++----- 1 file changed, 99 insertions(+), 36 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 7fecc0c67fd7..de45c86bb383 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -138,21 +138,36 @@ def validate_dates(self): def validate_dates_across_allocation(self): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): return + + 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): - 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)) + 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][0] + 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 @@ def create_leave_ledger_entry(self, submit=True): 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) + + 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( - leaves=self.total_leave_days * -1, + 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, - is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' - ) + 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 ''' + """Splits leave application into two ledger entries to consider expiry of allocation""" + raise_exception = False if frappe.flags.in_patch else True - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False + leaves = get_number_of_leave_days(self.employee, self.leave_type, + self.from_date, expiry_date, self.half_day, self.half_day_date) - 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 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) 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 084/148] 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 de45c86bb383..10244138318f 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -143,8 +143,7 @@ def validate_dates_across_allocation(self): 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 @@ def validate_balance_leaves(self): 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 @@ def create_leave_ledger_entry(self, submit=True): 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 085/148] 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 10244138318f..2256b53064ac 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 @@ def validate_dates_across_allocation(self): 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 abfc5dfdca43..b055df6ab670 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.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 @@ def setUp(self): 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 @@ def get_application(self, doc): 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 @@ def test_earned_leaves_creation(self): # 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 086/148] 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 2256b53064ac..0fa34ac6f339 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -503,10 +503,18 @@ def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, s 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 @@ def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, s 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 087/148] 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 b055df6ab670..e0f607477b22 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 @@ def test_insufficient_leave_balance_validation(self): 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 d4301675d1b58e2e7cf15ae6ee88d54cd6aecd57 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Mar 2022 16:58:15 +0530 Subject: [PATCH 088/148] chore: linter issue (cherry picked from commit 921d6b25d7bf974f3591bde6e6d9fa50a9791111) From c66a66e0902195b9fff502d8df13c4b1f1178356 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Mar 2022 15:41:14 +0530 Subject: [PATCH 089/148] 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 e0f607477b22..1898a0808dd7 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 @@ def test_attendance_update_for_exclude_holidays(self): # 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 @@ def test_optional_leave(self): # 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 090/148] 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 0fa34ac6f339..095d0eae8c28 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -295,12 +295,7 @@ def show_insufficient_balance_message(self, leave_balance_for_consumption): 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 3324ede1dd76..66c1d25d593e 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 @@ 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 091/148] 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 095d0eae8c28..518d79aa34bd 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 @@ def validate_dates_across_allocation(self): 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 @@ def validate_balance_leaves(self): 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 @@ def create_leave_ledger_entry(self, submit=True): ) 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 @@ def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, su 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(remaining_leaves, end_date): 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 e484d9e3a112..936184a9c0de 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 @@ 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 9b953de0dc27..b76858d84384 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 @@ def test_employee_leave_balance_summary(self): 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 @@ def test_get_leave_balance_near_alloc_expiry(self): # 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 092/148] 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 1898a0808dd7..3b742611bf0d 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 @@ def test_attendance_for_include_holidays(self): 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 b76858d84384..6f16a8d58cbc 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 @@ def test_employee_leave_balance_summary(self): 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 @@ def test_get_leave_balance_near_alloc_expiry(self): # 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 093/148] 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 3b742611bf0d..aaf0e4f1ce67 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 @@ def test_attendance_for_include_holidays(self): 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 @@ def test_attendance_for_include_holidays(self): 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 @@ def test_attendance_update_for_exclude_holidays(self): 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 @@ def test_optional_leave(self): 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 bdf693972662..c8e381e58ebb 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 094/148] 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 cde25a2bc22b..e5c046c493a0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -121,7 +121,21 @@ def autoname(self): 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 095/148] 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 bfafacdfb57b..08ac50fa6ee1 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -385,6 +385,15 @@ def test_valid_transfer_defaults(self): 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 096/148] fix: improve bom autoname logic (cherry picked from commit 7f2670941ca7f2e41a37a8ea3ed186a0fa04b57c) --- erpnext/manufacturing/doctype/bom/bom.py | 44 ++++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e5c046c493a0..14562c2824b9 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) - - if names: - # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1 - - # 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)] - - # split by (-) if cancelled - if names: - names = [cint(name.split('-')[-1]) for name in names] - idx = max(names) + 1 + existing_boms = frappe.get_all("BOM", filters={"item": self.item}) + 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: - 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 @@ def autoname(self): 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 097/148] 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 14562c2824b9..2c34e83a5b96 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 @@ def autoname(self): 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 08ac50fa6ee1..f43329121f38 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -394,6 +394,27 @@ def test_bom_name_length(self): } 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 098/148] 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 2c34e83a5b96..a6eef6049e34 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 @@ def get_next_version_index(existing_boms: List[str]) -> int: 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 f43329121f38..ac52ba2ce95d 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -402,12 +402,9 @@ def test_version_index(self): (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 099/148] 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 ac52ba2ce95d..d0e79aa9a81f 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -412,6 +412,42 @@ def test_version_index(self): 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 100/148] 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 47fe3296cf1e..0bfa7a286b5e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -635,12 +635,12 @@ def validate_qty(self): 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 101/148] 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 69c85a32533d..c15c917f8288 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 102/148] 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 68e545ba140e..baaf51b47a01 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -29,7 +29,10 @@ 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 6d113ba4eb6a..1ae2132dc2e7 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 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 @@ 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 @@ def test_fifo_multi_item_repack_consumption(self): {"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 103/148] 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 975216d1bd94..28226290e2fe 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.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 @@ def test_partial_manufacture_entries(self): 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 47fe3296cf1e..e2cbc3d3c2f7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -333,6 +333,14 @@ def create_batch_for_finished_good(self): 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 104/148] 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 a642b8a083e5..78914160e726 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -637,7 +637,8 @@ def calculate_outstanding_amount(self): 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 @@ def calculate_margin(self, item): 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 @@ def update_paid_amount_for_return(self, total_amount_to_pay): '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 105/148] 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 4ef29848bc67..0dc694f5a8b7 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 106/148] 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 54b381d7f896..30ca22aedc5e 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 1c9743c57002..c62622eecec7 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 107/148] 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 56cf2b4f08a0..acc1b0017d0e 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 d53df7e4f630..9370087da430 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 000000000000..0284097e281f --- /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 108/148] 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 3eaf6a28f37e..77d54a605e5c 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.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 b15bac682349..51d370595706 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 @@ 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 967be838e673..45234b8dbe27 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.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 @@ def test_exact_match_with_price(self): 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 eff2344e85cc..d4d337d8412e 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 d0e79aa9a81f..98314d9fade0 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -7,6 +7,7 @@ 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 @@ 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 12576cbf3225..b4c625d61089 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 bb5004ba86f0..33425d231423 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.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 2359815813d7..b2a41ff5b2a6 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_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 8bd60ea4aca4..696d9bca1448 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 c298c0a8dbb4..dd51017bb752 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 @@ 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 7802a3fea449..3da38a34522d 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 @@ 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 b951044f332d..9b672b4b5d3a 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 4357201d23d9..a749d9e1f1f8 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 dd3a007c411a..9d093b205e94 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 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.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 cad41e1dc03a..f7f8a5dbce31 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 @@ 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 d62915fc66da..16162acc8f35 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.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 f56cce2dfdca..564f48fef3bb 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 0a663c2a188f..8d7a2cf8d8c4 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 250126c6b98c..ec0d8a88e3f8 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 4d26397f4823..fc3dce1ee90d 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.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 321f48b2c595..dcdff4a0f1eb 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 @@ 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 baaf51b47a01..a50ced006cc3 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,6 +6,7 @@ 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.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 3976af4e88c8..501c1c1ad3cd 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.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 0b7ca257151f..055c22e0c5d0 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 @@ 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 f81770e487de..6ceba3f8d3f5 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 1ea0596d3332..6dc4fee56975 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 @@ 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 705ef27b37a5..866f3ab2d57e 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 @@ 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 2521ac9fe72c..94268a8ef377 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.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 5eb6b7399ae4..bc405b209951 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 41e3150f0d78..f3b6b89784a2 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 @@ 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 a737d873a96f..c8a8fce7d63c 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 @@ 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.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 ff1c19a82752..4e8d71fe5e41 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.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 308c62875d54..601ca054b53f 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.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 f8cea7172513..057a7d4c01f3 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 @@ 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 afe821845ae4..317abb6d03ec 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 3c34d4795cbb..6a3b21d81c84 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 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 @@ 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 86af0a0cf3be..d3e637138474 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.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 072b54b8205f..13496718eade 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 frappe.tests.utils import FrappeTestCase -from erpnext.tests.utils import ERPNextTestCase - -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 26db2642e4b8..cdb771935b0e 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 562d178c3292..ca963b74863a 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 32df58593756..f6c98f914d29 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 1568b1420226..0a9a5f84e119 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 @@ 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 109/148] 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 2359815813d7..f80670525651 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 @@ def test_qty_based_status(self): ] 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 110/148] 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 023c95d697d0..933949f51634 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 111/148] 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 933949f51634..567e41fb61f5 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 18d7c77bad25c04fe6ada296f2dad2a788fd1948 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 14 Mar 2022 12:05:34 +0530 Subject: [PATCH 112/148] fix: max_qty validation condition (cherry picked from commit d198c488a4071d13d0891d87f790562febd44b3e) --- 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 47fe3296cf1e..0bfa7a286b5e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -635,12 +635,12 @@ def validate_qty(self): 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 46f9503a5cd57af6b6d7d68495b725a81dc9f95d Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 14 Mar 2022 16:28:23 +0530 Subject: [PATCH 113/148] test: add test for planned_qty (cherry picked from commit b22bdc5ff762d41242c742bb3f6fda64eb98fd2e) --- .../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 2359815813d7..f80670525651 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 @@ def test_qty_based_status(self): ] 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 4768f4f2788c7590af4c7e59cb677ff3c1551d67 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 14 Mar 2022 16:29:00 +0530 Subject: [PATCH 114/148] 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 6167becdaac0..66da215dbbe5 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 115/148] 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 d843dfd3ce37..70effce56849 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 116/148] 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 89f7f18b42cc..8909da96fcf9 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 @@ def test_consolidated_credit_note_creation(self): 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 @@ def test_consolidated_credit_note_creation(self): 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 78914160e726..40833b9300f6 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -636,7 +636,12 @@ def calculate_outstanding_amount(self): 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 117/148] 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 d6e2e9ce2d78..e94c34d7adc4 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 07c2f1f0dd3d..f9c00c59bacb 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 118/148] 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 a6eef6049e34..e9bb345c574e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -170,6 +170,7 @@ def validate(self): 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 @@ def clear_operations(self): 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 98314d9fade0..ad893a43ec34 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -26,6 +26,9 @@ 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(), @@ -448,6 +451,22 @@ def test_bom_versioning(self): 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 000000000000..a1b6415b54eb --- /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 119/148] 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 e9bb345c574e..797115abb27d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -419,7 +419,7 @@ def clear_operations(self): 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 ad893a43ec34..e9fb4862a059 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -6,7 +6,6 @@ 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.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 @@ def test_bom_versioning(self): 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 a1b6415b54eb..980f49a80aab 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 120/148] 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 8017209e7de0..7f60bdc6d92a 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 121/148] 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 3c406156ebd2..fae09b320a48 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -48,7 +48,7 @@ def validate(self): 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 122/148] 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 2e7d3063ccb5..2c6654285ff3 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 57c67424f7d8..603b64d22fa2 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -566,7 +566,6 @@ def postprocess(source, doc): @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 e7ab101d5a8c..dc6fd84e1a4d 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 db2656e8c596..32ffba392a94 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -724,7 +724,6 @@ def set_missing_values(source, target): 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 123/148] 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 674ea83cc655..80b7264adced 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 124/148] 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 80b7264adced..2859bbc3c967 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 125/148] 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 891e75e00334..c18af93b2c8c 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 126/148] 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 2859bbc3c967..60953ca6d834 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 127/148] 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 6238f9c10e1e..ade6cc776365 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -999,7 +999,7 @@ def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): # 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 @@ def set_totals(self): 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 128/148] 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 55125431b52b..6f9031fc5005 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 @@ 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 129/148] 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 9d585411582a..466b2833fe1b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -54,7 +54,7 @@ def validate(self): 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 @@ def before_cancel(self): 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 c0430e1b9de5..a563646e90bb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1422,12 +1422,19 @@ def set_loyalty_program_tier(self): 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 130/148] 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 | 7 ++----- erpnext/setup/doctype/item_group/item_group.py | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js index 99b91afac175..cc51718c47f5 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 b743504a527c..ff2482d3f642 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; + width: fit-content; padding: 6px 12px; + margin-bottom: 8px; background-color: #ecf5fe; - width: fit-content; 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 5bcd0e4e21e6..91b2f4f974f1 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 131/148] 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 89cc0a8c8cc0..fa8d69d7df81 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 132/148] 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 000000000000..2373c8c2a670 --- /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 133/148] 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 6e70bc579e71..4f30eb250c26 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 134/148] 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 a563646e90bb..615ebc5218cb 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 d65ab94a6d3a..34a4f26a0870 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 f191d9323eeb..ed8d93e7962e 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -151,7 +151,6 @@ def set_missing_values(source, target): 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 set_missing_values(source, target): 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 603b64d22fa2..6f329e037fa9 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -641,7 +641,6 @@ def postprocess(source, target): 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 135/148] 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 615ebc5218cb..f1e64c7d020c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -49,7 +49,6 @@ 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 @@ def update_current_stock(self): 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 dc6fd84e1a4d..b10a7015dec9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -17,7 +17,6 @@ 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 @@ def validate(self): 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 fc3dce1ee90d..9dc112fda854 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 @@ def test_return_entire_bundled_items(self): 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 @@ def test_standalone_serial_no_return(self): 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 1ae2132dc2e7..ad78fba347b7 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.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 @@ def test_reposting_of_sales_return_for_packed_item(self): 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 b8bdf39301e1..b843d52b1bfa 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 136/148] 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 94268a8ef377..5f1b9542d6a1 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 @@ def test_reposting_packed_items(self): 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 137/148] 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 b10a7015dec9..93d2357cfa9e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -13,10 +13,7 @@ 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 @@ def on_submit(self): # 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 9dc112fda854..82f4e7dd294f 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 @@ def test_payment_terms_are_fetched_when_creating_sales_invoice(self): 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 138/148] 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 4b83e5e85324..0dd867f41563 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 @@ def set_status(self, status=None, write=True): 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 139/148] 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 6759569d54d6..d2bcd17a20ed 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 617979ef7454..85f5a2652e9d 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 5fa731a6aa34..4e39adbcf263 100644 --- a/erpnext/non_profit/doctype/donation/test_donation.py +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -5,7 +5,7 @@ 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 @@ def test_payment_entry_for_donations(self): '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 4d80e57eccf0..7639c2de68fc 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 11d32f9c2b4f..df7f723c9449 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 297a2dccb65d..2809c8da1a73 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -104,7 +104,7 @@ def generate_invoice(self, save=True, with_payment_entry=False): 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 9370087da430..ccafb6bb564d 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 000000000000..b6fc0a72c10c --- /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 54cde9c0cf40..5f840daba676 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 9eee722f4203..9b182ad4969b 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 0f0897841b4e..dc3ee6f28e24 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 @@ 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 @@ def get_payments(self): '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 6fa3b85d0614..4e328931ec18 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 @@ 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 @@ def test_duplicate_donation_certificate(self): 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 dfa817dd2710..c863aab3285b 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 a8da0bd2097d..2343b4ec5ccb 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 140/148] 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 6095413771cd..585059ff4793 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 @@ 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 @@ def test_unmarked_days(self): 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 @@ def test_unmarked_days_excluding_holidays(self): 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 @@ def test_unmarked_days_as_per_joining_and_relieving_dates(self): 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 aaf0e4f1ce67..af633e776cd7 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 @@ def setUp(self): 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 @@ def test_attendance_update_for_exclude_holidays(self): 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 c8e381e58ebb..4e5f00de6001 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 @@ def test_employee_salary_slip_read_permission(self): 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 From eac07238d9c3e34c21a85910d1d577bfa0815454 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Mar 2022 12:56:43 +0530 Subject: [PATCH 141/148] fix: Clean and fixes in Dimension-wise Accounts Balance Report (cherry picked from commit 08a06ce5c68023c41f53b584082b5e1c4ddb1f59) --- .../dimension_wise_accounts_balance_report.js | 5 +- .../dimension_wise_accounts_balance_report.py | 154 ++++++++---------- 2 files changed, 76 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js index 6a0394861b8a..ea05a35b259c 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js @@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("From Date"), "fieldtype": "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"), + "reqd": 1 }, { "fieldname": "finance_book", @@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldname": "dimension", "label": __("Select Dimension"), "fieldtype": "Select", + "default": "Cost Center", "options": get_accounting_dimension_options(), "reqd": 1, }, @@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }); function get_accounting_dimension_options() { - let options =["", "Cost Center", "Project"]; + let options =["Cost Center", "Project"]; frappe.db.get_list('Accounting Dimension', {fields:['document_type']}).then((res) => { res.forEach((dimension) => { diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index c69bb3f70c51..c14f95243237 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -16,20 +16,22 @@ def execute(filters=None): + validate_filters(filters) - dimension_items_list = get_dimension_items_list(filters.dimension, filters.company) + dimension_list = get_dimensions(filters) - if not dimension_items_list: + if not dimension_list: return [], [] - dimension_items_list = [''.join(d) for d in dimension_items_list] - columns = get_columns(dimension_items_list) - data = get_data(filters, dimension_items_list) + # dimension_items_list = [''.join(d) for d in dimension_items_list] + columns = get_columns(dimension_list) + data = get_data(filters, dimension_list) return columns, data -def get_data(filters, dimension_items_list): +def get_data(filters, dimension_list): company_currency = erpnext.get_company_currency(filters.company) + acc = frappe.db.sql(""" select name, account_number, parent_account, lft, rgt, root_type, @@ -52,60 +54,54 @@ def get_data(filters, dimension_items_list): where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company)) gl_entries_by_account = {} - set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account) - format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list) - accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list) - out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list) + set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account) + format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, + frappe.scrub(filters.get('dimension'))) + accumulate_values_into_parents(accounts, accounts_by_name, dimension_list) + out = prepare_data(accounts, filters, company_currency, dimension_list) out = filter_out_zero_value_rows(out, parent_children_map) return out -def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account): - for item in dimension_items_list: - condition = get_condition(filters.from_date, item, filters.dimension) - if account: - condition += " and account in ({})"\ - .format(", ".join([frappe.db.escape(d) for d in account])) - - gl_filters = { - "company": filters.get("company"), - "from_date": filters.get("from_date"), - "to_date": filters.get("to_date"), - "finance_book": cstr(filters.get("finance_book")) - } +def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account): + condition = get_condition(filters.get('dimension')) - gl_filters['item'] = ''.join(item) + if account: + condition += " and account in ({})"\ + .format(", ".join([frappe.db.escape(d) for d in account])) - if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", - filters.company, 'default_finance_book') + gl_filters = { + "company": filters.get("company"), + "from_date": filters.get("from_date"), + "to_date": filters.get("to_date"), + "finance_book": cstr(filters.get("finance_book")) + } - for key, value in filters.items(): - if value: - gl_filters.update({ - key: value - }) + gl_filters['dimensions'] = set(dimension_list) - gl_entries = frappe.db.sql(""" + if filters.get("include_default_book_entries"): + gl_filters["company_fb"] = frappe.db.get_value("Company", + filters.company, 'default_finance_book') + + gl_entries = frappe.db.sql(""" select - posting_date, account, debit, credit, is_opening, fiscal_year, + posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {condition} + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and is_cancelled = 0 order by account, posting_date""".format( - condition=condition), - gl_filters, as_dict=True) #nosec + dimension = frappe.scrub(filters.get('dimension')), condition=condition), gl_filters, as_dict=True) #nosec - for entry in gl_entries: - entry['dimension_item'] = ''.join(item) - gl_entries_by_account.setdefault(entry.account, []).append(entry) + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) -def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list): +def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type): for entries in itervalues(gl_entries_by_account): for entry in entries: @@ -115,11 +111,12 @@ def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_l _("Could not retrieve information for {0}.").format(entry.account), title="Error", raise_exception=1 ) - for item in dimension_items_list: - if item == entry.dimension_item: - d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit) -def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list): + for dimension in dimension_list: + if dimension == entry.get(dimension_type): + d[frappe.scrub(dimension)] = d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit) + +def prepare_data(accounts, filters, company_currency, dimension_list): data = [] for d in accounts: @@ -136,13 +133,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen if d.account_number else d.account_name) } - for item in dimension_items_list: - row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3) + for dimension in dimension_list: + row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3) - if abs(row[frappe.scrub(item)]) >= 0.005: + if abs(row[frappe.scrub(dimension)]) >= 0.005: # ignore zero values has_value = True - total += flt(d.get(frappe.scrub(item), 0.0), 3) + total += flt(d.get(frappe.scrub(dimension), 0.0), 3) row["has_value"] = has_value row["total"] = total @@ -150,62 +147,55 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen return data -def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list): +def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - for item in dimension_items_list: - accounts_by_name[d.parent_account][frappe.scrub(item)] = \ - accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0) + for dimension in dimension_list: + accounts_by_name[d.parent_account][frappe.scrub(dimension)] = \ + accounts_by_name[d.parent_account].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0) -def get_condition(from_date, item, dimension): +def get_condition(dimension): conditions = [] - if from_date: - conditions.append("posting_date >= %(from_date)s") - if dimension: - if dimension not in ['Cost Center', 'Project']: - if dimension in ['Customer', 'Supplier']: - dimension = 'Party' - else: - dimension = 'Voucher No' - txt = "{0} = %(item)s".format(frappe.scrub(dimension)) - conditions.append(txt) + conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension))) return " and {}".format(" and ".join(conditions)) if conditions else "" -def get_dimension_items_list(dimension, company): - meta = frappe.get_meta(dimension, cached=False) - fieldnames = [d.fieldname for d in meta.get("fields")] - filters = {} - if 'company' in fieldnames: - filters['company'] = company - return frappe.get_all(dimension, filters, as_list=True) +def get_dimensions(filters): + meta = frappe.get_meta(filters.get('dimension'), cached=False) + query_filters = {} + + if meta.has_field('company'): + query_filters = {'company': filters.get('company')} -def get_columns(dimension_items_list, accumulated_values=1, company=None): + return frappe.get_all(filters.get('dimension'), filters=query_filters, pluck='name') + +def get_columns(dimension_list): columns = [{ "fieldname": "account", "label": _("Account"), "fieldtype": "Link", "options": "Account", "width": 300 + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1 }] - if company: - columns.append({ - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) - for item in dimension_items_list: + + for dimension in dimension_list: columns.append({ - "fieldname": frappe.scrub(item), - "label": item, + "fieldname": frappe.scrub(dimension), + "label": dimension, "fieldtype": "Currency", "options": "currency", "width": 150 }) + columns.append({ "fieldname": "total", "label": "Total", From fe88cfb80d06fda941afc733eec7efe3dd2c12dc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Mar 2022 12:57:12 +0530 Subject: [PATCH 142/148] fix: Remove comments (cherry picked from commit cab69fe1f298b44613be21c06591e868c24ee249) --- .../dimension_wise_accounts_balance_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index c14f95243237..90992ac744cd 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -23,7 +23,6 @@ def execute(filters=None): if not dimension_list: return [], [] - # dimension_items_list = [''.join(d) for d in dimension_items_list] columns = get_columns(dimension_list) data = get_data(filters, dimension_list) From 9e8b8305e9c654efe7e4fae3c9a1a1d9df49620f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Mar 2022 18:20:11 +0530 Subject: [PATCH 143/148] fix: Error in bank reco statement (cherry picked from commit fbcb413d9689222442f56197ab14bc17bd9b72bb) --- .../bank_reconciliation_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b72d2669775d..fe644ed3569a 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -267,7 +267,7 @@ def get_loan_amount(filters): total_amount += flt(amount) - return amount + return total_amount def get_balance_row(label, amount, account_currency): if amount > 0: From c0183289f548d3f2f73a0ee0b2720d41008d54eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Mar 2022 09:02:04 +0530 Subject: [PATCH 144/148] fix: Validate income/expense account in sales and purchase invoice (cherry picked from commit 06936cf1c024ffa4ed471790b8859aeadc5c5575) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 ++++++ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 +++++++ erpnext/controllers/accounts_controller.py | 6 +++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f3452e1cf816..1c1d913886f0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.buying_controller import BuyingController from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( @@ -105,6 +106,7 @@ def validate(self): self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "stock_qty") self.set_expense_account(for_validate=True) + self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") @@ -309,6 +311,10 @@ def set_expense_account(self, for_validate=False): elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) + def validate_expense_account(self): + for item in self.get('items'): + validate_account_head(item.idx, item.expense_account, self.company, 'Expense') + def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f1e64c7d020c..0186b3b9366b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -38,6 +38,7 @@ get_gl_entries_on_asset_regain, make_depreciation_entry, ) +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data @@ -113,6 +114,8 @@ def validate(self): self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() + self.validate_income_account() + validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference) if cint(self.is_pos): @@ -180,6 +183,10 @@ def validate_item_cost_centers(self): if cost_center_company != self.company: frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company))) + def validate_income_account(self): + for item in self.get('items'): + validate_account_head(item.idx, item.income_account, self.company, 'Income') + def set_tax_withholding(self): tax_withholding_details = get_party_tax_withholding_details(self) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f146071c0bda..23449082b89d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1567,12 +1567,12 @@ def validate_taxes_and_charges(tax): tax.rate = None -def validate_account_head(idx, account, company): +def validate_account_head(idx, account, company, context=''): account_company = frappe.get_cached_value('Account', account, 'company') if account_company != company: - frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) + frappe.throw(_('Row {0}: {3} Account {1} does not belong to Company {2}') + .format(idx, frappe.bold(account), frappe.bold(company), context), title=_('Invalid Account')) def validate_cost_center(tax, doc): From 0269bc9191bf82c45d58af77360bab0480c1e172 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Mar 2022 18:00:39 +0530 Subject: [PATCH 145/148] fix: Test case (cherry picked from commit 4237e5d9283023bc41e6efb147140750d1075b17) --- .../test_deferred_revenue_and_expense.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 86eb2134fe82..17475a77dbf0 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -88,10 +88,12 @@ def test_deferred_revenue(self): posting_date="2021-05-01", parent_cost_center="Main - _CD", cost_center="Main - _CD", - do_not_submit=True, + do_not_save=True, rate=300, price_list_rate=300, ) + + si.items[0].income_account = "Sales - _CD" si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2021-05-01" si.items[0].service_end_date = "2021-08-01" @@ -269,11 +271,13 @@ def test_zero_months(self): posting_date="2021-05-01", parent_cost_center="Main - _CD", cost_center="Main - _CD", - do_not_submit=True, + do_not_save=True, rate=300, price_list_rate=300, ) + si.items[0].enable_deferred_revenue = 1 + si.items[0].income_account = "Sales - _CD" si.items[0].deferred_revenue_account = deferred_revenue_account si.items[0].income_account = "Sales - _CD" si.save() From ec79e13b610167c3e37b0c30a8ba41e3b63a8c7c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 18 Mar 2022 17:32:09 +0530 Subject: [PATCH 146/148] fix: respect db multi_tenancy while fetching precision (#30301) (#30303) [skip ci] (cherry picked from commit 5a9bf9ffd64ffba14cb4bff63431fff319a82ed7) Co-authored-by: Ankush Menat --- erpnext/stock/report/stock_ageing/stock_ageing.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 97a740e18448..7ca40033edb8 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -12,7 +12,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict -precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] @@ -30,6 +30,8 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li _func = itemgetter(1) data = [] + precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 details = item_dict["details"] @@ -76,6 +78,9 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: + + precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + range1 = range2 = range3 = above_range3 = 0.0 for item in fifo_queue: From 230d171c5831ece7a2a5225046b06ef1cd232c8a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 20 Mar 2022 15:07:13 +0530 Subject: [PATCH 147/148] chore: add change log for v13.23.0 --- erpnext/change_log/v13/v13_23_0.md | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 erpnext/change_log/v13/v13_23_0.md diff --git a/erpnext/change_log/v13/v13_23_0.md b/erpnext/change_log/v13/v13_23_0.md new file mode 100644 index 000000000000..2c87bb9f6736 --- /dev/null +++ b/erpnext/change_log/v13/v13_23_0.md @@ -0,0 +1,63 @@ +## Version 13.23.0 Release Notes + +### Features & Enhancements + +- feat: Bank Reconciliation for loan documents (backport #29865) ([#29865](https://github.com/frappe/erpnext/pull/29865)) +- feat: Include child item group products in Item Group Page & cleanup (backport #30091) ([#30091](https://github.com/frappe/erpnext/pull/30091)) +- refactor: Employee Leave Balance (backport #29439) ([#29439](https://github.com/frappe/erpnext/pull/29439)) + +### Fixes + +- perf(asset): fetch only distinct depreciable assets (backport #30114) ([#30114](https://github.com/frappe/erpnext/pull/30114)) +- fix(pos): do not reset mode of payments in case of consolidation (backport #30198) ([#30198](https://github.com/frappe/erpnext/pull/30198)) +- fix(pos): loyalty points in case of returned pos invoice (backport #30242) ([#30242](https://github.com/frappe/erpnext/pull/30242)) +- fix(psoa): add company filter to account (backport #30145) ([#30145](https://github.com/frappe/erpnext/pull/30145)) +- fix(psoa): no such element: dict object['account'] (backport #30153) ([#30153](https://github.com/frappe/erpnext/pull/30153)) +- fix(translation) - correction for assets translation (backport #30000) ([#30000](https://github.com/frappe/erpnext/pull/30000)) +- fix(ux): Improve label for better understanding (backport #30096) ([#30096](https://github.com/frappe/erpnext/pull/30096)) +- fix: 'save_quotations_as_draft' checkbox not honoured (backport #30160) ([#30160](https://github.com/frappe/erpnext/pull/30160)) +- fix: Add missing currency option in Supplier Quotation's `rounded_total` field (backport #30229) ([#30229](https://github.com/frappe/erpnext/pull/30229)) +- fix: Ambigous column in picklist query (backport #30102) ([#30102](https://github.com/frappe/erpnext/pull/30102)) +- fix: Cleanup and fixes in Dimension-wise Accounts Balance Report (backport #30284) ([#30284](https://github.com/frappe/erpnext/pull/30284)) +- fix: Do not consider cancelled entries (backport #30207) ([#30207](https://github.com/frappe/erpnext/pull/30207)) +- fix: Do not update ignore pricing rule check implicitly (backport #30269) ([#30269](https://github.com/frappe/erpnext/pull/30269)) +- fix: Error in bank reconciliation statement (backport #30261) ([#30261](https://github.com/frappe/erpnext/pull/30261)) +- fix: Get MRs that are yet to be received but fully ordered in Report `Requested Items to Order and Receive` (backport #29987) ([#29987](https://github.com/frappe/erpnext/pull/29987)) +- fix: Item discounts for quotation and other docs (backport #29940) ([#29940](https://github.com/frappe/erpnext/pull/29940)) +- fix: Item-wise sales history report (backport #30080) ([#30080](https://github.com/frappe/erpnext/pull/30080)) +- fix: Job Card sub operations status and list view ([#30243](https://github.com/frappe/erpnext/pull/30243)) +- fix: KSA E-Invoice QR Code showing wrong VAT amount (backport #30230) ([#30230](https://github.com/frappe/erpnext/pull/30230)) +- fix: Leave Policy Assignment creation patch ([#30215](https://github.com/frappe/erpnext/pull/30215)) +- fix: Multi-currency bank reconciliation fixes (backport #29979) ([#29979](https://github.com/frappe/erpnext/pull/29979)) +- fix: Multiple fixes in Gross Profit report (backport #29740) ([#29740](https://github.com/frappe/erpnext/pull/29740)) +- fix: Nil and Exempted values in GSTR-3B Report (backport #30039) ([#30039](https://github.com/frappe/erpnext/pull/30039)) +- fix: Non Profit fixes ([#30280](https://github.com/frappe/erpnext/pull/30280)) +- fix: Pos return payment mode issue ([#26872](https://github.com/frappe/erpnext/pull/26872)) +- fix: Remove tax invoice no field (backport #30136) ([#30136](https://github.com/frappe/erpnext/pull/30136)) +- fix: Sales and Purchase return optimization (backport #30152) ([#30152](https://github.com/frappe/erpnext/pull/30152)) +- fix: Search query of payroll entry reference in Journal Entry ([#30225](https://github.com/frappe/erpnext/pull/30225)) +- fix: Shipping rule application fixes (backport #30181) ([#30181](https://github.com/frappe/erpnext/pull/30181)) +- fix: Sub-Category Routing in Item Group Page Listing pills (backport #30258) ([#30258](https://github.com/frappe/erpnext/pull/30258)) +- fix: Validate income/expense account in sales and purchase invoice (backport #30266) ([#30266](https://github.com/frappe/erpnext/pull/30266)) +- fix: cannot create multicurrency sales order with product bundles (backport #30166) ([#30166](https://github.com/frappe/erpnext/pull/30166)) +- fix: cannot create purchase order from sales order (backport #30217) ([#30217](https://github.com/frappe/erpnext/pull/30217)) +- fix: customer credit limit validation on update (backport #30110) ([#30110](https://github.com/frappe/erpnext/pull/30110)) +- fix: disable "get items" buttons from submitted production plan (backport #30223) ([#30223](https://github.com/frappe/erpnext/pull/30223)) +- fix: do not reset asset_category (backport #29696) ([#29696](https://github.com/frappe/erpnext/pull/29696)) +- fix: dont fetch entire barcode table in get_item_details ([#30131](https://github.com/frappe/erpnext/pull/30131)) +- fix: dont reset UOM in MR on every get_item_detail call (backport #30164) ([#30164](https://github.com/frappe/erpnext/pull/30164)) +- fix: fetch new fields from routing to bom ([#30169](https://github.com/frappe/erpnext/pull/30169)) +- fix: filter default_discount_account field in item_group_defaults table is_group = 0 ([#30095](https://github.com/frappe/erpnext/pull/30095)) +- fix: ignore non-unique swift numbers while migrating (backport #30132) ([#30132](https://github.com/frappe/erpnext/pull/30132)) +- fix: incorrect debit credit amount in presentation currency (backport #30244) ([#30244](https://github.com/frappe/erpnext/pull/30244)) +- fix: leave allocation records query ([#30118](https://github.com/frappe/erpnext/pull/30118)) +- fix: max_qty validation condition in WO (backport #30216) ([#30216](https://github.com/frappe/erpnext/pull/30216)) +- fix: pos return payment mode issue (#26872) ([#30220](https://github.com/frappe/erpnext/pull/30220)) +- fix: program enrollment button labels (backport #30148) ([#30148](https://github.com/frappe/erpnext/pull/30148)) +- fix: respect db multi_tenancy while fetching precision (backport #30301) ([#30301](https://github.com/frappe/erpnext/pull/30301)) +- fix: salary slip amount rounding errors ([#30248](https://github.com/frappe/erpnext/pull/30248)) +- fix: packed items return when items are removed ([#30263](https://github.com/frappe/erpnext/pull/30263)) +- fix: wrong payment days in salary slip for employees joining/leaving during mid payroll dates (backport #29082) ([#29082](https://github.com/frappe/erpnext/pull/29082)) +- refactor: removed unrequired code and test for standalone delivery note serial return ([#30276](https://github.com/frappe/erpnext/pull/30276)) +- revert: BU Schlüssel (a21f76f) (backport #29654) ([#29654](https://github.com/frappe/erpnext/pull/29654)) +- chore: add German translations (backport #30265) ([#30265](https://github.com/frappe/erpnext/pull/30265)) From 5ab6e3fc85d8e5103a7cd5f9b77b1455f4942930 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Mar 2022 10:31:24 +0530 Subject: [PATCH 148/148] chore: bump version --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 90a9a21edd32..76762540148e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ from erpnext.hooks import regional_overrides -__version__ = '13.22.1' +__version__ = '13.23.0' def get_default_company(user=None): '''Get default company for user'''