From 035eaa5e40e64df45259f834ab49798d7367f43e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 5 Sep 2023 15:24:59 +0530 Subject: [PATCH 01/20] test: tds payable monthly --- .../test_tds_payable_monthly.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py new file mode 100644 index 00000000000..d918d9e898b --- /dev/null +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -0,0 +1,116 @@ +# 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.doctype.cost_center.test_cost_center import create_cost_center +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import ( + create_tax_withholding_category, +) +from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import execute +from erpnext.accounts.utils import get_fiscal_year + + +class TestTdsPayableMonthly(FrappeTestCase): + def setUp(self): + delete_docs() + create_tax_accounts() + create_tax_categories() + + def test_tax_withholding_for_customers(self): + si = create_sales_invoice(rate=1000) + pe = create_tcs_payment_entry() + filters = frappe._dict( + company="_Test Company", party_type="Customer", from_date=today(), to_date=today() + ) + result = execute(filters)[1] + expected_values = [ + [pe.name, "TCS", 0.075, 2550, 0.9, 2550.9], + [si.name, "TCS", 0.075, 1000, 0.9, 1000.9], + ] + self.check_expected_values(result, expected_values) + + def check_expected_values(self, result, expected_values): + for i in range(len(result)): + voucher = frappe._dict(result[i]) + voucher_expected_values = expected_values[i] + self.assertEqual(voucher.ref_no, voucher_expected_values[0]) + self.assertEqual(voucher.section_code, voucher_expected_values[1]) + self.assertEqual(voucher.rate, voucher_expected_values[2]) + self.assertEqual(voucher.base_total, voucher_expected_values[3]) + self.assertEqual(voucher.tax_amount, voucher_expected_values[4]) + self.assertEqual(voucher.grand_total, voucher_expected_values[5]) + + def tearDown(self): + delete_docs() + + +def delete_docs(): + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company'") + frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company'") + + +def create_tax_accounts(): + account_names = ["TCS", "TDS"] + for account in account_names: + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": account, + "parent_account": "Duties and Taxes - _TC", + "report_type": "Balance Sheet", + "root_type": "Liability", + } + ).insert(ignore_if_duplicate=True) + + +def create_tax_categories(): + fiscal_year = get_fiscal_year(today(), company="_Test Company") + from_date = fiscal_year[1] + to_date = fiscal_year[2] + + tax_category = create_tax_withholding_category( + category_name="TCS", + rate=0.075, + from_date=from_date, + to_date=to_date, + account="TCS - _TC", + cumulative_threshold=300, + ) + + customer = frappe.get_doc("Customer", "_Test Customer") + customer.tax_withholding_category = "TCS" + customer.save() + + +def create_tcs_payment_entry(): + payment_entry = create_payment_entry( + payment_type="Receive", + party_type="Customer", + party="_Test Customer", + paid_from="Debtors - _TC", + paid_to="Cash - _TC", + paid_amount=2550, + ) + + payment_entry.append( + "taxes", + { + "account_head": "TCS - _TC", + "charge_type": "Actual", + "tax_amount": 0.9, + "add_deduct_tax": "Add", + "description": "Test", + "cost_center": "Main - _TC", + }, + ) + payment_entry.submit() + return payment_entry From dbeb132688b958f96531977861880641499b747e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 5 Sep 2023 17:33:30 +0530 Subject: [PATCH 02/20] refactor: use accounts mixin --- .../test_tds_payable_monthly.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index d918d9e898b..89ecef1904c 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -13,14 +13,16 @@ from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_cate create_tax_withholding_category, ) from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year -class TestTdsPayableMonthly(FrappeTestCase): +class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase): def setUp(self): - delete_docs() + self.create_company() + self.clear_old_entries() create_tax_accounts() - create_tax_categories() + create_tcs_category() def test_tax_withholding_for_customers(self): si = create_sales_invoice(rate=1000) @@ -30,8 +32,8 @@ class TestTdsPayableMonthly(FrappeTestCase): ) result = execute(filters)[1] expected_values = [ - [pe.name, "TCS", 0.075, 2550, 0.9, 2550.9], - [si.name, "TCS", 0.075, 1000, 0.9, 1000.9], + [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], + [si.name, "TCS", 0.075, 1000, 0.53, 1000.53], ] self.check_expected_values(result, expected_values) @@ -47,14 +49,7 @@ class TestTdsPayableMonthly(FrappeTestCase): self.assertEqual(voucher.grand_total, voucher_expected_values[5]) def tearDown(self): - delete_docs() - - -def delete_docs(): - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company'") - frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company'") - frappe.db.sql("delete from `tabGL Entry` where company='_Test Company'") - frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company'") + self.clear_old_entries() def create_tax_accounts(): @@ -72,7 +67,7 @@ def create_tax_accounts(): ).insert(ignore_if_duplicate=True) -def create_tax_categories(): +def create_tcs_category(): fiscal_year = get_fiscal_year(today(), company="_Test Company") from_date = fiscal_year[1] to_date = fiscal_year[2] @@ -106,7 +101,7 @@ def create_tcs_payment_entry(): { "account_head": "TCS - _TC", "charge_type": "Actual", - "tax_amount": 0.9, + "tax_amount": 0.53, "add_deduct_tax": "Add", "description": "Test", "cost_center": "Main - _TC", From fe69d5364dd03dd2e916e0c3385f3d4e6e0163b7 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 1 Sep 2023 04:58:59 +0000 Subject: [PATCH 03/20] fix: `company` is ambiguous (cherry picked from commit 3e1065a56164f8bb01d4361d378fa5d0a6130372) --- .../item_wise_purchase_register/item_wise_purchase_register.py | 2 +- .../report/item_wise_sales_register/item_wise_sales_register.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 050e6bc5d2f..ce1a62d0065 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -287,7 +287,7 @@ def get_conditions(filters): conditions = "" for opts in ( - ("company", " and company=%(company)s"), + ("company", " and `tabPurchase Invoice`.company=%(company)s"), ("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"), ("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 4d24dd90762..19bb449cd94 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -332,7 +332,7 @@ def get_conditions(filters, additional_conditions=None): conditions = "" for opts in ( - ("company", " and company=%(company)s"), + ("company", " and `tabSales Invoice`.company=%(company)s"), ("customer", " and `tabSales Invoice`.customer = %(customer)s"), ("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"), From 4fede56d98f803ee4912fa80cea386135c378d2c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 06:45:58 +0000 Subject: [PATCH 04/20] fix: use primary key for link lookup (backport #36919) (#36978) fix: use primary key for link lookup (#36919) (cherry picked from commit 8ce6b8179e1e583a63dc47bd430dd7b9f1dd23aa) Co-authored-by: Devin Slauenwhite --- erpnext/stock/report/stock_balance/stock_balance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 7e81a72028f..80bf8508cf3 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -323,8 +323,10 @@ class StockBalanceReport(object): for field in ["item_code", "brand"]: if not self.filters.get(field): continue - - query = query.where(item_table[field] == self.filters.get(field)) + elif field == "item_code": + query = query.where(item_table.name == self.filters.get(field)) + else: + query = query.where(item_table[field] == self.filters.get(field)) return query From f251d6cb699cf403e559e37276ebcca01498291e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Sep 2023 15:18:21 +0530 Subject: [PATCH 05/20] fix: Update party type for payroll payable account --- .../doctype/loan_repayment/loan_repayment.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d7e11aafa81..c4bacda4321 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -405,6 +405,16 @@ class LoanRepayment(AccountsController): else: payment_account = self.payment_account + payment_party_type = "" + payment_party = "" + + if ( + hasattr(self, "process_payroll_accounting_entry_based_on_employee") + and self.process_payroll_accounting_entry_based_on_employee + ): + payment_party_type = "Employee" + payment_party = self.applicant + if self.total_penalty_paid: gle_map.append( self.get_gl_dict( @@ -452,6 +462,8 @@ class LoanRepayment(AccountsController): "remarks": _(remarks), "cost_center": self.cost_center, "posting_date": getdate(self.posting_date), + "party_type": payment_party_type, + "party": payment_party, } ) ) From e210b28f0d2fbac90faaa92d0ca9645545b0637b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:39:46 +0530 Subject: [PATCH 06/20] chore: asset finance books validation (backport #36979) (#36981) * chore: asset finance books validation (#36979) (cherry picked from commit 0077659e931063653af415b9857b4f61bfcf227d) * chore: fix tests --------- Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.py | 22 ++++++++++++++++++++++ erpnext/assets/doctype/asset/test_asset.py | 14 ++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 34d5430210c..2647ed7b895 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -40,6 +40,7 @@ class Asset(AccountsController): self.validate_item() self.validate_cost_center() self.set_missing_values() + self.validate_finance_books() if not self.split_from: self.prepare_depreciation_data() self.validate_gross_and_purchase_amount() @@ -206,6 +207,27 @@ class Asset(AccountsController): finance_books = get_item_details(self.item_code, self.asset_category) self.set("finance_books", finance_books) + def validate_finance_books(self): + if not self.calculate_depreciation or len(self.finance_books) == 1: + return + + finance_books = set() + + for d in self.finance_books: + if d.finance_book in finance_books: + frappe.throw( + _("Row #{}: Please use a different Finance Book.").format(d.idx), + title=_("Duplicate Finance Book"), + ) + else: + finance_books.add(d.finance_book) + + if not d.finance_book: + frappe.throw( + _("Row #{}: Finance Book should not be empty since you're using multiple.").format(d.idx), + title=_("Missing Finance Book"), + ) + def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a2826d929b8..87e74712d5b 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1332,6 +1332,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, @@ -1342,6 +1343,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, @@ -1352,6 +1354,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 3", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1381,6 +1384,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1391,6 +1395,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 6, @@ -1647,6 +1652,15 @@ def create_asset_data(): if not frappe.db.exists("Location", "Test Location"): frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + if not frappe.db.exists("Finance Book", "Test Finance Book 1"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 2"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 3"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert() + def create_asset(**args): args = frappe._dict(args) From 1894371b684cefba1210d333a9adce1bdc1a8029 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Sep 2023 16:29:12 +0530 Subject: [PATCH 07/20] chore: Update function --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c4bacda4321..4171ca68305 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -502,6 +502,7 @@ def create_repayment_entry( amount_paid, penalty_amount=None, payroll_payable_account=None, + process_payroll_accounting_entry_based_on_employee=0, ): lr = frappe.get_doc( @@ -518,6 +519,7 @@ def create_repayment_entry( "amount_paid": amount_paid, "loan_type": loan_type, "payroll_payable_account": payroll_payable_account, + "process_payroll_accounting_entry_based_on_employee": process_payroll_accounting_entry_based_on_employee, } ).insert() From 24a481525019c9ed91eb47b919df9c855c56d843 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Sep 2023 13:19:00 +0530 Subject: [PATCH 08/20] chore: Update employee for tests (cherry picked from commit ae01d70b337795215c8738341780dbe24d275c46) --- erpnext/setup/doctype/employee/test_employee.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index 071c336326f..5a693c5effb 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -66,5 +66,8 @@ def make_employee(user, company=None, **kwargs): employee.insert() return employee.name else: - frappe.db.set_value("Employee", {"employee_name": user}, "status", "Active") - return frappe.get_value("Employee", {"employee_name": user}, "name") + employee = frappe.get_doc("Employee", {"employee_name": user}) + employee.update(kwargs) + employee.status = "Active" + employee.save() + return employee.name From 2ae4463b7692704a214fceec5d67cd6c32b67af3 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 6 Sep 2023 22:20:29 +0530 Subject: [PATCH 09/20] fix: correct asset daily depr schedule calculation [v14] (#36991) fix: correct asset daily depr schedule calculation --- erpnext/assets/doctype/asset/asset.py | 76 +++++++++++++++------- erpnext/assets/doctype/asset/test_asset.py | 24 +++---- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2647ed7b895..e72ddcb12a0 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1385,26 +1385,41 @@ def get_straight_line_or_manual_depr_amount( daily_depr_amount = ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) ) / date_diff( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) - * row.frequency_of_depreciation, - ), - add_months( - row.depreciation_start_date, - flt( - row.total_number_of_depreciations - - asset.number_of_depreciations_booked - - number_of_pending_depreciations + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, ) - * row.frequency_of_depreciation, + ), + add_days( + get_last_day( + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + - 1 + ) + * row.frequency_of_depreciation, + ) + ), + 1, ), ) - to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) - from_date = add_months( - row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + + to_date = get_last_day( + add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) ) - return daily_depr_amount * date_diff(to_date, from_date) + from_date = add_days( + get_last_day( + add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + ), + 1, + ) + + return daily_depr_amount * (date_diff(to_date, from_date) + 1) else: return ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) @@ -1417,18 +1432,29 @@ def get_straight_line_or_manual_depr_amount( - flt(asset.opening_accumulated_depreciation) - flt(row.expected_value_after_useful_life) ) / date_diff( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) - * row.frequency_of_depreciation, + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, + ) + ), + add_days( + get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1 ), - row.depreciation_start_date, ) - to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) - from_date = add_months( - row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + + to_date = get_last_day( + add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) ) - return daily_depr_amount * date_diff(to_date, from_date) + from_date = add_days( + get_last_day( + add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + ), + 1, + ) + + return daily_depr_amount * (date_diff(to_date, from_date) + 1) else: return ( flt(asset.gross_purchase_amount) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 87e74712d5b..1adbeed65a5 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -743,18 +743,18 @@ class TestDepreciationMethods(AssetSetup): ) expected_schedules = [ - ["2023-01-31", 1019.18, 1019.18], - ["2023-02-28", 920.55, 1939.73], - ["2023-03-31", 1019.18, 2958.91], - ["2023-04-30", 986.3, 3945.21], - ["2023-05-31", 1019.18, 4964.39], - ["2023-06-30", 986.3, 5950.69], - ["2023-07-31", 1019.18, 6969.87], - ["2023-08-31", 1019.18, 7989.05], - ["2023-09-30", 986.3, 8975.35], - ["2023-10-31", 1019.18, 9994.53], - ["2023-11-30", 986.3, 10980.83], - ["2023-12-31", 1019.17, 12000.0], + ["2023-01-31", 1021.98, 1021.98], + ["2023-02-28", 923.08, 1945.06], + ["2023-03-31", 1021.98, 2967.04], + ["2023-04-30", 989.01, 3956.05], + ["2023-05-31", 1021.98, 4978.03], + ["2023-06-30", 989.01, 5967.04], + ["2023-07-31", 1021.98, 6989.02], + ["2023-08-31", 1021.98, 8011.0], + ["2023-09-30", 989.01, 9000.01], + ["2023-10-31", 1021.98, 10021.99], + ["2023-11-30", 989.01, 11011.0], + ["2023-12-31", 989.0, 12000.0], ] schedules = [ From 2077b2cde413a66347b3b72e201a2702d7f6c0b6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 28 Aug 2023 13:09:51 +0530 Subject: [PATCH 10/20] fix: show letterhead and terms for AR pdf (cherry picked from commit 0a9187ea422f2312ffd9508f1f3c6a120c228f71) --- ...ement_of_accounts_accounts_receivable.html | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html index 259526f8c43..647600a9fea 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html @@ -8,9 +8,24 @@ } +
+ {% if letter_head.content %} +
{{ letter_head.content }}
+
+ {% endif %} +
+ +

{{ _(report.report_name) }}

- {{ filters.customer }} + {{ filters.customer_name }}

{% if (filters.tax_id) %} @@ -341,4 +356,9 @@ {% endif %} + {% if terms_and_conditions %} +
+ {{ terms_and_conditions }} +
+ {% endif %}

{{ _("Printed On ") }}{{ frappe.utils.now() }}

From 657ca7ff22602ab34acc25c3c2a2d7bcd2ba25e9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 28 Aug 2023 13:11:28 +0530 Subject: [PATCH 11/20] feat: add field for specifying pdf name (cherry picked from commit 5c2a949593727ec6c9f84f8b801035e4da01558d) # Conflicts: # erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py --- .../process_statement_of_accounts.json | 10 ++++++++-- .../process_statement_of_accounts.py | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index 45373741f39..e711ae0de2b 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -49,6 +49,7 @@ "column_break_21", "start_date", "section_break_33", + "pdf_name", "subject", "column_break_28", "cc_to", @@ -273,7 +274,7 @@ "fieldname": "help_text", "fieldtype": "HTML", "label": "Help Text", - "options": "
\n

Note

\n
    \n
  • \nYou can use Jinja tags in Subject and Body fields for dynamic values.\n
  • \n All fields in this doctype are available under the doc object and all fields for the customer to whom the mail will go to is available under the customer object.\n
\n

Examples

\n\n
    \n
  • Subject:

    Statement Of Accounts for {{ customer.name }}

  • \n
  • Body:

    \n
    Hello {{ customer.name }},
    PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.
  • \n
\n" + "options": "
\n

Note

\n
    \n
  • \nYou can use Jinja tags in Subject and Body fields for dynamic values.\n
  • \n All fields in this doctype are available under the doc object and all fields for the customer to whom the mail will go to is available under the customer object.\n
\n

Examples

\n\n
    \n
  • Subject:

    Statement Of Accounts for {{ customer.customer_name }}

  • \n
  • Body:

    \n
    Hello {{ customer.customer_name }},
    PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.
  • \n
\n" }, { "fieldname": "subject", @@ -368,10 +369,15 @@ "fieldname": "based_on_payment_terms", "fieldtype": "Check", "label": "Based On Payment Terms" + }, + { + "fieldname": "pdf_name", + "fieldtype": "Data", + "label": "PDF Name" } ], "links": [], - "modified": "2023-06-23 10:13:15.051950", + "modified": "2023-08-28 12:59:53.071334", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 6cd601f663d..b9ea2142958 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -26,7 +26,13 @@ class ProcessStatementOfAccounts(Document): if not self.subject: self.subject = "Statement Of Accounts for {{ customer.customer_name }}" if not self.body: - self.body = "Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." + if self.report == "General Ledger": + body_str = " from {{ doc.from_date }} to {{ doc.to_date }}." + else: + body_str = " until {{ doc.posting_date }}." + self.body = "Hello {{ customer.customer_name }},
PFA your Statement Of Accounts" + body_str + if not self.pdf_name: + self.pdf_name = "{{ customer.customer_name }}" validate_template(self.subject) validate_template(self.body) @@ -139,6 +145,7 @@ def get_ar_filters(doc, entry): return { "report_date": doc.posting_date if doc.posting_date else None, "customer": entry.customer, + "customer_name": entry.customer_name if entry.customer_name else None, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "sales_partner": doc.sales_partner if doc.sales_partner else None, "sales_person": doc.sales_person if doc.sales_person else None, @@ -368,10 +375,18 @@ def send_emails(document_name, from_scheduler=False): if report: for customer, report_pdf in report.items(): - attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}] + context = get_context(customer, doc) + filename = frappe.render_template(doc.pdf_name, context) + attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) +<<<<<<< HEAD context = get_context(customer, doc) +======= + if not recipients: + continue + +>>>>>>> 5c2a949593 (feat: add field for specifying pdf name) subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) From 53270dd933d1e1ab22787de4ecbab6894810c5e8 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 28 Aug 2023 17:14:15 +0530 Subject: [PATCH 12/20] fix: generate pdf only when result exists (cherry picked from commit f07f4ce86f5963d03ea41ab699df2c4ce53ef2c5) --- .../process_statement_of_accounts.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index b9ea2142958..6c8afbecb5f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -63,11 +63,6 @@ def get_report_pdf(doc, consolidated=True): filters = get_common_filters(doc) - if doc.report == "General Ledger": - filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) - else: - filters.update(get_ar_filters(doc, entry)) - if doc.report == "General Ledger": col, res = get_soa(filters) for x in [0, -2, -1]: @@ -75,8 +70,11 @@ def get_report_pdf(doc, consolidated=True): if len(res) == 3: continue else: + filters.update(get_ar_filters(doc, entry)) ar_res = get_ar_soa(filters) col, res = ar_res[0], ar_res[1] + if not res: + continue statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing) From f9f1ac3601a8ddb3c6c56bdc97d3b718f7c04d51 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 29 Aug 2023 14:51:35 +0530 Subject: [PATCH 13/20] test: auto email for ar report (cherry picked from commit a006b66e45c1b0c192674ad522b05f5565513e35) --- .../process_statement_of_accounts.py | 9 +++-- .../test_process_statement_of_accounts.py | 37 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 6c8afbecb5f..ef6ba3e3ab1 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -367,7 +367,7 @@ def download_statements(document_name): @frappe.whitelist() -def send_emails(document_name, from_scheduler=False): +def send_emails(document_name, from_scheduler=False, posting_date=None): doc = frappe.get_doc("Process Statement Of Accounts", document_name) report = get_report_pdf(doc, consolidated=False) @@ -403,7 +403,7 @@ def send_emails(document_name, from_scheduler=False): ) if doc.enable_auto_email and from_scheduler: - new_to_date = getdate(today()) + new_to_date = getdate(posting_date or today()) if doc.frequency == "Weekly": new_to_date = add_days(new_to_date, 7) else: @@ -414,6 +414,8 @@ def send_emails(document_name, from_scheduler=False): ) doc.db_set("to_date", new_to_date, commit=True) doc.db_set("from_date", new_from_date, commit=True) + doc.db_set("posting_date", new_to_date, commit=True) + doc.db_set("report", doc.report, commit=True) return True else: return False @@ -423,7 +425,8 @@ def send_emails(document_name, from_scheduler=False): def send_auto_email(): selected = frappe.get_list( "Process Statement Of Accounts", - filters={"to_date": format_date(today()), "enable_auto_email": 1}, + filters={"enable_auto_email": 1}, + or_filters={"to_date": format_date(today()), "posting_date": format_date(today())}, ) for entry in selected: send_emails(entry.name, from_scheduler=True) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index c281040aaf2..fb0d8d152f0 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -1,9 +1,42 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_days, getdate, today + +from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import ( + send_emails, +) +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + class TestProcessStatementOfAccounts(unittest.TestCase): - pass + def setUp(self): + self.si = create_sales_invoice() + self.process_soa = create_process_soa() + + def test_auto_email_for_process_soa_ar(self): + send_emails(self.process_soa.name, from_scheduler=True) + self.process_soa.load_from_db() + self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7))) + + def tearDown(self): + frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + + +def create_process_soa(): + frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + process_soa = frappe.new_doc("Process Statement Of Accounts") + soa_dict = { + "name": "Test Process SOA", + "company": "_Test Company", + } + process_soa.update(soa_dict) + process_soa.set("customers", [{"customer": "_Test Customer"}]) + process_soa.enable_auto_email = 1 + process_soa.frequency = "Weekly" + process_soa.report = "Accounts Receivable" + process_soa.save() + return process_soa From 284181d766e4b321e0fc642b3230c1ce86338e37 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 29 Aug 2023 16:16:50 +0530 Subject: [PATCH 14/20] fix: remove report field db set (cherry picked from commit 060da2c5bc47300d8e7103af63626d0b53ce2807) --- .../process_statement_of_accounts.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index ef6ba3e3ab1..21708ab4ec6 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -412,10 +412,11 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): doc.add_comment( "Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()) ) - doc.db_set("to_date", new_to_date, commit=True) - doc.db_set("from_date", new_from_date, commit=True) - doc.db_set("posting_date", new_to_date, commit=True) - doc.db_set("report", doc.report, commit=True) + if doc.report == "General Ledger": + doc.db_set("to_date", new_to_date, commit=True) + doc.db_set("from_date", new_from_date, commit=True) + else: + doc.db_set("posting_date", new_to_date, commit=True) return True else: return False From acd9c692017d811f668e300fb0fedfb093df6458 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 10 Sep 2023 17:26:41 +0530 Subject: [PATCH 15/20] feat: Add half-yearly asset maintenance periodicity. (backport #37006) (#37014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: Add half-yearly asset maintenance periodicity. (#37006) (cherry picked from commit 846ae32d922bbff8a1a02719b615a36a1eaf1eaa) Co-authored-by: Bernd Oliver Sünderhauf <46800703+bosue@users.noreply.github.com> --- .../assets/doctype/asset_maintenance/asset_maintenance.py | 6 ++++-- .../asset_maintenance_task/asset_maintenance_task.json | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 83031415ec3..5c40072086e 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -80,14 +80,16 @@ def calculate_next_due_date( next_due_date = add_days(start_date, 7) if periodicity == "Monthly": next_due_date = add_months(start_date, 1) + if periodicity == "Quarterly": + next_due_date = add_months(start_date, 3) + if periodicity == "Half-yearly": + next_due_date = add_months(start_date, 6) if periodicity == "Yearly": next_due_date = add_years(start_date, 1) if periodicity == "2 Yearly": next_due_date = add_years(start_date, 2) if periodicity == "3 Yearly": next_due_date = add_years(start_date, 3) - if periodicity == "Quarterly": - next_due_date = add_months(start_date, 3) if end_date and ( (start_date and start_date >= end_date) or (last_completion_date and last_completion_date >= end_date) diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json index b7cb23e6687..80d90c63473 100644 --- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json +++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json @@ -71,7 +71,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Periodicity", - "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly\n2 Yearly\n3 Yearly", "reqd": 1 }, { @@ -153,4 +153,4 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} From 619644af04a44b38c0c4f5160b3c897ea2535181 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:25:30 +0530 Subject: [PATCH 16/20] chore: resolve conflicts --- .../process_statement_of_accounts.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 21708ab4ec6..d2249c2147f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -378,13 +378,9 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) -<<<<<<< HEAD - context = get_context(customer, doc) -======= if not recipients: continue - ->>>>>>> 5c2a949593 (feat: add field for specifying pdf name) + subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) From a35abf840397f02ababf4ab869464bf82709a98a Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:06:24 +0530 Subject: [PATCH 17/20] chore: linting issues --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index d2249c2147f..e1f32952205 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -380,7 +380,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): recipients, cc = get_recipients_and_cc(customer, doc) if not recipients: continue - + subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) From 21be889a771f815a720d44494ba2e6e8174adb83 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:56:20 +0530 Subject: [PATCH 18/20] fix(ux): docstatus filter for `Reference Name` in QI (backport #37024) (#37028) fix(ux): docstatus filter for `Reference Name` in QI (#37024) (cherry picked from commit d739ab6ee3165b0c661086e71ad78aa1c41f58a6) Co-authored-by: s-aga-r --- .../doctype/quality_inspection/quality_inspection.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index eea28791a9f..05fa2324dd4 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -6,6 +6,14 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { setup: function(frm) { + frm.set_query("reference_name", function() { + return { + filters: { + "docstatus": ["!=", 2], + } + } + }); + frm.set_query("batch_no", function() { return { filters: { From 66027877d3cdd7ac5de421d9c5b631fe07630f6a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:04:37 +0530 Subject: [PATCH 19/20] fix: `Parent Task` link with `Project Task` (backport #37025) (#37033) * feat: new field in `Task` to hold ref of Template Task (cherry picked from commit b4bcd9ba3f1bfcbd72867d24448ed14bda579dd4) # Conflicts: # erpnext/projects/doctype/task/task.json * fix: set `Template Task` ref in `Project Task` (cherry picked from commit d3295c43e3ba86b66d2085572f921f8af94e27d5) * fix: reload task before save (cherry picked from commit 5cae2e79bd1fcc6222ba08988af2c9d1353ede6c) * test: add test case for Task having common subject (cherry picked from commit 0d5c8f03bdcfb8f34bf389eb3c67f57302be6b75) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- erpnext/projects/doctype/project/project.py | 12 +++-- .../projects/doctype/project/test_project.py | 49 +++++++++++++++++-- erpnext/projects/doctype/task/task.json | 13 ++++- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index d80133c988a..082ba915207 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -84,6 +84,7 @@ class Project(Document): issue=task_details.issue, is_group=task_details.is_group, color=task_details.color, + template_task=task_details.name, ) ).insert() @@ -103,9 +104,13 @@ class Project(Document): return date def dependency_mapping(self, template_tasks, project_tasks): - for template_task in template_tasks: - project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] - project_task = frappe.get_doc("Task", project_task.name) + for project_task in project_tasks: + if project_task.get("template_task"): + template_task = frappe.get_doc("Task", project_task.template_task) + else: + template_task = list(filter(lambda x: x.subject == project_task.subject, template_tasks))[0] + template_task = frappe.get_doc("Task", template_task.name) + self.check_depends_on_value(template_task, project_task, project_tasks) self.check_for_parent_tasks(template_task, project_task, project_tasks) @@ -117,6 +122,7 @@ class Project(Document): filter(lambda x: x.subject == child_task_subject, project_tasks) ) if len(corresponding_project_task): + project_task.reload() # reload, as it might have been updated in the previous iteration project_task.append("depends_on", {"task": corresponding_project_task[0].name}) project_task.save() diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 8a599cef753..e49fecd1f47 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -1,9 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, nowdate from erpnext.projects.doctype.project_template.test_project_template import make_project_template @@ -15,7 +14,7 @@ test_records = frappe.get_test_records("Project") test_ignore = ["Sales Order"] -class TestProject(unittest.TestCase): +class TestProject(FrappeTestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): project_name = "Test Project with Template - No Parent and Dependend Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) @@ -155,6 +154,50 @@ class TestProject(unittest.TestCase): so.reload() self.assertFalse(so.project) + def test_project_with_template_tasks_having_common_name(self): + # Step - 1: Create Template Parent Tasks + template_parent_task1 = create_task(subject="Parent Task - 1", is_template=1, is_group=1) + template_parent_task2 = create_task(subject="Parent Task - 2", is_template=1, is_group=1) + template_parent_task3 = create_task(subject="Parent Task - 1", is_template=1, is_group=1) + + # Step - 2: Create Template Child Tasks + template_task1 = create_task( + subject="Task - 1", is_template=1, parent_task=template_parent_task1.name + ) + template_task2 = create_task( + subject="Task - 2", is_template=1, parent_task=template_parent_task2.name + ) + template_task3 = create_task( + subject="Task - 1", is_template=1, parent_task=template_parent_task3.name + ) + + # Step - 3: Create Project Template + template_tasks = [ + template_parent_task1, + template_task1, + template_parent_task2, + template_task2, + template_parent_task3, + template_task3, + ] + project_template = make_project_template( + "Project template with common Task Subject", template_tasks + ) + + # Step - 4: Create Project against the Project Template + project = get_project("Project with common Task Subject", project_template) + project_tasks = frappe.get_all( + "Task", {"project": project.name}, ["subject", "parent_task", "is_group"] + ) + + # Test - 1: No. of Project Tasks should be equal to No. of Template Tasks + self.assertEquals(len(project_tasks), len(template_tasks)) + + # Test - 2: All child Project Tasks should have Parent Task linked + for pt in project_tasks: + if not pt.is_group: + self.assertIsNotNone(pt.parent_task) + def get_project(name, template): diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 141a99e612b..33a8799f96c 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -52,13 +52,15 @@ "company", "lft", "rgt", - "old_parent" + "old_parent", + "template_task" ], "fields": [ { "fieldname": "subject", "fieldtype": "Data", "in_global_search": 1, + "in_list_view": 1, "in_standard_filter": 1, "label": "Subject", "reqd": 1, @@ -138,6 +140,7 @@ "fieldname": "parent_task", "fieldtype": "Link", "ignore_user_permissions": 1, + "in_list_view": 1, "label": "Parent Task", "options": "Task", "search_index": 1 @@ -382,6 +385,12 @@ "fieldtype": "Date", "label": "Completed On", "mandatory_depends_on": "eval: doc.status == \"Completed\"" + }, + { + "fieldname": "template_task", + "fieldtype": "Data", + "hidden": 1, + "label": "Template Task" } ], "icon": "fa fa-check", @@ -389,7 +398,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2022-06-23 16:58:47.005241", + "modified": "2023-09-06 13:52:05.861175", "modified_by": "Administrator", "module": "Projects", "name": "Task", From d278b116030df0f272c3c101b75fe3b66a344c04 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 12 Sep 2023 13:32:56 +0530 Subject: [PATCH 20/20] feat: provision to set required by from Production Plan (#37039) * feat: provision to set the Required By date from production plan * test: added test case for validate schedule_date --- .../material_request_plan_item.json | 41 ++++++++++++++----- .../production_plan/production_plan.py | 2 +- .../production_plan/test_production_plan.py | 12 +++++- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 09bf1d8a736..d07bf0fa66b 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -10,22 +10,25 @@ "warehouse", "item_name", "material_request_type", - "actual_qty", - "ordered_qty", + "quantity", "required_bom_qty", "column_break_4", - "quantity", + "schedule_date", "uom", "conversion_factor", - "projected_qty", - "reserved_qty_for_production", - "safety_stock", "item_details", "description", "min_order_qty", "section_break_8", "sales_order", - "requested_qty" + "bin_qty_section", + "actual_qty", + "requested_qty", + "reserved_qty_for_production", + "column_break_yhelv", + "ordered_qty", + "projected_qty", + "safety_stock" ], "fields": [ { @@ -65,7 +68,7 @@ "fieldtype": "Column Break" }, { - "columns": 1, + "columns": 2, "fieldname": "quantity", "fieldtype": "Float", "in_list_view": 1, @@ -80,12 +83,12 @@ "read_only": 1 }, { - "columns": 2, + "columns": 1, "default": "0", "fieldname": "actual_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Available Qty", + "label": "Qty In Stock", "no_copy": 1, "read_only": 1 }, @@ -176,11 +179,27 @@ "fieldtype": "Float", "label": "Conversion Factor", "read_only": 1 + }, + { + "columns": 1, + "fieldname": "schedule_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Required By" + }, + { + "fieldname": "bin_qty_section", + "fieldtype": "Section Break", + "label": "BIN Qty" + }, + { + "fieldname": "column_break_yhelv", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2023-05-03 12:43:29.895754", + "modified": "2023-09-12 12:09:08.358326", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a494550423f..795cb97fffa 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -725,7 +725,7 @@ class ProductionPlan(Document): # key for Sales Order:Material Request Type:Customer key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "") - schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) + schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days)) if not key in material_request_map: # make a new MR for the combination diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2871a29d768..f5778847b9c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2,7 +2,7 @@ # See license.txt import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_to_date, flt, now_datetime, nowdate +from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.production_plan import ( @@ -58,6 +58,9 @@ class TestProductionPlan(FrappeTestCase): pln = create_production_plan(item_code="Test Production Item 1") self.assertTrue(len(pln.mr_items), 2) + for row in pln.mr_items: + row.schedule_date = add_to_date(nowdate(), days=10) + pln.make_material_request() pln.reload() self.assertTrue(pln.status, "Material Requested") @@ -71,6 +74,13 @@ class TestProductionPlan(FrappeTestCase): self.assertTrue(len(material_requests), 2) + for row in material_requests: + mr_schedule_date = getdate(frappe.db.get_value("Material Request", row[0], "schedule_date")) + + expected_date = getdate(add_to_date(nowdate(), days=10)) + + self.assertEqual(mr_schedule_date, expected_date) + pln.make_work_order() work_orders = frappe.get_all( "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1