From 63aaa1e357280b24c537a502a479f7bb7a6654e4 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 12 Jan 2022 16:10:57 +0530 Subject: [PATCH 001/133] fix: consider returned_qty while updating billed_amt --- .../doctype/delivery_note/delivery_note.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 70d48a42d72..1573512fc65 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -335,15 +335,25 @@ class DeliveryNote(SellingController): def update_billed_amount_based_on_so(so_detail, update_modified=True): # Billed against Sales Order directly - billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail) + billed_against_so = frappe.db.sql("""select sum(si_item.amount) + from `tabSales Invoice Item` si_item, `tabSales Invoice` si + where + si_item.parent = si.name + and si_item.so_detail=%s + and (si_item.dn_detail is null or si_item.dn_detail = '') + and si_item.docstatus=1 + and si.update_stock = 0 + """, so_detail) billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row - dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent + dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name=dn_item.parent and dn_item.so_detail=%s - and dn.docstatus=1 and dn.is_return = 0 + where + dn.name = dn_item.parent + and dn_item.so_detail=%s + and dn.docstatus=1 + and dn.is_return = 0 order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1) updated_dn = [] @@ -362,7 +372,11 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): # Distribute billed amount directly against SO between DNs based on FIFO if billed_against_so and billed_amt_agianst_dn < dnd.amount: - pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn + if dnd.returned_qty: + pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty + else: + pending_to_bill = flt(dnd.amount) + pending_to_bill -= billed_amt_agianst_dn if pending_to_bill <= billed_against_so: billed_amt_agianst_dn += pending_to_bill billed_against_so -= pending_to_bill From 5de6b8dc4df407fd953efe69640e22bd4ea90b6e Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 12 Jan 2022 16:13:06 +0530 Subject: [PATCH 002/133] fix: check so_detail before dn_detail --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 772e8c4e872..5b6d3915119 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1256,14 +1256,14 @@ class SalesInvoice(SellingController): def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): - if d.dn_detail: + if d.so_detail: + updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) + elif d.dn_detail: billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` where dn_detail=%s and docstatus=1""", d.dn_detail) billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) updated_delivery_notes.append(d.delivery_note) - elif d.so_detail: - updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) From fc65a3d9895c8ba9de957da820ed6b59c6c1bcbd Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 14 Jan 2022 16:15:26 +0530 Subject: [PATCH 003/133] feat: add patch --- erpnext/patches.txt | 1 + .../v13_0/set_billed_amount_in_returned_dn.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 18319f70c47..e8619a00bb9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,3 +341,4 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning +erpnext.patches.v13_0.set_billed_amount_in_returned_dn \ No newline at end of file diff --git a/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py b/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py new file mode 100644 index 00000000000..1f86c76d14f --- /dev/null +++ b/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py @@ -0,0 +1,22 @@ +# Copyright (c) 2022, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so + + +def execute(): + dn_item = frappe.qb.DocType('Delivery Note Item') + + so_detail_list = (frappe.qb.from_(dn_item) + .select(dn_item.so_detail) + .where( + (dn_item.so_detail.notnull()) & + (dn_item.so_detail != '') & + (dn_item.docstatus == 1) & + (dn_item.returned_qty > 0) + )).run() + + for so_detail in so_detail_list: + update_billed_amount_based_on_so(so_detail[0], False) \ No newline at end of file From 0a9ec9f591f8b4d0e630a3c902b69c9996f080dd Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 14 Jan 2022 19:21:52 +0530 Subject: [PATCH 004/133] refactor: use frappe.qb instead of sql --- .../doctype/delivery_note/delivery_note.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1573512fc65..b9e1a420c1a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -334,27 +334,35 @@ class DeliveryNote(SellingController): frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) def update_billed_amount_based_on_so(so_detail, update_modified=True): + from frappe.query_builder.functions import Sum + # Billed against Sales Order directly - billed_against_so = frappe.db.sql("""select sum(si_item.amount) - from `tabSales Invoice Item` si_item, `tabSales Invoice` si - where - si_item.parent = si.name - and si_item.so_detail=%s - and (si_item.dn_detail is null or si_item.dn_detail = '') - and si_item.docstatus=1 - and si.update_stock = 0 - """, so_detail) + si = frappe.qb.DocType("Sales Invoice").as_("si") + si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") + sum_amount = Sum(si_item.amount).as_("amount") + + billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where( + (si_item.parent == si.name) & + (si_item.so_detail == so_detail) & + ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & + (si_item.docstatus == 1) & + (si.update_stock == 0) + ).run() billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row - dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where - dn.name = dn_item.parent - and dn_item.so_detail=%s - and dn.docstatus=1 - and dn.is_return = 0 - order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1) + + dn = frappe.qb.DocType("Delivery Note").as_("dn") + dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") + + dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where( + (dn.name == dn_item.parent) & + (dn_item.so_detail == so_detail) & + (dn.docstatus == 1) & + (dn.is_return == 0) + ).orderby( + dn.posting_date, dn.posting_time, dn.name + ).run(as_dict=True) updated_dn = [] for dnd in dn_details: From afda48a12b33b829d2e4aa3f210438e8feb3c645 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Jan 2022 11:54:14 +0530 Subject: [PATCH 005/133] fix: cost center validation of asset (cherry picked from commit 4390adcaa1191ae919164af0903ee12eba5c3cfa) # Conflicts: # erpnext/assets/doctype/asset/test_asset.py --- erpnext/assets/doctype/asset/asset.py | 14 +++++++++ erpnext/assets/doctype/asset/test_asset.py | 33 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a57a3e281d3..b2ed5add784 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -37,6 +37,7 @@ class Asset(AccountsController): self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() + self.validate_cost_center() self.set_missing_values() self.prepare_depreciation_data() self.validate_gross_and_purchase_amount() @@ -96,6 +97,19 @@ class Asset(AccountsController): elif item.is_stock_item: frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code)) + def validate_cost_center(self): + if not self.cost_center: return + + cost_center_company = frappe.db.get_value('Cost Center', self.cost_center, 'company') + if cost_center_company != self.company: + frappe.throw( + _("Selected Cost Center {} doesn't belongs to {}").format( + frappe.bold(self.cost_center), + frappe.bold(self.company) + ), + title=_("Invalid Cost Center") + ) + def validate_in_use_date(self): if not self.available_for_use_date: frappe.throw(_("Available for use date is required")) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index ba4dbee72da..23c196675f6 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1054,6 +1054,39 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(gle, expected_gle) self.assertEqual(asset.get("value_after_depreciation"), 0) +<<<<<<< HEAD +======= + + def test_expected_value_change(self): + """ + tests if changing `expected_value_after_useful_life` + affects `value_after_depreciation` + """ + + asset = create_asset(calculate_depreciation=1) + asset.opening_accumulated_depreciation = 2000 + asset.number_of_depreciations_booked = 1 + + asset.finance_books[0].expected_value_after_useful_life = 100 + asset.save() + asset.reload() + self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) + + # changing expected_value_after_useful_life shouldn't affect value_after_depreciation + asset.finance_books[0].expected_value_after_useful_life = 200 + asset.save() + asset.reload() + self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) +>>>>>>> 4390adcaa1 (fix: cost center validation of asset) + + def test_asset_cost_center(self): + asset = create_asset(is_existing_asset = 1, do_not_save=1) + asset.cost_center = "Main - WP" + + self.assertRaises(frappe.ValidationError, asset.submit) + + asset.cost_center = "Main - _TC" + asset.submit() def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): From b66f86b0448f70aff242ee8264ec4af9d6017934 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 21 Jan 2022 12:28:02 +0530 Subject: [PATCH 006/133] fix: merge conflict --- erpnext/assets/doctype/asset/test_asset.py | 24 ---------------------- 1 file changed, 24 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 23c196675f6..1e47b6ae609 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1054,30 +1054,6 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(gle, expected_gle) self.assertEqual(asset.get("value_after_depreciation"), 0) -<<<<<<< HEAD -======= - - def test_expected_value_change(self): - """ - tests if changing `expected_value_after_useful_life` - affects `value_after_depreciation` - """ - - asset = create_asset(calculate_depreciation=1) - asset.opening_accumulated_depreciation = 2000 - asset.number_of_depreciations_booked = 1 - - asset.finance_books[0].expected_value_after_useful_life = 100 - asset.save() - asset.reload() - self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) - - # changing expected_value_after_useful_life shouldn't affect value_after_depreciation - asset.finance_books[0].expected_value_after_useful_life = 200 - asset.save() - asset.reload() - self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) ->>>>>>> 4390adcaa1 (fix: cost center validation of asset) def test_asset_cost_center(self): asset = create_asset(is_existing_asset = 1, do_not_save=1) From c6273b32ee5fe644df699d9f668e17ad0848732a Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Jan 2022 13:39:10 +0530 Subject: [PATCH 007/133] fix: Use get for conditionally available fields while setting missing values - Due to custom field "supplier" and missing field "supplier_address", dot operator breaks - Make sure to use "get" instead of just dot operator if field is in some doctypes, not all (cherry picked from commit 78b6b29a57928d936181337d2ee5e85d951e5bce) --- erpnext/controllers/buying_controller.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a3d2502268e..e5f35e1b72e 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -70,9 +70,18 @@ class BuyingController(StockController, Subcontracting): # set contact and address details for supplier, if they are not mentioned if getattr(self, "supplier", None): - self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions, - doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'), - fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'))) + self.update_if_missing( + get_party_details( + self.supplier, + party_type="Supplier", + doctype=self.doctype, + company=self.company, + party_address=self.get("supplier_address"), + shipping_address=self.get('shipping_address'), + fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'), + ignore_permissions=self.flags.ignore_permissions + ) + ) self.set_missing_item_details(for_validate) From 86a560eeb0526bde1dfe15674b597360459d620d Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 31 Jan 2022 13:08:19 +0530 Subject: [PATCH 008/133] fix: Validation for invalid serial nos at POS invoice level (#29447) (cherry picked from commit 05bbb69d0ed53e6687692b72f046d6bf07a4eb08) --- .../doctype/pos_invoice/pos_invoice.py | 15 +++++++++++++++ .../doctype/pos_invoice/test_pos_invoice.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index de31b5c4136..245697880c5 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -159,6 +159,20 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.") .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) + def validate_invalid_serial_nos(self, item): + serial_nos = get_serial_nos(item.serial_no) + error_msg = [] + invalid_serials, msg = "", "" + for serial_no in serial_nos: + if not frappe.db.exists('Serial No', serial_no): + invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no + msg = (_("Row #{}: Following Serial numbers for item {} are Invalid: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials))) + if invalid_serials: + error_msg.append(msg) + + if error_msg: + frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) + def validate_stock_availablility(self): if self.is_return or self.docstatus != 1: return @@ -168,6 +182,7 @@ class POSInvoice(SalesInvoice): if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) + self.validate_invalid_serial_nos(d) elif d.batch_no: self.validate_pos_reserved_batch_qty(d) else: diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 56479a0b77d..ba751c081bb 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase): pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) + def test_invalid_serial_no_validation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + se = make_serialized_item(company='_Test Company', + target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + serial_nos = se.get("items")[0].serial_no + 'wrong' + + pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', + account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', + expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', + item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1) + + pos.get('items')[0].has_serial_no = 1 + pos.get('items')[0].serial_no = serial_nos + pos.insert() + + self.assertRaises(frappe.ValidationError, pos.submit) + def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, From fedeb2a70f80a39d31cb928d9876fbc94f27561c Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 31 Jan 2022 19:07:07 +0530 Subject: [PATCH 009/133] chore: remove patch --- erpnext/patches.txt | 1 - .../v13_0/set_billed_amount_in_returned_dn.py | 22 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4d3b5f913ca..029b603debb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -342,7 +342,6 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning -erpnext.patches.v13_0.set_billed_amount_in_returned_dn erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail diff --git a/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py b/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py deleted file mode 100644 index 1f86c76d14f..00000000000 --- a/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2022, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so - - -def execute(): - dn_item = frappe.qb.DocType('Delivery Note Item') - - so_detail_list = (frappe.qb.from_(dn_item) - .select(dn_item.so_detail) - .where( - (dn_item.so_detail.notnull()) & - (dn_item.so_detail != '') & - (dn_item.docstatus == 1) & - (dn_item.returned_qty > 0) - )).run() - - for so_detail in so_detail_list: - update_billed_amount_based_on_so(so_detail[0], False) \ No newline at end of file From f5bb778d3785510c16708ee322df4b33937e0c02 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Wed, 19 Jan 2022 15:27:09 +0530 Subject: [PATCH 010/133] bypass selling price validation for free item (cherry picked from commit 8c536ffb20424627daac287b6a58af6c35536ebb) --- erpnext/controllers/selling_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4ff851d7f94..75fcaee3832 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -204,7 +204,7 @@ class SellingController(StockController): valuation_rate_map = {} for item in self.items: - if not item.item_code: + if not item.item_code or item.is_free_item: continue last_purchase_rate, is_stock_item = frappe.get_cached_value( @@ -251,7 +251,7 @@ class SellingController(StockController): valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate for item in self.items: - if not item.item_code: + if not item.item_code or item.is_free_item: continue last_valuation_rate = valuation_rate_map.get( From 2b83dabae13ab2d5f274e1334f2b488c649fb5f1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 12:52:30 +0530 Subject: [PATCH 011/133] fix: do not hide Loan Repayment Entry field in salary slip (backport #29535) (#29551) Co-authored-by: Rucha Mahabal --- .../doctype/salary_slip_loan/salary_slip_loan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json index 3d070812152..b7b20d945d6 100644 --- a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json @@ -70,7 +70,6 @@ { "fieldname": "loan_repayment_entry", "fieldtype": "Link", - "hidden": 1, "label": "Loan Repayment Entry", "no_copy": 1, "options": "Loan Repayment", @@ -88,7 +87,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-14 20:47:11.725818", + "modified": "2022-01-31 14:50:14.823213", "modified_by": "Administrator", "module": "Loan Management", "name": "Salary Slip Loan", @@ -97,5 +96,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From a54c84728fcf95c3f5540d576080b7ca39348a65 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 1 Feb 2022 14:42:55 +0530 Subject: [PATCH 012/133] feat: Provisional accounting for expenses (#29451) * feat: Provisonal accounting for expenses * fix: Method for provisional accounting entry * chore: Add test case * fix: Remove test case * fix: Use company doctype * fix: Add provisional expense account field in Purchase Receipt Item * fix: Test case * fix: Move provisional expense account to parent * fix: Patch (cherry picked from commit 528c71382f972ec4464c31c9bc633c179a882072) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py # erpnext/patches.txt # erpnext/setup/doctype/company/company.json # erpnext/stock/doctype/purchase_receipt/purchase_receipt.py # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py --- .../purchase_invoice/purchase_invoice.py | 29 +++-- .../purchase_invoice/test_purchase_invoice.py | 46 +++++++- erpnext/controllers/stock_controller.py | 5 +- erpnext/patches.txt | 20 +++- .../v13_0/enable_provisional_accounting.py | 19 +++ erpnext/setup/doctype/company/company.json | 47 +++++--- erpnext/setup/doctype/company/company.py | 14 ++- .../purchase_receipt/purchase_receipt.json | 19 ++- .../purchase_receipt/purchase_receipt.py | 109 +++++++++++++----- .../purchase_receipt/test_purchase_receipt.py | 42 +++++++ .../purchase_receipt_item.json | 5 +- 11 files changed, 287 insertions(+), 68 deletions(-) create mode 100644 erpnext/patches/v13_0/enable_provisional_accounting.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c5cc05749a7..916cdc58e55 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -543,6 +543,17 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] +<<<<<<< HEAD +======= + exchange_rate_map, net_rate_map = get_purchase_document_details(self) + + enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \ + 'enable_provisional_accounting_for_non_stock_items')) + + purchase_receipt_doc_map = {} + +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) @@ -637,19 +648,23 @@ class PurchaseInvoice(BuyingController): else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) - auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) - - if auto_accounting_for_non_stock_items: - service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") - + if provisional_accounting_for_non_stock_items: if item.purchase_receipt: + provisional_account = self.get_company_default("default_provisional_account") + purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) + + if not purchase_receipt_doc: + purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) + purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0, 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail, - 'account':service_received_but_not_billed_account}, ['name']) + 'account':provisional_account}, ['name']) if expense_booked_in_pr: - expense_account = service_received_but_not_billed_account + # Intentionally passing purchase invoice item to handle partial billing + purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1) if not self.is_internal_transfer(): gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index d01baebe636..1d2dcdf2776 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.exceptions import InvalidCurrency from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as create_purchase_invoice_from_receipt, +) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_taxes, make_purchase_receipt, @@ -1124,8 +1129,6 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice - from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order # create a new supplier to test supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier', @@ -1198,6 +1201,45 @@ class TestPurchaseInvoice(unittest.TestCase): payment_entry.load_from_db() self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def test_provisional_accounting_entry(self): + item = create_item("_Test Non Stock Item", is_stock_item=0) + provisional_account = create_account(account_name="Provision Account", + parent_account="Current Liabilities - _TC", company="_Test Company") + + company = frappe.get_doc('Company', '_Test Company') + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)) + + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].expense_account = 'Cost of Goods Sold - _TC' + pi.save() + pi.submit() + + # Check GLE for Purchase Invoice + expected_gle = [ + ['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)], + ['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)] + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 250, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date], + ["Provision Account - _TC", 0, 250, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date] + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.save() + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date from `tabGL Entry` diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2912d3eb0bd..8d17683953e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -40,7 +40,10 @@ class StockController(AccountsController): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - if cint(erpnext.is_perpetual_inventory_enabled(self.company)): + provisional_accounting_for_non_stock_items = \ + cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + + if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items: warehouse_account = get_warehouse_account_map(self.company) if self.docstatus==1: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 569fd96e8a3..4f09f8a586f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -345,4 +345,22 @@ erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail -erpnext.patches.v13_0.update_sane_transfer_against \ No newline at end of file +<<<<<<< HEAD +erpnext.patches.v13_0.update_sane_transfer_against +======= +erpnext.patches.v13_0.enable_provisional_accounting + +[post_model_sync] +erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template +erpnext.patches.v14_0.delete_shopify_doctypes +erpnext.patches.v14_0.delete_hub_doctypes +erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 +erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v14_0.rearrange_company_fields +erpnext.patches.v14_0.update_leave_notification_template +erpnext.patches.v14_0.restore_einvoice_fields +erpnext.patches.v13_0.update_sane_transfer_against +erpnext.patches.v12_0.add_company_link_to_einvoice_settings +erpnext.patches.v14_0.migrate_cost_center_allocations +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py new file mode 100644 index 00000000000..8e222700f86 --- /dev/null +++ b/erpnext/patches/v13_0/enable_provisional_accounting.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + frappe.reload_doc("setup", "doctype", "company") + + company = frappe.qb.DocType("Company") + + frappe.qb.update( + company + ).set( + company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items + ).set( + company.default_provisional_account, company.service_received_but_not_billed + ).where( + company.enable_perpetual_inventory_for_non_stock_items == 1 + ).where( + company.service_received_but_not_billed.isnotnull() + ).run() \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index dae64e4ad65..00ceb95bc74 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "field:company_name", - "creation": "2013-04-10 08:35:39", + "creation": "2022-01-25 10:29:55.938239", "description": "Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Organization.", "doctype": "DocType", "document_type": "Setup", @@ -66,12 +66,12 @@ "payment_terms", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", - "enable_perpetual_inventory_for_non_stock_items", + "enable_provisional_accounting_for_non_stock_items", "default_inventory_account", "stock_adjustment_account", "column_break_32", "stock_received_but_not_billed", - "service_received_but_not_billed", + "default_provisional_account", "expenses_included_in_valuation", "fixed_asset_defaults", "accumulated_depreciation_account", @@ -692,20 +692,6 @@ "label": "Default Buying Terms", "options": "Terms and Conditions" }, - { - "fieldname": "service_received_but_not_billed", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Service Received But Not Billed", - "no_copy": 1, - "options": "Account" - }, - { - "default": "0", - "fieldname": "enable_perpetual_inventory_for_non_stock_items", - "fieldtype": "Check", - "label": "Enable Perpetual Inventory For Non Stock Items" - }, { "fieldname": "default_in_transit_warehouse", "fieldtype": "Link", @@ -735,6 +721,28 @@ "fieldtype": "Link", "label": "Repair and Maintenance Account", "options": "Account" +<<<<<<< HEAD +======= + }, + { + "fieldname": "section_break_28", + "fieldtype": "Section Break", + "label": "Chart of Accounts" + }, + { + "default": "0", + "fieldname": "enable_provisional_accounting_for_non_stock_items", + "fieldtype": "Check", + "label": "Enable Provisional Accounting For Non Stock Items" + }, + { + "fieldname": "default_provisional_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Provisional Account", + "no_copy": 1, + "options": "Account" +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) } ], "icon": "fa fa-building", @@ -742,7 +750,11 @@ "image_field": "company_logo", "is_tree": 1, "links": [], +<<<<<<< HEAD "modified": "2021-12-02 14:52:08.187233", +======= + "modified": "2022-01-25 10:33:16.826067", +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -802,5 +814,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 142fe04b6f2..3347935234c 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -11,6 +11,7 @@ import frappe.defaults from frappe import _ from frappe.cache_manager import clear_defaults_cache from frappe.contacts.address_and_contact import load_address_and_contact +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.utils import cint, formatdate, get_timestamp, today from frappe.utils.nestedset import NestedSet from past.builtins import cmp @@ -47,7 +48,7 @@ class Company(NestedSet): self.validate_currency() self.validate_coa_input() self.validate_perpetual_inventory() - self.validate_perpetual_inventory_for_non_stock_items() + self.validate_provisional_account_for_non_stock_items() self.check_country_change() self.check_parent_changed() self.set_chart_of_accounts() @@ -189,11 +190,14 @@ class Company(NestedSet): frappe.msgprint(_("Set default inventory account for perpetual inventory"), alert=True, indicator='orange') - def validate_perpetual_inventory_for_non_stock_items(self): + def validate_provisional_account_for_non_stock_items(self): if not self.get("__islocal"): - if cint(self.enable_perpetual_inventory_for_non_stock_items) == 1 and not self.service_received_but_not_billed: - frappe.throw(_("Set default {0} account for perpetual inventory for non stock items").format( - frappe.bold('Service Received But Not Billed'))) + if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account: + frappe.throw(_("Set default {0} account for non stock items").format( + frappe.bold('Provisional Account'))) + + make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden", + not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False) def check_country_change(self): frappe.flags.country_change = False diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 112ddedac29..b54a90eed35 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -106,6 +106,8 @@ "terms", "bill_no", "bill_date", + "accounting_details_section", + "provisional_expense_account", "more_info", "project", "status", @@ -1144,16 +1146,30 @@ "label": "Represents Company", "options": "Company", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "provisional_expense_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Provisional Expense Account", + "options": "Account" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:11:10.181328", + "modified": "2022-02-01 11:40:52.690984", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -1214,6 +1230,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e7e9e9c1d7f..218ec583f93 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -9,6 +9,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, flt, getdate, nowdate from six import iteritems +import erpnext from erpnext.accounts.utils import get_account_currency 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 @@ -113,6 +114,7 @@ class PurchaseReceipt(BuyingController): self.validate_uom_is_integer("uom", ["qty", "received_qty"]) self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_cwip_accounts() + self.validate_provisional_expense_account() self.check_on_hold_or_closed_status() @@ -134,6 +136,15 @@ class PurchaseReceipt(BuyingController): company = self.company) break + def validate_provisional_expense_account(self): + provisional_accounting_for_non_stock_items = \ + cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + + if provisional_accounting_for_non_stock_items: + default_provisional_account = self.get_company_default("default_provisional_account") + if not self.provisional_expense_account: + self.provisional_expense_account = default_provisional_account + def validate_with_previous_doc(self): super(PurchaseReceipt, self).validate_with_previous_doc({ "Purchase Order": { @@ -255,13 +266,26 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): +<<<<<<< HEAD stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) +======= + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( + get_purchase_document_details, + ) + + if erpnext.is_perpetual_inventory_enabled(self.company): + stock_rbnb = self.get_company_default("stock_received_but_not_billed") + landed_cost_entries = get_item_account_wise_additional_cost(self.name) + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) warehouse_with_no_account = [] stock_items = self.get_stock_items() + provisional_accounting_for_non_stock_items = \ + cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): @@ -384,43 +408,58 @@ class PurchaseReceipt(BuyingController): elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(d.warehouse) - elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items: - service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") - credit_currency = get_account_currency(service_received_but_not_billed_account) - debit_currency = get_account_currency(d.expense_account) - remarks = self.get("remarks") or _("Accounting Entry for Service") - - self.add_gl_entry( - gl_entries=gl_entries, - account=service_received_but_not_billed_account, - cost_center=d.cost_center, - debit=0.0, - credit=d.amount, - remarks=remarks, - against_account=d.expense_account, - account_currency=credit_currency, - project=d.project, - voucher_detail_no=d.name, item=d) - - self.add_gl_entry( - gl_entries=gl_entries, - account=d.expense_account, - cost_center=d.cost_center, - debit=d.amount, - credit=0.0, - remarks=remarks, - against_account=service_received_but_not_billed_account, - account_currency = debit_currency, - project=d.project, - voucher_detail_no=d.name, - item=d) + elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and provisional_accounting_for_non_stock_items: + self.add_provisional_gl_entry(d, gl_entries, self.posting_date) if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) + def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): + provisional_expense_account = self.get('provisional_expense_account') + credit_currency = get_account_currency(provisional_expense_account) + debit_currency = get_account_currency(item.expense_account) + expense_account = item.expense_account + remarks = self.get("remarks") or _("Accounting Entry for Service") + multiplication_factor = 1 + + if reverse: + multiplication_factor = -1 + expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account']) + + self.add_gl_entry( + gl_entries=gl_entries, + account=provisional_expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=multiplication_factor * item.amount, + remarks=remarks, + against_account=expense_account, + account_currency=credit_currency, + project=item.project, + voucher_detail_no=item.name, + item=item, + posting_date=posting_date) + + self.add_gl_entry( + gl_entries=gl_entries, + account=expense_account, + cost_center=item.cost_center, + debit=multiplication_factor * item.amount, + credit=0.0, + remarks=remarks, + against_account=provisional_expense_account, + account_currency = debit_currency, + project=item.project, + voucher_detail_no=item.name, + item=item, + posting_date=posting_date) + def make_tax_gl_entries(self, gl_entries): - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + + if erpnext.is_perpetual_inventory_enabled(self.company): + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')]) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} @@ -477,7 +516,8 @@ class PurchaseReceipt(BuyingController): def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account, debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None, - project=None, voucher_detail_no=None, item=None): + project=None, voucher_detail_no=None, item=None, posting_date=None): + gl_entry = { "account": account, "cost_center": cost_center, @@ -496,6 +536,9 @@ class PurchaseReceipt(BuyingController): if credit_in_account_currency: gl_entry.update({"credit_in_account_currency": credit_in_account_currency}) + if posting_date: + gl_entry.update({"posting_date": posting_date}) + gl_entries.append(self.get_gl_dict(gl_entry, item=item)) def get_asset_gl_entry(self, gl_entries): @@ -524,6 +567,7 @@ class PurchaseReceipt(BuyingController): # debit cwip account debit_in_account_currency = (base_asset_amount if cwip_account_currency == self.company_currency else asset_amount) + self.add_gl_entry( gl_entries=gl_entries, account=cwip_account, @@ -539,6 +583,7 @@ class PurchaseReceipt(BuyingController): # credit arbnb account credit_in_account_currency = (base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount) + self.add_gl_entry( gl_entries=gl_entries, account=arbnb_account, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6774dafb68c..5d11955ed90 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1331,6 +1331,7 @@ class TestPurchaseReceipt(ERPNextTestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) +<<<<<<< HEAD def test_service_item_purchase_with_perpetual_inventory(self): company = '_Test Company with perpetual inventory' service_item = '_Test Non Stock Item' @@ -1355,10 +1356,31 @@ class TestPurchaseReceipt(ERPNextTestCase): item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) item_row_with_diff_rate.rate = 100 pr.append('items', item_row_with_diff_rate) +======= + def test_purchase_receipt_with_exchange_rate_difference(self): + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( + make_purchase_receipt as create_purchase_receipt, + ) + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( + make_purchase_invoice as create_purchase_invoice, + ) + + pi = create_purchase_invoice(company="_Test Company with perpetual inventory", + cost_center = "Main - TCP1", + warehouse = "Stores - TCP1", + expense_account ="_Test Account Cost for Goods Sold - TCP1", + currency = "USD", conversion_rate = 70) + + pr = create_purchase_receipt(pi.name) + pr.conversion_rate = 80 + pr.items[0].purchase_invoice = pi.name + pr.items[0].purchase_invoice_item = pi.items[0].name +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) pr.save() pr.submit() +<<<<<<< HEAD item_one_gl_entry = frappe.db.get_all("GL Entry", { 'voucher_type': pr.doctype, 'voucher_no': pr.name, @@ -1383,6 +1405,26 @@ class TestPurchaseReceipt(ERPNextTestCase): 'enable_perpetual_inventory_for_non_stock_items', before_test_value ) +======= + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value( + 'Company', pr.company, 'exchange_gain_loss_account' + ) + + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value( + 'GL Entry', + { + 'account': exchange_gain_loss_account, + 'voucher_no': pr.name + }, + 'credit' + ) + discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) + + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + +>>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 30ea1c3cadc..e5994b2dd48 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -976,7 +976,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-11-15 15:46:10.591600", + "modified": "2022-02-01 11:32:27.980524", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -985,5 +985,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From aa16a4bb5f132b85e207b95b96edcdd6b2c72219 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Feb 2022 15:02:23 +0530 Subject: [PATCH 013/133] chore: Resolve conflicts --- .../purchase_invoice/purchase_invoice.py | 5 --- erpnext/patches.txt | 19 +-------- erpnext/setup/doctype/company/company.json | 7 ---- .../purchase_receipt/purchase_receipt.py | 11 ----- .../purchase_receipt/test_purchase_receipt.py | 42 ------------------- 5 files changed, 1 insertion(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 916cdc58e55..50d9c3a5de0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -543,17 +543,12 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] -<<<<<<< HEAD -======= - exchange_rate_map, net_rate_map = get_purchase_document_details(self) - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \ 'enable_provisional_accounting_for_non_stock_items')) purchase_receipt_doc_map = {} ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4f09f8a586f..7cbb6fa0ae9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -345,22 +345,5 @@ erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail -<<<<<<< HEAD erpnext.patches.v13_0.update_sane_transfer_against -======= -erpnext.patches.v13_0.enable_provisional_accounting - -[post_model_sync] -erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents -erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template -erpnext.patches.v14_0.delete_shopify_doctypes -erpnext.patches.v14_0.delete_hub_doctypes -erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 -erpnext.patches.v14_0.delete_agriculture_doctypes -erpnext.patches.v14_0.rearrange_company_fields -erpnext.patches.v14_0.update_leave_notification_template -erpnext.patches.v14_0.restore_einvoice_fields -erpnext.patches.v13_0.update_sane_transfer_against -erpnext.patches.v12_0.add_company_link_to_einvoice_settings -erpnext.patches.v14_0.migrate_cost_center_allocations ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) +erpnext.patches.v13_0.enable_provisional_accounting \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 00ceb95bc74..84bc6afa4d5 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -721,8 +721,6 @@ "fieldtype": "Link", "label": "Repair and Maintenance Account", "options": "Account" -<<<<<<< HEAD -======= }, { "fieldname": "section_break_28", @@ -742,7 +740,6 @@ "label": "Default Provisional Account", "no_copy": 1, "options": "Account" ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) } ], "icon": "fa fa-building", @@ -750,11 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-12-02 14:52:08.187233", -======= "modified": "2022-01-25 10:33:16.826067", ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 218ec583f93..47c81280f6b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -266,21 +266,10 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): -<<<<<<< HEAD - stock_rbnb = self.get_company_default("stock_received_but_not_billed") - landed_cost_entries = get_item_account_wise_additional_cost(self.name) - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) -======= - from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( - get_purchase_document_details, - ) - if erpnext.is_perpetual_inventory_enabled(self.company): stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) warehouse_with_no_account = [] stock_items = self.get_stock_items() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5d11955ed90..6774dafb68c 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1331,7 +1331,6 @@ class TestPurchaseReceipt(ERPNextTestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) -<<<<<<< HEAD def test_service_item_purchase_with_perpetual_inventory(self): company = '_Test Company with perpetual inventory' service_item = '_Test Non Stock Item' @@ -1356,31 +1355,10 @@ class TestPurchaseReceipt(ERPNextTestCase): item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) item_row_with_diff_rate.rate = 100 pr.append('items', item_row_with_diff_rate) -======= - def test_purchase_receipt_with_exchange_rate_difference(self): - from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( - make_purchase_receipt as create_purchase_receipt, - ) - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( - make_purchase_invoice as create_purchase_invoice, - ) - - pi = create_purchase_invoice(company="_Test Company with perpetual inventory", - cost_center = "Main - TCP1", - warehouse = "Stores - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1", - currency = "USD", conversion_rate = 70) - - pr = create_purchase_receipt(pi.name) - pr.conversion_rate = 80 - pr.items[0].purchase_invoice = pi.name - pr.items[0].purchase_invoice_item = pi.items[0].name ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) pr.save() pr.submit() -<<<<<<< HEAD item_one_gl_entry = frappe.db.get_all("GL Entry", { 'voucher_type': pr.doctype, 'voucher_no': pr.name, @@ -1405,26 +1383,6 @@ class TestPurchaseReceipt(ERPNextTestCase): 'enable_perpetual_inventory_for_non_stock_items', before_test_value ) -======= - # Get exchnage gain and loss account - exchange_gain_loss_account = frappe.db.get_value( - 'Company', pr.company, 'exchange_gain_loss_account' - ) - - # fetching the latest GL Entry with exchange gain and loss account account - amount = frappe.db.get_value( - 'GL Entry', - { - 'account': exchange_gain_loss_account, - 'voucher_no': pr.name - }, - 'credit' - ) - discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) - - self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) - ->>>>>>> 528c71382f (feat: Provisional accounting for expenses (#29451)) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From c71dbda1660d5fcb795f7eebca288413ce8ef677 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Feb 2022 15:08:45 +0530 Subject: [PATCH 014/133] fix: Linting Issues --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 50d9c3a5de0..165f6000ce7 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -543,8 +543,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) - provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \ + provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) purchase_receipt_doc_map = {} From e2bdd5efdb20b150c294a1026ab5424f4eba9a38 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 16:18:16 +0530 Subject: [PATCH 015/133] fix: leave application tests (backport #29539) (#29563) Co-authored-by: Rucha Mahabal --- erpnext/hr/doctype/employee/test_employee.py | 2 +- .../test_leave_application.py | 20 ++++++++++--------- .../doctype/salary_slip/test_salary_slip.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 8a2da0866e9..67cbea67e1f 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -36,7 +36,7 @@ class TestEmployee(unittest.TestCase): employee_doc.reload() make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List") frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 0d2e3989e3e..93f4176bd5f 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -75,10 +75,8 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec frappe.set_user("Administrator") - - @classmethod - def setUpClass(cls): set_leave_approver() + frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") def tearDown(self): @@ -134,10 +132,11 @@ class TestLeaveApplication(unittest.TestCase): 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() - frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) + employee = get_employee() + frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) - leave_application = make_leave_application("_T-Employee-00001", 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) leave_application.reload() self.assertEqual(leave_application.total_leave_days, 4) self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) @@ -157,25 +156,28 @@ class TestLeaveApplication(unittest.TestCase): 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() - frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) + employee = get_employee() + frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) # already marked attendance on a holiday should be deleted in this case config = { "doctype": "Attendance", - "employee": "_T-Employee-00001", + "employee": employee.name, "status": "Present" } attendance_on_holiday = frappe.get_doc(config) attendance_on_holiday.attendance_date = first_sunday + attendance_on_holiday.flags.ignore_validate = True attendance_on_holiday.save() # already marked attendance on a non-holiday should be updated attendance = frappe.get_doc(config) attendance.attendance_date = add_days(first_sunday, 3) + attendance.flags.ignore_validate = True attendance.save() - leave_application = make_leave_application("_T-Employee-00001", 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) leave_application.reload() # holiday should be excluded while marking attendance self.assertEqual(leave_application.total_leave_days, 3) @@ -325,7 +327,7 @@ class TestLeaveApplication(unittest.TestCase): employee = get_employee() default_holiday_list = make_holiday_list() - frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list) + frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list) first_sunday = get_first_sunday(default_holiday_list) optional_leave_date = add_days(first_sunday, 1) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 46c99517d66..cc011a50c6b 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -147,7 +147,7 @@ class TestSalarySlip(unittest.TestCase): # 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") + 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"}) # mark attendance From 75faa151fdc7690a9dc1fa6e53ed6696402d1ab2 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 22 Dec 2021 19:14:47 +0530 Subject: [PATCH 016/133] fix: Allowing non stock items in POS (cherry picked from commit ec37930404dd1bf4563e4bc2ea4acd767d77aff4) --- .../accounts/doctype/pos_invoice/pos_invoice.py | 15 ++++++++++----- .../selling/page/point_of_sale/point_of_sale.py | 4 ++-- .../selling/page/point_of_sale/pos_controller.js | 16 +++++++++++----- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index de31b5c4136..f17822aff3d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -43,7 +43,7 @@ class POSInvoice(SalesInvoice): self.validate_serialised_or_batched_item() self.validate_stock_availablility() self.validate_return_items_qty() - self.validate_non_stock_items() + # self.validate_non_stock_items() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() @@ -162,9 +162,11 @@ class POSInvoice(SalesInvoice): def validate_stock_availablility(self): if self.is_return or self.docstatus != 1: return - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') for d in self.get('items'): + is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) + if is_service_item: + return if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) @@ -495,9 +497,12 @@ def get_stock_availability(item_code, warehouse): bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty - else: - if frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) + elif frappe.db.exists('Product Bundle', item_code): + return get_bundle_availability(item_code, warehouse) + #To continue the flow considering a service item + elif frappe.db.get_value('Item', item_code, 'is_stock_item') == 0: + return 0 + def get_bundle_availability(bundle_item_code, warehouse): product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 73ac3afb31b..7052a360918 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -99,7 +99,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ), {'warehouse': warehouse}, as_dict=1) if items_data: - items_data = filter_service_items(items_data) + # items_data = filter_service_items(items_data) items = [d.item_code for d in items_data] item_prices_data = frappe.get_all("Item Price", fields = ["item_code", "price_list_rate", "currency"], @@ -146,7 +146,7 @@ def search_for_serial_or_batch_or_barcode_number(search_value): def filter_service_items(items): for item in items: - if not item['is_stock_item']: + if not item.get('is_stock_item'): if not frappe.db.exists('Product Bundle', item['item_code']): items.remove(item) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ce74f6d0a58..a481adca924 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -637,11 +637,17 @@ erpnext.PointOfSale.Controller = class { const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold() if (!(available_qty > 0)) { - frappe.model.clear_doc(item_row.doctype, item_row.name); - frappe.throw({ - title: __("Not Available"), - message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) - }) + frappe.db.get_value('Item', item_row.item_code, 'is_stock_item').then(({message}) => { + const is_service_item = message.is_stock_item; + console.log('is_service_item', is_service_item); + if (!is_service_item) return; + + frappe.model.clear_doc(item_row.doctype, item_row.name); + frappe.throw({ + title: __("Not Available"), + message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) + }) + }); } else if (available_qty < qty_needed) { frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), From 0aff20e85cb45ce172a05d63df169d01b61a3f7b Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 10:47:13 +0530 Subject: [PATCH 017/133] fix: sider issues (cherry picked from commit 6b973d658c9b7f77864627c585908f4c7e7e1920) --- erpnext/selling/page/point_of_sale/pos_controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index a481adca924..5e3da8912b8 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -639,14 +639,13 @@ erpnext.PointOfSale.Controller = class { if (!(available_qty > 0)) { frappe.db.get_value('Item', item_row.item_code, 'is_stock_item').then(({message}) => { const is_service_item = message.is_stock_item; - console.log('is_service_item', is_service_item); if (!is_service_item) return; frappe.model.clear_doc(item_row.doctype, item_row.name); frappe.throw({ title: __("Not Available"), message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) - }) + }); }); } else if (available_qty < qty_needed) { frappe.throw({ From 15c6b93ee384feccd9d7841cf553f7b1a44ee780 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 3 Jan 2022 14:46:53 +0530 Subject: [PATCH 018/133] fix: Removed validate_non_stock_items, filter_service_items methods (cherry picked from commit 9e20fa85c1823bb8622d7321f561b7d010b7d053) --- .../doctype/pos_invoice/pos_invoice.py | 20 ++++++------------- .../page/point_of_sale/point_of_sale.py | 9 --------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index f17822aff3d..cf85ef28191 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -43,7 +43,6 @@ class POSInvoice(SalesInvoice): self.validate_serialised_or_batched_item() self.validate_stock_availablility() self.validate_return_items_qty() - # self.validate_non_stock_items() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() @@ -247,14 +246,6 @@ class POSInvoice(SalesInvoice): .format(d.idx, bold_serial_no, bold_return_against) ) - def validate_non_stock_items(self): - for d in self.get("items"): - is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") - if not is_stock_item: - if not frappe.db.exists('Product Bundle', d.item_code): - frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) - def validate_mode_of_payment(self): if len(self.payments) == 0: frappe.throw(_("At least one mode of payment is required for POS invoice.")) @@ -497,11 +488,12 @@ def get_stock_availability(item_code, warehouse): bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty - elif frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) - #To continue the flow considering a service item - elif frappe.db.get_value('Item', item_code, 'is_stock_item') == 0: - return 0 + else: + if frappe.db.exists('Product Bundle', item_code): + return get_bundle_availability(item_code, warehouse) + else: + # Is a service item + return 0 def get_bundle_availability(bundle_item_code, warehouse): diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 7052a360918..a11d186df46 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -99,7 +99,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ), {'warehouse': warehouse}, as_dict=1) if items_data: - # items_data = filter_service_items(items_data) items = [d.item_code for d in items_data] item_prices_data = frappe.get_all("Item Price", fields = ["item_code", "price_list_rate", "currency"], @@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value): return {} -def filter_service_items(items): - for item in items: - if not item.get('is_stock_item'): - if not frappe.db.exists('Product Bundle', item['item_code']): - items.remove(item) - - return items - def get_conditions(search_term): condition = "(" condition += """item.name like {search_term} From 8ed2a7bc2ce478a2e15623b21d7d027a794b62b1 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 17 Jan 2022 19:38:05 +0530 Subject: [PATCH 019/133] fix: removing `get_value` call by returning is_stock_item in `get_stock_availability` method (cherry picked from commit ac9a9fb22943a183e9e83077ec428262c4065a15) --- .../accounts/doctype/pos_invoice/pos_invoice.py | 10 ++++++---- .../selling/page/point_of_sale/point_of_sale.py | 4 ++-- .../selling/page/point_of_sale/pos_controller.js | 15 ++++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index cf85ef28191..15ade6c8597 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -175,7 +175,7 @@ class POSInvoice(SalesInvoice): if allow_negative_stock: return - available_stock = get_stock_availability(d.item_code, d.warehouse) + available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) if flt(available_stock) <= 0: @@ -485,15 +485,17 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): if frappe.db.get_value('Item', item_code, 'is_stock_item'): + is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - return bin_qty - pos_sales_qty + return bin_qty - pos_sales_qty, is_stock_item else: + is_stock_item = False if frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) + return get_bundle_availability(item_code, warehouse), is_stock_item else: # Is a service item - return 0 + return 0, is_stock_item def get_bundle_availability(bundle_item_code, warehouse): diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index a11d186df46..216e35a3903 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -24,7 +24,7 @@ def search_by_term(search_term, warehouse, price_list): ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"], as_dict=1) - item_stock_qty = get_stock_availability(item_code, warehouse) + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) price_list_rate, currency = frappe.db.get_value('Item Price', { 'price_list': price_list, 'item_code': item_code @@ -111,7 +111,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te for item in items_data: item_code = item.item_code item_price = item_prices.get(item_code) or {} - item_stock_qty = get_stock_availability(item_code, warehouse) + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) row = {} row.update(item) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5e3da8912b8..8cebfe67b3f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -630,23 +630,24 @@ erpnext.PointOfSale.Controller = class { } async check_stock_availability(item_row, qty_needed, warehouse) { - const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message; + const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; + const available_qty = resp[0]; + const is_stock_item = resp[1]; frappe.dom.unfreeze(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold() if (!(available_qty > 0)) { - frappe.db.get_value('Item', item_row.item_code, 'is_stock_item').then(({message}) => { - const is_service_item = message.is_stock_item; - if (!is_service_item) return; - + if (is_stock_item) { frappe.model.clear_doc(item_row.doctype, item_row.name); frappe.throw({ title: __("Not Available"), message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) }); - }); + } else { + return; + } } else if (available_qty < qty_needed) { frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), @@ -681,7 +682,7 @@ erpnext.PointOfSale.Controller = class { callback(res) { if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {} - me.item_stock_map[item_code][warehouse] = res.message; + me.item_stock_map[item_code][warehouse] = res.message[0]; } }); } From c57d184576eea7e46befbfe29f14fe43af527e3e Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 17 Jan 2022 19:53:39 +0530 Subject: [PATCH 020/133] fix: sider fix (cherry picked from commit 27b35d72e2ba6163c1ea0ba705d018aa7692dc8a) --- erpnext/selling/page/point_of_sale/pos_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 8cebfe67b3f..56aa24f6ec2 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -681,7 +681,7 @@ erpnext.PointOfSale.Controller = class { }, callback(res) { if (!me.item_stock_map[item_code]) - me.item_stock_map[item_code] = {} + me.item_stock_map[item_code] = {}; me.item_stock_map[item_code][warehouse] = res.message[0]; } }); From 7815ffb84267ea8bcbb07425b5f579239ff90506 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 17 Jan 2022 20:49:11 +0530 Subject: [PATCH 021/133] fix: remove qty indicator from non stock items (cherry picked from commit 845c02a989553a8375c94dbd0a8a71687fa9ff07) --- .../page/point_of_sale/pos_item_selector.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index a30bcd7cf6d..1177615aee9 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -79,14 +79,20 @@ erpnext.PointOfSale.ItemSelector = class { const me = this; // eslint-disable-next-line no-unused-vars const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; - const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; - + let indicator_color; let qty_to_display = actual_qty; - if (Math.round(qty_to_display) > 999) { - qty_to_display = Math.round(qty_to_display)/1000; - qty_to_display = qty_to_display.toFixed(1) + 'K'; + if (item.is_stock_item) { + indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"); + + if (Math.round(qty_to_display) > 999) { + qty_to_display = Math.round(qty_to_display)/1000; + qty_to_display = qty_to_display.toFixed(1) + 'K'; + } + } else { + indicator_color = ''; + qty_to_display = ''; } function get_item_image_html() { From c8f2a7fc6997ff7931df7c5039c7e34369f347b6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 13:14:28 +0530 Subject: [PATCH 022/133] test: point of sale search (cherry picked from commit 650d44a714cb94c9afdd80d1c38ce9c1e58cf28c) --- erpnext/tests/test_point_of_sale.py | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 erpnext/tests/test_point_of_sale.py diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py new file mode 100644 index 00000000000..e97c78e77aa --- /dev/null +++ b/erpnext/tests/test_point_of_sale.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + + +import frappe + +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile +from erpnext.selling.page.point_of_sale.point_of_sale import get_items +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 TestPointOfSale(ERPNextTestCase): + def test_item_search(self): + """ + Test Stock and Service Item Search. + """ + + pos_profile = make_pos_profile() + item1 = make_item("Test Stock Item", {"is_stock_item": 1}) + make_stock_entry( + item_code="Test Stock Item", qty=10, to_warehouse="_Test Warehouse - _TC", rate=500 + ) + + result = get_items( + start=0, + page_length=20, + price_list=None, + item_group=item1.item_group, + pos_profile=pos_profile.name, + search_term="Test Stock Item", + ) + filtered_items = result.get("items") + + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items[0]["item_code"], "Test Stock Item") + self.assertEqual(filtered_items[0]["actual_qty"], 10) + + item2 = make_item("Test Service Item", {"is_stock_item": 0}) + result = get_items( + start=0, + page_length=20, + price_list=None, + item_group=item2.item_group, + pos_profile=pos_profile.name, + search_term="Test Service Item", + ) + filtered_items = result.get("items") + + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items[0]["item_code"], "Test Service Item") + + frappe.db.rollback() From 04acbea93dbe1ba9d8e83cae79cad68ecf85a936 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 13:17:01 +0530 Subject: [PATCH 023/133] chore: remove useless rollback (cherry picked from commit 2aca54eb383c4e99861e40a6d1d8524ffd66f2ae) --- erpnext/tests/test_point_of_sale.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index e97c78e77aa..e68f1dc50d0 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -50,5 +50,3 @@ class TestPointOfSale(ERPNextTestCase): self.assertEqual(len(filtered_items), 1) self.assertEqual(filtered_items[0]["item_code"], "Test Service Item") - - frappe.db.rollback() From 33e8cd9cdecafb09e8b6448c1258ade567bc93fb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 14:02:52 +0530 Subject: [PATCH 024/133] chore: remove unused import (cherry picked from commit 4e4159ec0668ddecb9684e3c96810b5fca10b29a) --- erpnext/tests/test_point_of_sale.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index e68f1dc50d0..e1abf167bd2 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -2,8 +2,6 @@ # MIT License. See license.txt -import frappe - from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.selling.page.point_of_sale.point_of_sale import get_items from erpnext.stock.doctype.item.test_item import make_item From 4778f16c560932d9478d2c478774e6773631cbda Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 14:44:12 +0530 Subject: [PATCH 025/133] fix: flaky test (cherry picked from commit bf70feb7c9be7320188d72e26b2ef5d212c7a290) --- erpnext/tests/test_point_of_sale.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index e1abf167bd2..df2dc8b99a1 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -16,9 +16,12 @@ class TestPointOfSale(ERPNextTestCase): """ pos_profile = make_pos_profile() - item1 = make_item("Test Stock Item", {"is_stock_item": 1}) + item1 = make_item("Test Search Stock Item", {"is_stock_item": 1}) make_stock_entry( - item_code="Test Stock Item", qty=10, to_warehouse="_Test Warehouse - _TC", rate=500 + item_code="Test Search Stock Item", + qty=10, + to_warehouse="_Test Warehouse - _TC", + rate=500, ) result = get_items( @@ -27,24 +30,24 @@ class TestPointOfSale(ERPNextTestCase): price_list=None, item_group=item1.item_group, pos_profile=pos_profile.name, - search_term="Test Stock Item", + search_term="Test Search Stock Item", ) filtered_items = result.get("items") self.assertEqual(len(filtered_items), 1) - self.assertEqual(filtered_items[0]["item_code"], "Test Stock Item") + self.assertEqual(filtered_items[0]["item_code"], item1.item_code) self.assertEqual(filtered_items[0]["actual_qty"], 10) - item2 = make_item("Test Service Item", {"is_stock_item": 0}) + item2 = make_item("Test Search Service Item", {"is_stock_item": 0}) result = get_items( start=0, page_length=20, price_list=None, item_group=item2.item_group, pos_profile=pos_profile.name, - search_term="Test Service Item", + search_term="Test Search Service Item", ) filtered_items = result.get("items") self.assertEqual(len(filtered_items), 1) - self.assertEqual(filtered_items[0]["item_code"], "Test Service Item") + self.assertEqual(filtered_items[0]["item_code"], item2.item_code) From 640b2d57deb7f31bbff118e3bb8812329d3ae20b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Feb 2022 17:25:26 +0530 Subject: [PATCH 026/133] chore: fix tests --- .../v13_0/enable_provisional_accounting.py | 3 ++ .../purchase_receipt/test_purchase_receipt.py | 52 ------------------- 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py index 8e222700f86..85bbaed89df 100644 --- a/erpnext/patches/v13_0/enable_provisional_accounting.py +++ b/erpnext/patches/v13_0/enable_provisional_accounting.py @@ -2,6 +2,9 @@ import frappe def execute(): + if not frappe.get_meta("Company").has_field("enable_perpetual_inventory_for_non_stock_items"): + return + frappe.reload_doc("setup", "doctype", "company") company = frappe.qb.DocType("Company") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6774dafb68c..556535317d4 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1331,58 +1331,6 @@ class TestPurchaseReceipt(ERPNextTestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) - def test_service_item_purchase_with_perpetual_inventory(self): - company = '_Test Company with perpetual inventory' - service_item = '_Test Non Stock Item' - - before_test_value = frappe.db.get_value( - 'Company', company, 'enable_perpetual_inventory_for_non_stock_items' - ) - frappe.db.set_value( - 'Company', company, - 'enable_perpetual_inventory_for_non_stock_items', 1 - ) - srbnb_account = 'Stock Received But Not Billed - TCP1' - frappe.db.set_value( - 'Company', company, - 'service_received_but_not_billed', srbnb_account - ) - - pr = make_purchase_receipt( - company=company, item=service_item, - warehouse='Finished Goods - TCP1', do_not_save=1 - ) - item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) - item_row_with_diff_rate.rate = 100 - pr.append('items', item_row_with_diff_rate) - - pr.save() - pr.submit() - - item_one_gl_entry = frappe.db.get_all("GL Entry", { - 'voucher_type': pr.doctype, - 'voucher_no': pr.name, - 'account': srbnb_account, - 'voucher_detail_no': pr.items[0].name - }, pluck="name") - - item_two_gl_entry = frappe.db.get_all("GL Entry", { - 'voucher_type': pr.doctype, - 'voucher_no': pr.name, - 'account': srbnb_account, - 'voucher_detail_no': pr.items[1].name - }, pluck="name") - - # check if the entries are not merged into one - # seperate entries should be made since voucher_detail_no is different - self.assertEqual(len(item_one_gl_entry), 1) - self.assertEqual(len(item_two_gl_entry), 1) - - frappe.db.set_value( - 'Company', company, - 'enable_perpetual_inventory_for_non_stock_items', before_test_value - ) - def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, From ffbf7ec3b748e91bc954ed596e67da2671aced18 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 17:44:13 +0530 Subject: [PATCH 027/133] fix: don't show "create" on cancelled BOMs (#29570) (#29572) (cherry picked from commit a3a05c0c23be03edf46353ee3121426485eca36f) Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 34d6d012418..f24fd24d1ff 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -93,7 +93,7 @@ frappe.ui.form.on("BOM", { }); } - if(frm.doc.docstatus!=0) { + if(frm.doc.docstatus==1) { frm.add_custom_button(__("Work Order"), function() { frm.trigger("make_work_order"); }, __("Create")); From 535e6d61f026f52af41c79d21d225a47dde8a72c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 20:22:49 +0530 Subject: [PATCH 028/133] fix: employee reminders fixes (backport #29548) (#29576) Co-authored-by: Rucha Mahabal --- .../hr/doctype/employee/employee_reminders.py | 23 ++- .../employee/test_employee_reminders.py | 139 ++++++++++++++---- 2 files changed, 127 insertions(+), 35 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py index 559bd393e62..0bb66374d1e 100644 --- a/erpnext/hr/doctype/employee/employee_reminders.py +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -20,6 +20,7 @@ def send_reminders_in_advance_weekly(): send_advance_holiday_reminders("Weekly") + def send_reminders_in_advance_monthly(): to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders")) frequency = frappe.db.get_single_value("HR Settings", "frequency") @@ -28,6 +29,7 @@ def send_reminders_in_advance_monthly(): send_advance_holiday_reminders("Monthly") + def send_advance_holiday_reminders(frequency): """Send Holiday Reminders in Advance to Employees `frequency` (str): 'Weekly' or 'Monthly' @@ -42,7 +44,7 @@ def send_advance_holiday_reminders(frequency): else: return - employees = frappe.db.get_all('Employee', pluck='name') + employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name') for employee in employees: holidays = get_holidays_for_employee( employee, @@ -51,10 +53,13 @@ def send_advance_holiday_reminders(frequency): raise_exception=False ) - if not (holidays is None): - send_holidays_reminder_in_advance(employee, holidays) + send_holidays_reminder_in_advance(employee, holidays) + def send_holidays_reminder_in_advance(employee, holidays): + if not holidays: + return + employee_doc = frappe.get_doc('Employee', employee) employee_email = get_employee_email(employee_doc) frequency = frappe.db.get_single_value("HR Settings", "frequency") @@ -101,6 +106,7 @@ def send_birthday_reminders(): reminder_text, message = get_birthday_reminder_text_and_message(others) send_birthday_reminder(person_email, reminder_text, others, message) + def get_birthday_reminder_text_and_message(birthday_persons): if len(birthday_persons) == 1: birthday_person_text = birthday_persons[0]['name'] @@ -116,6 +122,7 @@ def get_birthday_reminder_text_and_message(birthday_persons): return reminder_text, message + def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): frappe.sendmail( recipients=recipients, @@ -129,10 +136,12 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message) header=_("Birthday Reminder 🎂") ) + def get_employees_who_are_born_today(): """Get all employee born today & group them based on their company""" return get_employees_having_an_event_today("birthday") + def get_employees_having_an_event_today(event_type): """Get all employee who have `event_type` today & group them based on their company. `event_type` @@ -210,13 +219,14 @@ def send_work_anniversary_reminders(): reminder_text, message = get_work_anniversary_reminder_text_and_message(others) send_work_anniversary_reminder(person_email, reminder_text, others, message) + def get_work_anniversary_reminder_text_and_message(anniversary_persons): if len(anniversary_persons) == 1: anniversary_person = anniversary_persons[0]['name'] persons_name = anniversary_person # Number of years completed at the company completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year - anniversary_person += f" completed {completed_years} years" + anniversary_person += f" completed {completed_years} year(s)" else: person_names_with_years = [] names = [] @@ -225,7 +235,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): names.append(person_text) # Number of years completed at the company completed_years = getdate().year - person['date_of_joining'].year - person_text += f" completed {completed_years} years" + person_text += f" completed {completed_years} year(s)" person_names_with_years.append(person_text) # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim @@ -239,6 +249,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): return reminder_text, message + def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): frappe.sendmail( recipients=recipients, @@ -249,5 +260,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person anniversary_persons=anniversary_persons, message=message, ), - header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️") + header=_("Work Anniversary Reminder") ) diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py index 52c00982443..bdb51b008a9 100644 --- a/erpnext/hr/doctype/employee/test_employee_reminders.py +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -5,10 +5,12 @@ import unittest from datetime import timedelta import frappe -from frappe.utils import getdate +from frappe.utils import add_months, getdate +from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change +from erpnext.hr.utils import get_holidays_for_employee class TestEmployeeReminders(unittest.TestCase): @@ -46,6 +48,24 @@ class TestEmployeeReminders(unittest.TestCase): cls.test_employee = test_employee cls.test_holiday_dates = test_holiday_dates + # Employee without holidays in this month/week + test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company") + test_employee_2 = frappe.get_doc('Employee', test_employee_2) + + test_holiday_list = make_holiday_list( + 'TestHolidayRemindersList2', + holiday_dates=[ + {'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'}, + ], + from_date=add_months(getdate(), -2), + to_date=add_months(getdate(), 2) + ) + test_employee_2.holiday_list = test_holiday_list.name + test_employee_2.save() + + cls.test_employee_2 = test_employee_2 + cls.holiday_list_2 = test_holiday_list + @classmethod def get_test_holiday_dates(cls): today_date = getdate() @@ -61,6 +81,7 @@ class TestEmployeeReminders(unittest.TestCase): def setUp(self): # Clear Email Queue frappe.db.sql("delete from `tabEmail Queue`") + frappe.db.sql("delete from `tabEmail Queue Recipient`") def test_is_holiday(self): from erpnext.hr.doctype.employee.employee import is_holiday @@ -103,11 +124,10 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) def test_work_anniversary_reminders(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:] - employee.company_email = "test@example.com" - employee.company = "_Test Company" - employee.save() + make_employee("test_work_anniversary@gmail.com", + date_of_joining="1998" + frappe.utils.nowdate()[4:], + company="_Test Company", + ) from erpnext.hr.doctype.employee.employee_reminders import ( get_employees_having_an_event_today, @@ -115,7 +135,12 @@ class TestEmployeeReminders(unittest.TestCase): ) employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') - self.assertTrue(employees_having_work_anniversary.get("_Test Company")) + employees = employees_having_work_anniversary.get("_Test Company") or [] + user_ids = [] + for entry in employees: + user_ids.append(entry.user_id) + + self.assertTrue("test_work_anniversary@gmail.com" in user_ids) hr_settings = frappe.get_doc("HR Settings", "HR Settings") hr_settings.send_work_anniversary_reminders = 1 @@ -126,16 +151,24 @@ class TestEmployeeReminders(unittest.TestCase): email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) - def test_send_holidays_reminder_in_advance(self): - from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance - from erpnext.hr.utils import get_holidays_for_employee + def test_work_anniversary_reminder_not_sent_for_0_years(self): + make_employee("test_work_anniversary_2@gmail.com", + date_of_joining=getdate(), + company="_Test Company", + ) - # Get HR settings and enable advance holiday reminders - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.send_holiday_reminders = 1 - set_proceed_with_frequency_change() - hr_settings.frequency = 'Weekly' - hr_settings.save() + from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today + + employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + employees = employees_having_work_anniversary.get("_Test Company") or [] + user_ids = [] + for entry in employees: + user_ids.append(entry.user_id) + + self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids) + + def test_send_holidays_reminder_in_advance(self): + setup_hr_settings('Weekly') holidays = get_holidays_for_employee( self.test_employee.get('name'), @@ -151,32 +184,80 @@ class TestEmployeeReminders(unittest.TestCase): email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertEqual(len(email_queue), 1) + self.assertTrue("Holidays this Week." in email_queue[0].message) def test_advance_holiday_reminders_monthly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly - # Get HR settings and enable advance holiday reminders - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.send_holiday_reminders = 1 - set_proceed_with_frequency_change() - hr_settings.frequency = 'Monthly' - hr_settings.save() + setup_hr_settings('Monthly') + + # disable emp 2, set same holiday list + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.test_employee.holiday_list + }) send_reminders_in_advance_monthly() - email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) + # even though emp 2 has holiday, non-active employees should not be recipients + recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + self.assertTrue(self.test_employee_2.user_id not in recipients) + + # teardown: enable emp 2 + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.holiday_list_2 + }) + def test_advance_holiday_reminders_weekly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly - # Get HR settings and enable advance holiday reminders - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.send_holiday_reminders = 1 - hr_settings.frequency = 'Weekly' - hr_settings.save() + setup_hr_settings('Weekly') + + # disable emp 2, set same holiday list + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.test_employee.holiday_list + }) send_reminders_in_advance_weekly() - email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) + + # even though emp 2 has holiday, non-active employees should not be recipients + recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + self.assertTrue(self.test_employee_2.user_id not in recipients) + + # teardown: enable emp 2 + frappe.db.set_value('Employee', self.test_employee_2.name, { + 'status': 'Left', + 'holiday_list': self.holiday_list_2 + }) + + def test_reminder_not_sent_if_no_holdays(self): + setup_hr_settings('Monthly') + + # reminder not sent if there are no holidays + holidays = get_holidays_for_employee( + self.test_employee_2.get('name'), + getdate(), getdate() + timedelta(days=3), + only_non_weekly=True, + raise_exception=False + ) + send_holidays_reminder_in_advance( + self.test_employee_2.get('name'), + holidays + ) + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertEqual(len(email_queue), 0) + + +def setup_hr_settings(frequency=None): + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + set_proceed_with_frequency_change() + hr_settings.frequency = frequency or 'Weekly' + hr_settings.save() \ No newline at end of file From 4e58c1da3ac1649d64377d53aec68e489a4a9d41 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 2 Feb 2022 14:56:11 +0530 Subject: [PATCH 029/133] fix: ordered_qty for production-plan-item --- erpnext/manufacturing/doctype/work_order/work_order.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index b12e157390f..fc72639be68 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -449,7 +449,13 @@ class WorkOrder(Document): def update_ordered_qty(self): if self.production_plan and self.production_plan_item: - qty = self.qty if self.docstatus == 1 else 0 + qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") + + if self.docstatus == 1: + qty += self.qty + elif self.docstatus == 2: + qty -= self.qty + frappe.db.set_value('Production Plan Item', self.production_plan_item, 'ordered_qty', qty) From 0608f61b808600b0d8f933f7486215f329e4dda0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Feb 2022 17:51:46 +0530 Subject: [PATCH 030/133] fix: Program Enrollment tests (backport #29592) (#29594) Co-authored-by: Rucha Mahabal --- .../doctype/program_enrollment/program_enrollment.py | 12 ++++++++++-- .../hr/doctype/employee/test_employee_reminders.py | 8 ++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index a23d49267e6..4d0f3a98011 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -6,6 +6,7 @@ import frappe from frappe import _, msgprint from frappe.desk.reportview import get_match_cond from frappe.model.document import Document +from frappe.query_builder.functions import Min from frappe.utils import comma_and, get_link_to_form, getdate @@ -60,8 +61,15 @@ class ProgramEnrollment(Document): frappe.throw(_("Student is already enrolled.")) def update_student_joining_date(self): - date = frappe.db.sql("select min(enrollment_date) from `tabProgram Enrollment` where student= %s", self.student) - frappe.db.set_value("Student", self.student, "joining_date", date) + table = frappe.qb.DocType('Program Enrollment') + date = ( + frappe.qb.from_(table) + .select(Min(table.enrollment_date).as_('enrollment_date')) + .where(table.student == self.student) + ).run(as_dict=True) + + if date: + frappe.db.set_value("Student", self.student, "joining_date", date[0].enrollment_date) def make_fee_records(self): from erpnext.education.api import get_fee_components diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py index bdb51b008a9..a4097ab9d19 100644 --- a/erpnext/hr/doctype/employee/test_employee_reminders.py +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -207,8 +207,8 @@ class TestEmployeeReminders(unittest.TestCase): # teardown: enable emp 2 frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Left', - 'holiday_list': self.holiday_list_2 + 'status': 'Active', + 'holiday_list': self.holiday_list_2.name }) def test_advance_holiday_reminders_weekly(self): @@ -232,8 +232,8 @@ class TestEmployeeReminders(unittest.TestCase): # teardown: enable emp 2 frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Left', - 'holiday_list': self.holiday_list_2 + 'status': 'Active', + 'holiday_list': self.holiday_list_2.name }) def test_reminder_not_sent_if_no_holdays(self): From e2f5ddb20a71e26af9e5fe3cdeb51e2382001894 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 2 Feb 2022 19:33:48 +0530 Subject: [PATCH 031/133] fix(pos): pricing rule on transactions doesn't work (cherry picked from commit e082e3f702a4bc8993411115504baa3e3ff1d996) --- erpnext/selling/page/point_of_sale/pos_controller.js | 7 ++++++- erpnext/selling/page/point_of_sale/pos_item_cart.js | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ce74f6d0a58..bee9400e734 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -248,7 +248,7 @@ erpnext.PointOfSale.Controller = class { numpad_event: (value, action) => this.update_item_field(value, action), - checkout: () => this.payment.checkout(), + checkout: () => this.save_and_checkout(), edit_cart: () => this.payment.edit_cart(), @@ -707,4 +707,9 @@ erpnext.PointOfSale.Controller = class { }) .catch(e => console.log(e)); } + + async save_and_checkout() { + this.frm.is_dirty() && await this.frm.save(); + this.payment.checkout(); + } }; diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 4920584d95e..4a99f068cd5 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -191,10 +191,10 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = ''; }); - this.$component.on('click', '.checkout-btn', function() { + this.$component.on('click', '.checkout-btn', async function() { if ($(this).attr('style').indexOf('--blue-500') == -1) return; - me.events.checkout(); + await me.events.checkout(); me.toggle_checkout_btn(false); me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); @@ -985,6 +985,7 @@ erpnext.PointOfSale.ItemCart = class { $(frm.wrapper).off('refresh-fields'); $(frm.wrapper).on('refresh-fields', () => { if (frm.doc.items.length) { + this.$cart_items_wrapper.html(''); frm.doc.items.forEach(item => { this.update_item_html(item); }); From 36609c50c0332498ee088e6f44759f7c2747c733 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 2 Feb 2022 23:02:56 +0530 Subject: [PATCH 032/133] test: add test case for multiple WO --- .../production_plan/test_production_plan.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 21a126b2a79..276e70859e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -385,6 +385,61 @@ class TestProductionPlan(ERPNextTestCase): # lowest most level of subassembly should be first self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) + def test_multiple_work_order_for_production_plan_item(self): + def create_work_order(item, pln, qty): + # Get Production Items + items_data = pln.get_production_items() + + # Update qty + items_data[(item, None, None)]["qty"] = qty + + # Create and Submit Work Order for each item in items_data + for key, item in items_data.items(): + if pln.sub_assembly_items: + item['use_multi_level_bom'] = 0 + + wo_name = pln.create_work_order(item) + wo_doc = frappe.get_doc("Work Order", wo_name) + wo_doc.update({ + 'wip_warehouse': 'Work In Progress - _TC', + 'fg_warehouse': 'Finished Goods - _TC' + }) + wo_doc.submit() + wo_list.append(wo_name) + + item = "Test Production Item 1" + raw_materials = ["Raw Material Item 1", "Raw Material Item 2"] + + # Create BOM + bom = make_bom(item=item, raw_materials=raw_materials) + + # Create Production Plan + pln = create_production_plan(item_code=bom.item, planned_qty=10) + + # All the created Work Orders + wo_list = [] + + # Create and Submit 1st Work Order for 5 qty + create_work_order(item, pln, 5) + pln.reload() + self.assertEqual(pln.po_items[0].ordered_qty, 5) + + # Create and Submit 2nd Work Order for 3 qty + create_work_order(item, pln, 3) + pln.reload() + self.assertEqual(pln.po_items[0].ordered_qty, 8) + + # Cancel 1st Work Order + wo1 = frappe.get_doc("Work Order", wo_list[0]) + wo1.cancel() + pln.reload() + self.assertEqual(pln.po_items[0].ordered_qty, 3) + + # Cancel 2nd Work Order + wo2 = frappe.get_doc("Work Order", wo_list[1]) + wo2.cancel() + pln.reload() + self.assertEqual(pln.po_items[0].ordered_qty, 0) def create_production_plan(**args): args = frappe._dict(args) From 754a3d65f0947f5bee191dba08f781963e48e2f4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:22:15 +0530 Subject: [PATCH 033/133] fix: ignore empty customer/supplier in item query (#29610) (#29613) * fix: dont try to filter by customer/supplier if None * test: item query with emtpy supplier (cherry picked from commit 41a95e56241ff8f3dceac7285f0bc6b9a43d7a06) Co-authored-by: Ankush Menat --- erpnext/controllers/queries.py | 3 +++ erpnext/controllers/tests/test_queries.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index ef5ee36a758..9324f07ec97 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -249,6 +249,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals del filters['customer'] else: del filters['supplier'] + else: + filters.pop('customer', None) + filters.pop('supplier', None) description_cond = '' diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 908d78c15bf..60d1733021c 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -56,6 +56,12 @@ class TestQueries(unittest.TestCase): bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1}) self.assertEqual(len(bundled_stock_items), 0) + # empty customer/supplier should be stripped of instead of failure + query(txt="", filters={"customer": None}) + query(txt="", filters={"customer": ""}) + query(txt="", filters={"supplier": None}) + query(txt="", filters={"supplier": ""}) + def test_bom_qury(self): query = add_default_params(queries.bom, "BOM") From 22ff462abbaa93989c2afa2c492aab898b72e00f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 3 Feb 2022 15:25:34 +0530 Subject: [PATCH 034/133] fix: add default if qty is not found --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index fc72639be68..c5daba58c3d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -449,7 +449,7 @@ class WorkOrder(Document): def update_ordered_qty(self): if self.production_plan and self.production_plan_item: - qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") + qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 if self.docstatus == 1: qty += self.qty From b93d8f8716feef75e7c1b05fe068652b75a65ff7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 17:17:33 +0530 Subject: [PATCH 035/133] feat: show stock value difference in stock ledger report (#29607) (#29621) (cherry picked from commit 04f6426dbf47a14b4a2950f2b3d373dd40d83347) Co-authored-by: Ankush Menat --- erpnext/stock/report/stock_ledger/stock_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index c60a6ca56ea..81fa0458f29 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -104,6 +104,7 @@ def get_columns(): {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, + {"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, From 596a3a65c20d2e698955f7c14424aa82c0020b6f Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 25 Jan 2022 18:38:41 +0530 Subject: [PATCH 036/133] fix: Incorrect packing list for recurring items & code cleanup - Fix Incorrect packing list for recurring items in the Items table - Re-organised functions based on external use and order of use - Deleted `clean_packing_list` function and reduced no.of loops - Raw SQL to QB - Minor formatting changes (cherry picked from commit 3f48fc1898f613b6629d5d2a38bafc452e68fd80) --- .../stock/doctype/packed_item/packed_item.py | 139 ++++++++++-------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index e4091c40dc4..676a841f5d3 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -16,37 +16,72 @@ from erpnext.stock.get_item_details import get_item_details class PackedItem(Document): pass +def make_packing_list(doc): + """make packing list for Product Bundle item""" + if doc.get("_action") and doc._action == "update_after_submit": + return + + if not doc.is_new(): + reset_packing_list_if_deleted_items_exist(doc) + + parent_items = [] + for item in doc.get("items"): + if frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + for bundle_item in get_product_bundle_items(item.item_code): + update_packing_list_item( + doc=doc, packing_item_code=bundle_item.item_code, + qty=flt(bundle_item.qty) * flt(item.stock_qty), + main_item_row=item, description=bundle_item.description + ) + + if [item.item_code, item.name] not in parent_items: + parent_items.append([item.item_code, item.name]) + + if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"): + update_product_bundle_price(doc, parent_items) + +def reset_packing_list_if_deleted_items_exist(doc): + doc_before_save = doc.get_doc_before_save() + items_are_deleted = len(doc_before_save.get("items")) != len(doc.get("items")) + + if items_are_deleted: + doc.set("packed_items", []) + def get_product_bundle_items(item_code): - return frappe.db.sql("""select t1.item_code, t1.qty, t1.uom, t1.description - from `tabProduct Bundle Item` t1, `tabProduct Bundle` t2 - where t2.new_item_code=%s and t1.parent = t2.name order by t1.idx""", item_code, as_dict=1) + product_bundle = frappe.qb.DocType("Product Bundle") + product_bundle_item = frappe.qb.DocType("Product Bundle Item") -def get_packing_item_details(item, company): - return frappe.db.sql(""" - select i.item_name, i.is_stock_item, i.description, i.stock_uom, id.default_warehouse - from `tabItem` i LEFT JOIN `tabItem Default` id ON id.parent=i.name and id.company=%s - where i.name = %s""", - (company, item), as_dict = 1)[0] - -def get_bin_qty(item, warehouse): - det = frappe.db.sql("""select actual_qty, projected_qty from `tabBin` - where item_code = %s and warehouse = %s""", (item, warehouse), as_dict = 1) - return det and det[0] or frappe._dict() + query = ( + frappe.qb.from_(product_bundle_item) + .join(product_bundle).on(product_bundle_item.parent == product_bundle.name) + .select( + product_bundle_item.item_code, + product_bundle_item.qty, + product_bundle_item.uom, + product_bundle_item.description + ).where( + product_bundle.new_item_code == item_code + ).orderby( + product_bundle_item.idx + ) + ) + return query.run(as_dict=True) def update_packing_list_item(doc, packing_item_code, qty, main_item_row, description): + old_packed_items_map = None + if doc.amended_from: old_packed_items_map = get_old_packed_item_details(doc.packed_items) - else: - old_packed_items_map = False + item = get_packing_item_details(packing_item_code, doc.company) # check if exists exists = 0 for d in doc.get("packed_items"): - if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code: - if d.parent_detail_docname != main_item_row.name: - d.parent_detail_docname = main_item_row.name - + if (d.parent_item == main_item_row.item_code and + d.item_code == packing_item_code and + d.parent_detail_docname == main_item_row.name + ): pi, exists = d, 1 break @@ -69,7 +104,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip pi.batch_no = cstr(main_item_row.get("batch_no")) if not pi.target_warehouse: pi.target_warehouse = main_item_row.get("target_warehouse") - bin = get_bin_qty(packing_item_code, pi.warehouse) + bin = get_packed_item_bin_qty(packing_item_code, pi.warehouse) pi.actual_qty = flt(bin.get("actual_qty")) pi.projected_qty = flt(bin.get("projected_qty")) if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)): @@ -77,41 +112,23 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse -def make_packing_list(doc): - """make packing list for Product Bundle item""" - if doc.get("_action") and doc._action == "update_after_submit": return +def get_packing_item_details(item, company): + return frappe.db.sql(""" + select i.item_name, i.is_stock_item, i.description, i.stock_uom, id.default_warehouse + from `tabItem` i LEFT JOIN `tabItem Default` id ON id.parent=i.name and id.company=%s + where i.name = %s""", + (company, item), as_dict = 1)[0] - parent_items = [] - for d in doc.get("items"): - if frappe.db.get_value("Product Bundle", {"new_item_code": d.item_code}): - for i in get_product_bundle_items(d.item_code): - update_packing_list_item(doc, i.item_code, flt(i.qty)*flt(d.stock_qty), d, i.description) +def get_packed_item_bin_qty(item, warehouse): + det = frappe.db.sql("""select actual_qty, projected_qty from `tabBin` + where item_code = %s and warehouse = %s""", (item, warehouse), as_dict = 1) + return det and det[0] or frappe._dict() - if [d.item_code, d.name] not in parent_items: - parent_items.append([d.item_code, d.name]) - - cleanup_packing_list(doc, parent_items) - - if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"): - update_product_bundle_price(doc, parent_items) - -def cleanup_packing_list(doc, parent_items): - """Remove all those child items which are no longer present in main item table""" - delete_list = [] - for d in doc.get("packed_items"): - if [d.parent_item, d.parent_detail_docname] not in parent_items: - # mark for deletion from doclist - delete_list.append(d) - - if not delete_list: - return doc - - packed_items = doc.get("packed_items") - doc.set("packed_items", []) - - for d in packed_items: - if d not in delete_list: - add_item_to_packing_list(doc, d) +def get_old_packed_item_details(old_packed_items): + old_packed_items_map = {} + for items in old_packed_items: + old_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) + return old_packed_items_map def add_item_to_packing_list(doc, packed_item): doc.append("packed_items", { @@ -165,15 +182,12 @@ def update_parent_item_price(doc, parent_item_code, bundle_price): current_parent_item_price = parent_item_doc.amount if current_parent_item_price != bundle_price: parent_item_doc.amount = bundle_price - update_parent_item_rate(parent_item_doc, bundle_price) - -def update_parent_item_rate(parent_item_doc, bundle_price): - parent_item_doc.rate = bundle_price/parent_item_doc.qty + parent_item_doc.rate = bundle_price/(parent_item_doc.qty or 1) @frappe.whitelist() def get_items_from_product_bundle(args): - args = json.loads(args) - items = [] + args, items = json.loads(args), [] + bundled_items = get_product_bundle_items(args["item_code"]) for item in bundled_items: args.update({ @@ -187,8 +201,3 @@ def get_items_from_product_bundle(args): def on_doctype_update(): frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) -def get_old_packed_item_details(old_packed_items): - old_packed_items_map = {} - for items in old_packed_items: - old_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) - return old_packed_items_map From c59c5bf2c67dd7a591fed1306438ba1c9e5c3e96 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 25 Jan 2022 19:51:47 +0530 Subject: [PATCH 037/133] chore: SQL to QB & accomodate Update Items - `doc_before_save` does not exist via Update Items (updates stuff in the backend so doc isn't considered unsaved/dirty) - converted more raw sql to qb and ORM (cherry picked from commit f8a578654276e95e9b87be6aee6a68dc58c0d561) --- .../stock/doctype/packed_item/packed_item.py | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 676a841f5d3..3cdc134a1c2 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -42,7 +42,10 @@ def make_packing_list(doc): def reset_packing_list_if_deleted_items_exist(doc): doc_before_save = doc.get_doc_before_save() - items_are_deleted = len(doc_before_save.get("items")) != len(doc.get("items")) + if doc_before_save: + items_are_deleted = len(doc_before_save.get("items")) != len(doc.get("items")) + else: + items_are_deleted = True if items_are_deleted: doc.set("packed_items", []) @@ -112,17 +115,34 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse -def get_packing_item_details(item, company): - return frappe.db.sql(""" - select i.item_name, i.is_stock_item, i.description, i.stock_uom, id.default_warehouse - from `tabItem` i LEFT JOIN `tabItem Default` id ON id.parent=i.name and id.company=%s - where i.name = %s""", - (company, item), as_dict = 1)[0] +def get_packing_item_details(item_code, company): + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + query = ( + frappe.qb.from_(item) + .left_join(item_default) + .on( + (item_default.parent == item.name) + & (item_default.company == company) + ).select( + item.item_name, item.is_stock_item, + item.description, item.stock_uom, + item_default.default_warehouse + ).where( + item.name == item_code + ) + ) + return query.run(as_dict=True)[0] def get_packed_item_bin_qty(item, warehouse): - det = frappe.db.sql("""select actual_qty, projected_qty from `tabBin` - where item_code = %s and warehouse = %s""", (item, warehouse), as_dict = 1) - return det and det[0] or frappe._dict() + bin_data = frappe.db.get_values( + "Bin", + fieldname=["actual_qty", "projected_qty"], + filters={"item_code": item, "warehouse": warehouse}, + as_dict=True + ) + + return bin_data[0] if bin_data else {} def get_old_packed_item_details(old_packed_items): old_packed_items_map = {} From 6e7f1a300c71bc0dfec60dfe20de1db05f63e0bf Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 25 Jan 2022 23:52:52 +0530 Subject: [PATCH 038/133] fix: Linter and minor code refactor - Create an indexed map of stale packed items table to avoid loops to check if packed item row exists - Reset packed items if row deletion takes place - Renamed functions to self-explain them - Split long function - Reduce function calls inside function (makes it harder to follow through) (cherry picked from commit 4c677eafe958a448074b3efc859334c9a088be2c) --- erpnext/public/js/controllers/buying.js | 2 +- .../stock/doctype/packed_item/packed_item.py | 191 ++++++++++-------- 2 files changed, 103 insertions(+), 90 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 613f93cc3f3..93169d972e4 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -441,7 +441,7 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { type: "GET", method: "erpnext.stock.doctype.packed_item.packed_item.get_items_from_product_bundle", args: { - args: { + row: { item_code: args.product_bundle, quantity: args.quantity, parenttype: frm.doc.doctype, diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 3cdc134a1c2..9c9dfcb455a 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -8,7 +8,7 @@ import json import frappe from frappe.model.document import Document -from frappe.utils import cstr, flt +from frappe.utils import flt from erpnext.stock.get_item_details import get_item_details @@ -16,23 +16,26 @@ from erpnext.stock.get_item_details import get_item_details class PackedItem(Document): pass + def make_packing_list(doc): """make packing list for Product Bundle item""" - if doc.get("_action") and doc._action == "update_after_submit": - return + if doc.get("_action") and doc._action == "update_after_submit": return + + parent_items, reset = [], False + stale_packed_items_table = get_indexed_packed_items_table(doc) if not doc.is_new(): - reset_packing_list_if_deleted_items_exist(doc) + reset = reset_packing_list_if_deleted_items_exist(doc) - parent_items = [] for item in doc.get("items"): if frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): for bundle_item in get_product_bundle_items(item.item_code): - update_packing_list_item( - doc=doc, packing_item_code=bundle_item.item_code, - qty=flt(bundle_item.qty) * flt(item.stock_qty), - main_item_row=item, description=bundle_item.description + pi_row = add_packed_item_row( + doc=doc, packing_item=bundle_item, + main_item_row=item, packed_items_table=stale_packed_items_table, + reset=reset ) + update_packed_item_details(bundle_item, pi_row, item, doc) if [item.item_code, item.name] not in parent_items: parent_items.append([item.item_code, item.name]) @@ -40,15 +43,31 @@ def make_packing_list(doc): if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"): update_product_bundle_price(doc, parent_items) +def get_indexed_packed_items_table(doc): + """ + Create dict from stale packed items table like: + {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} + """ + indexed_table = {} + for packed_item in doc.get("packed_items"): + key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname) + indexed_table[key] = packed_item + + return indexed_table + def reset_packing_list_if_deleted_items_exist(doc): doc_before_save = doc.get_doc_before_save() - if doc_before_save: - items_are_deleted = len(doc_before_save.get("items")) != len(doc.get("items")) - else: - items_are_deleted = True + reset_table = False - if items_are_deleted: + if doc_before_save: + # reset table if items were deleted + reset_table = len(doc_before_save.get("items")) > len(doc.get("items")) + else: + reset_table = True # reset if via Update Items (cannot determine action) + + if reset_table: doc.set("packed_items", []) + return reset_table def get_product_bundle_items(item_code): product_bundle = frappe.qb.DocType("Product Bundle") @@ -70,52 +89,30 @@ def get_product_bundle_items(item_code): ) return query.run(as_dict=True) -def update_packing_list_item(doc, packing_item_code, qty, main_item_row, description): - old_packed_items_map = None +def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset): + """Add and return packed item row. + doc: Transaction document + packing_item (dict): Packed Item details + main_item_row (dict): Items table row corresponding to packed item + packed_items_table (dict): Packed Items table before save (indexed) + reset (bool): State if table is reset or preserved as is + """ + exists, pi_row = False, {} - if doc.amended_from: - old_packed_items_map = get_old_packed_item_details(doc.packed_items) - - item = get_packing_item_details(packing_item_code, doc.company) - - # check if exists - exists = 0 - for d in doc.get("packed_items"): - if (d.parent_item == main_item_row.item_code and - d.item_code == packing_item_code and - d.parent_detail_docname == main_item_row.name - ): - pi, exists = d, 1 - break + # check if row already exists in packed items table + key = (main_item_row.item_code, packing_item.item_code, main_item_row.name) + if packed_items_table.get(key): + pi_row, exists = packed_items_table.get(key), True if not exists: - pi = doc.append('packed_items', {}) + pi_row = doc.append('packed_items', {}) + elif reset: # add row if row exists but table is reset + pi_row.idx, pi_row.name = None, None + pi_row = doc.append('packed_items', pi_row) - pi.parent_item = main_item_row.item_code - pi.item_code = packing_item_code - pi.item_name = item.item_name - pi.parent_detail_docname = main_item_row.name - pi.uom = item.stock_uom - pi.qty = flt(qty) - pi.conversion_factor = main_item_row.conversion_factor - if description and not pi.description: - pi.description = description - if not pi.warehouse and not doc.amended_from: - pi.warehouse = (main_item_row.warehouse if ((doc.get('is_pos') or item.is_stock_item \ - or not item.default_warehouse) and main_item_row.warehouse) else item.default_warehouse) - if not pi.batch_no and not doc.amended_from: - pi.batch_no = cstr(main_item_row.get("batch_no")) - if not pi.target_warehouse: - pi.target_warehouse = main_item_row.get("target_warehouse") - bin = get_packed_item_bin_qty(packing_item_code, pi.warehouse) - pi.actual_qty = flt(bin.get("actual_qty")) - pi.projected_qty = flt(bin.get("projected_qty")) - if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)): - pi.batch_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].batch_no - pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no - pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse + return pi_row -def get_packing_item_details(item_code, company): +def get_packed_item_details(item_code, company): item = frappe.qb.DocType("Item") item_default = frappe.qb.DocType("Item Default") query = ( @@ -134,6 +131,44 @@ def get_packing_item_details(item_code, company): ) return query.run(as_dict=True)[0] +def update_packed_item_details(packing_item, pi_row, main_item_row, doc): + "Update additional packed item row details." + item = get_packed_item_details(packing_item.item_code, doc.company) + + prev_doc_packed_items_map = None + if doc.amended_from: + prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items) + + pi_row.parent_item = main_item_row.item_code + pi_row.parent_detail_docname = main_item_row.name + pi_row.item_code = packing_item.item_code + pi_row.item_name = item.item_name + pi_row.uom = item.stock_uom + pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty) + pi_row.conversion_factor = main_item_row.conversion_factor + + if not pi_row.description: + pi_row.description = packing_item.get("description") + + if not pi_row.warehouse and not doc.amended_from: + pi_row.warehouse = (main_item_row.warehouse if ((doc.get('is_pos') or item.is_stock_item \ + or not item.default_warehouse) and main_item_row.warehouse) else item.default_warehouse) + + # TODO batch_no, actual_batch_qty, incoming_rate + + if not pi_row.target_warehouse: + pi_row.target_warehouse = main_item_row.get("target_warehouse") + + bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse) + pi_row.actual_qty = flt(bin.get("actual_qty")) + pi_row.projected_qty = flt(bin.get("projected_qty")) + + if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)): + prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)) + pi_row.batch_no = prev_doc_row[0].batch_no + pi_row.serial_no = prev_doc_row[0].serial_no + pi_row.warehouse = prev_doc_row[0].warehouse + def get_packed_item_bin_qty(item, warehouse): bin_data = frappe.db.get_values( "Bin", @@ -144,37 +179,14 @@ def get_packed_item_bin_qty(item, warehouse): return bin_data[0] if bin_data else {} -def get_old_packed_item_details(old_packed_items): - old_packed_items_map = {} +def get_cancelled_doc_packed_item_details(old_packed_items): + prev_doc_packed_items_map = {} for items in old_packed_items: - old_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) - return old_packed_items_map - -def add_item_to_packing_list(doc, packed_item): - doc.append("packed_items", { - 'parent_item': packed_item.parent_item, - 'item_code': packed_item.item_code, - 'item_name': packed_item.item_name, - 'uom': packed_item.uom, - 'qty': packed_item.qty, - 'rate': packed_item.rate, - 'conversion_factor': packed_item.conversion_factor, - 'description': packed_item.description, - 'warehouse': packed_item.warehouse, - 'batch_no': packed_item.batch_no, - 'actual_batch_qty': packed_item.actual_batch_qty, - 'serial_no': packed_item.serial_no, - 'target_warehouse': packed_item.target_warehouse, - 'actual_qty': packed_item.actual_qty, - 'projected_qty': packed_item.projected_qty, - 'incoming_rate': packed_item.incoming_rate, - 'prevdoc_doctype': packed_item.prevdoc_doctype, - 'parent_detail_docname': packed_item.parent_detail_docname - }) + prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) + return prev_doc_packed_items_map def update_product_bundle_price(doc, parent_items): """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" - if not doc.get('items'): return @@ -204,17 +216,18 @@ def update_parent_item_price(doc, parent_item_code, bundle_price): parent_item_doc.amount = bundle_price parent_item_doc.rate = bundle_price/(parent_item_doc.qty or 1) -@frappe.whitelist() -def get_items_from_product_bundle(args): - args, items = json.loads(args), [] - bundled_items = get_product_bundle_items(args["item_code"]) +@frappe.whitelist() +def get_items_from_product_bundle(row): + row, items = json.loads(row), [] + + bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: - args.update({ + row.update({ "item_code": item.item_code, - "qty": flt(args["quantity"]) * flt(item.qty) + "qty": flt(row["quantity"]) * flt(item.qty) }) - items.append(get_item_details(args)) + items.append(get_item_details(row)) return items From cc22cbe50b5c3dacc6629af8b192f5f9410fe375 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Jan 2022 13:25:55 +0530 Subject: [PATCH 039/133] chore: Break updation logic into smaller functions - Smaller functions for updation - All calls visible from parent function to avoid context switching due to nested calls (cherry picked from commit 2c14ab0439b792f2914f744c5079e41c863d21a3) --- .../stock/doctype/packed_item/packed_item.py | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 9c9dfcb455a..81c84eeb18f 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -18,8 +18,9 @@ class PackedItem(Document): def make_packing_list(doc): - """make packing list for Product Bundle item""" - if doc.get("_action") and doc._action == "update_after_submit": return + "Make/Update packing list for Product Bundle Item." + if doc.get("_action") and doc._action == "update_after_submit": + return parent_items, reset = [], False stale_packed_items_table = get_indexed_packed_items_table(doc) @@ -27,18 +28,21 @@ def make_packing_list(doc): if not doc.is_new(): reset = reset_packing_list_if_deleted_items_exist(doc) - for item in doc.get("items"): - if frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): - for bundle_item in get_product_bundle_items(item.item_code): + for item_row in doc.get("items"): + if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): + for bundle_item in get_product_bundle_items(item_row.item_code): pi_row = add_packed_item_row( doc=doc, packing_item=bundle_item, - main_item_row=item, packed_items_table=stale_packed_items_table, + main_item_row=item_row, packed_items_table=stale_packed_items_table, reset=reset ) - update_packed_item_details(bundle_item, pi_row, item, doc) + item_data = get_packed_item_details(bundle_item.item_code, doc.company) + update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data) + update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc) + update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) - if [item.item_code, item.name] not in parent_items: - parent_items.append([item.item_code, item.name]) + if [item_row.item_code, item_row.name] not in parent_items: + parent_items.append([item_row.item_code, item_row.name]) if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"): update_product_bundle_price(doc, parent_items) @@ -47,6 +51,8 @@ def get_indexed_packed_items_table(doc): """ Create dict from stale packed items table like: {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} + + Use: to quickly retrieve/check if row existed in table instead of looping n times """ indexed_table = {} for packed_item in doc.get("packed_items"): @@ -131,30 +137,24 @@ def get_packed_item_details(item_code, company): ) return query.run(as_dict=True)[0] -def update_packed_item_details(packing_item, pi_row, main_item_row, doc): - "Update additional packed item row details." - item = get_packed_item_details(packing_item.item_code, doc.company) - - prev_doc_packed_items_map = None - if doc.amended_from: - prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items) - +def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data): pi_row.parent_item = main_item_row.item_code pi_row.parent_detail_docname = main_item_row.name pi_row.item_code = packing_item.item_code - pi_row.item_name = item.item_name - pi_row.uom = item.stock_uom + pi_row.item_name = item_data.item_name + pi_row.uom = item_data.stock_uom pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty) pi_row.conversion_factor = main_item_row.conversion_factor if not pi_row.description: pi_row.description = packing_item.get("description") - if not pi_row.warehouse and not doc.amended_from: - pi_row.warehouse = (main_item_row.warehouse if ((doc.get('is_pos') or item.is_stock_item \ - or not item.default_warehouse) and main_item_row.warehouse) else item.default_warehouse) - +def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc): # TODO batch_no, actual_batch_qty, incoming_rate + if not pi_row.warehouse and not doc.amended_from: + fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse) + pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse) + else item_data.default_warehouse) if not pi_row.target_warehouse: pi_row.target_warehouse = main_item_row.get("target_warehouse") @@ -163,6 +163,12 @@ def update_packed_item_details(packing_item, pi_row, main_item_row, doc): pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) +def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc): + "Update packed item row details from cancelled doc into amended doc." + prev_doc_packed_items_map = None + if doc.amended_from: + prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items) + if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)): prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)) pi_row.batch_no = prev_doc_row[0].batch_no @@ -216,6 +222,9 @@ def update_parent_item_price(doc, parent_item_code, bundle_price): parent_item_doc.amount = bundle_price parent_item_doc.rate = bundle_price/(parent_item_doc.qty or 1) +def on_doctype_update(): + frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) + @frappe.whitelist() def get_items_from_product_bundle(row): @@ -231,6 +240,5 @@ def get_items_from_product_bundle(row): return items -def on_doctype_update(): - frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) - +# TODO +# rewrite price calculation logic, theres so much redundancy and bad logic \ No newline at end of file From fc35a74f7630792c8fd1049b3395449fc9dcfa72 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Jan 2022 19:03:40 +0530 Subject: [PATCH 040/133] refactor: Price fetching and updation logic - fetch price from price list, use item master valuation rate as fallback fo0r packed item - use a item code, item row name map to maintain cumulative price - reset table if item in a row is replaced - loop over items table only to set price, lesser iterations than packed items table (cherry picked from commit 2f4d266ee132e34d81034321f47a0aca96ee1774) --- .../doctype/packed_item/packed_item.json | 5 +- .../stock/doctype/packed_item/packed_item.py | 89 +++++++++++-------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 830d5469bf0..d2d47897658 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -218,8 +218,6 @@ "label": "Conversion Factor" }, { - "fetch_from": "item_code.valuation_rate", - "fetch_if_empty": 1, "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1, @@ -232,7 +230,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-01 15:10:29.646399", + "modified": "2022-01-28 16:03:30.780111", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", @@ -240,5 +238,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 81c84eeb18f..e3b5795f4c9 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -10,7 +10,7 @@ import frappe from frappe.model.document import Document from frappe.utils import flt -from erpnext.stock.get_item_details import get_item_details +from erpnext.stock.get_item_details import get_item_details, get_price_list_rate class PackedItem(Document): @@ -22,7 +22,9 @@ def make_packing_list(doc): if doc.get("_action") and doc._action == "update_after_submit": return - parent_items, reset = [], False + parent_items_price, reset = {}, False + set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates") + stale_packed_items_table = get_indexed_packed_items_table(doc) if not doc.is_new(): @@ -39,13 +41,14 @@ def make_packing_list(doc): item_data = get_packed_item_details(bundle_item.item_code, doc.company) update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data) update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc) + update_packed_item_price_data(pi_row, item_data, doc) update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) - if [item_row.item_code, item_row.name] not in parent_items: - parent_items.append([item_row.item_code, item_row.name]) + if set_price_from_children: # create/update bundle item wise price dict + update_product_bundle_rate(parent_items_price, pi_row) - if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"): - update_product_bundle_price(doc, parent_items) + if parent_items_price: + set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item def get_indexed_packed_items_table(doc): """ @@ -66,8 +69,13 @@ def reset_packing_list_if_deleted_items_exist(doc): reset_table = False if doc_before_save: - # reset table if items were deleted - reset_table = len(doc_before_save.get("items")) > len(doc.get("items")) + # reset table if: + # 1. items were deleted + # 2. if bundle item replaced by another item (same no. of items but different items) + # we maintain list to maintain repeated item rows as well + items_before_save = [item.item_code for item in doc_before_save.get("items")] + items_after_save = [item.item_code for item in doc.get("items")] + reset_table = items_before_save != items_after_save else: reset_table = True # reset if via Update Items (cannot determine action) @@ -130,6 +138,7 @@ def get_packed_item_details(item_code, company): ).select( item.item_name, item.is_stock_item, item.description, item.stock_uom, + item.valuation_rate, item_default.default_warehouse ).where( item.name == item_code @@ -163,6 +172,22 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) +def update_packed_item_price_data(pi_row, item_data, doc): + "Set price as per price list or from the Item master." + if pi_row.rate: + return + + item_doc = frappe.get_cached_doc("Item", pi_row.item_code) + row_data = pi_row.as_dict().copy() + row_data.update({ + "company": doc.get("company"), + "price_list": doc.get("selling_price_list"), + "currency": doc.get("currency") + }) + rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") + + pi_row.rate = rate or item_data.get("valuation_rate") or 0.0 + def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc): "Update packed item row details from cancelled doc into amended doc." prev_doc_packed_items_map = None @@ -191,36 +216,27 @@ def get_cancelled_doc_packed_item_details(old_packed_items): prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) return prev_doc_packed_items_map -def update_product_bundle_price(doc, parent_items): - """Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" - if not doc.get('items'): - return +def update_product_bundle_rate(parent_items_price, pi_row): + """ + Update the price dict of Product Bundles based on the rates of the Items in the bundle. - parent_items_index = 0 - bundle_price = 0 + Stucture: + {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} + """ + key = (pi_row.parent_item, pi_row.parent_detail_docname) + rate = parent_items_price.get(key) + if not rate: + parent_items_price[key] = 0.0 - for bundle_item in doc.get("packed_items"): - if parent_items[parent_items_index][0] == bundle_item.parent_item: - bundle_item_rate = bundle_item.rate if bundle_item.rate else 0 - bundle_price += bundle_item.qty * bundle_item_rate - else: - update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price) + parent_items_price[key] += flt(pi_row.rate) - bundle_item_rate = bundle_item.rate if bundle_item.rate else 0 - bundle_price = bundle_item.qty * bundle_item_rate - parent_items_index += 1 - - # for the last product bundle - if doc.get("packed_items"): - update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price) - -def update_parent_item_price(doc, parent_item_code, bundle_price): - parent_item_doc = doc.get('items', {'item_code': parent_item_code})[0] - - current_parent_item_price = parent_item_doc.amount - if current_parent_item_price != bundle_price: - parent_item_doc.amount = bundle_price - parent_item_doc.rate = bundle_price/(parent_item_doc.qty or 1) +def set_product_bundle_rate_amount(doc, parent_items_price): + "Set cumulative rate and amount in bundle item." + for item in doc.get("items"): + bundle_rate = parent_items_price.get((item.item_code, item.name)) + if bundle_rate and bundle_rate != item.rate: + item.rate = bundle_rate + item.amount = flt(bundle_rate * item.qty) def on_doctype_update(): frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) @@ -239,6 +255,3 @@ def get_items_from_product_bundle(row): items.append(get_item_details(row)) return items - -# TODO -# rewrite price calculation logic, theres so much redundancy and bad logic \ No newline at end of file From f33a1e858fd540b3600d30dba98f435f0ff93e11 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 3 Feb 2022 17:01:33 +0530 Subject: [PATCH 041/133] test: Packed Items test file (cherry picked from commit 4e7b4dc9a845c8090e5c97010ce857caafb000dd) --- .../doctype/packed_item/test_packed_item.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 erpnext/stock/doctype/packed_item/test_packed_item.py diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py new file mode 100644 index 00000000000..074391c2282 --- /dev/null +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -0,0 +1,104 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.tests.utils import ERPNextTestCase, change_settings + + +class TestPackedItem(ERPNextTestCase): + "Test impact on Packed Items table in various scenarios." + @classmethod + def setUpClass(cls) -> None: + make_item("_Test Product Bundle X", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + make_item("_Test Normal Stock Item", {"is_stock_item": 1}) + + make_product_bundle( + "_Test Product Bundle X", + ["_Test Bundle Item 1", "_Test Bundle Item 2"], + qty=2 + ) + + def test_sales_order_adding_bundle_item(self): + "Test impact on packed items if bundle item row is added." + so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, + do_not_submit=True) + + self.assertEqual(so.items[0].qty, 1) + self.assertEqual(len(so.packed_items), 2) + self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1") + self.assertEqual(so.packed_items[0].qty, 2) + + def test_sales_order_updating_bundle_item(self): + "Test impact on packed items if bundle item row is updated." + so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, + do_not_submit=True) + + so.items[0].qty = 2 # change qty + so.save() + + self.assertEqual(so.packed_items[0].qty, 4) + self.assertEqual(so.packed_items[1].qty, 4) + + # change item code to non bundle item + so.items[0].item_code = "_Test Normal Stock Item" + so.save() + + self.assertEqual(len(so.packed_items), 0) + + def test_sales_order_recurring_bundle_item(self): + "Test impact on packed items if same bundle item is added and removed." + so_items = [] + for qty in [2, 4, 6, 8]: + so_items.append({ + "item_code": "_Test Product Bundle X", + "qty": qty, + "rate": 400, + "warehouse": "_Test Warehouse - _TC" + }) + + # create SO with recurring bundle item + so = make_sales_order(item_list=so_items, do_not_submit=True) + + # check alternate rows for qty + self.assertEqual(len(so.packed_items), 8) + self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2") + self.assertEqual(so.packed_items[1].qty, 4) + self.assertEqual(so.packed_items[3].qty, 8) + self.assertEqual(so.packed_items[5].qty, 12) + self.assertEqual(so.packed_items[7].qty, 16) + + # delete intermediate row (2nd) + del so.items[1] + so.save() + + # check alternate rows for qty + self.assertEqual(len(so.packed_items), 6) + self.assertEqual(so.packed_items[1].qty, 4) + self.assertEqual(so.packed_items[3].qty, 12) + self.assertEqual(so.packed_items[5].qty, 16) + + # delete last row + del so.items[2] + so.save() + + # check alternate rows for qty + self.assertEqual(len(so.packed_items), 4) + self.assertEqual(so.packed_items[1].qty, 4) + self.assertEqual(so.packed_items[3].qty, 12) + + @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) + def test_sales_order_bundle_item_cumulative_price(self): + "Test if Bundle Item rate is cumulative from packed items." + so = make_sales_order(item_code = "_Test Product Bundle X", qty=2, + do_not_submit=True) + + so.packed_items[0].rate = 150 + so.packed_items[1].rate = 200 + so.save() + + self.assertEqual(so.items[0].rate, 350) + self.assertEqual(so.items[0].amount, 700) From 3fea24f9dba46607f9ccaf3366ef21e74f9c6241 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 3 Feb 2022 17:07:11 +0530 Subject: [PATCH 042/133] fix: (Linter) import sequence (cherry picked from commit f18ec2d94705022fe6fd5ae87112f7e59be21651) --- erpnext/stock/doctype/packed_item/test_packed_item.py | 2 +- 1 file changed, 1 insertion(+), 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 074391c2282..ed4eecde1da 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,9 +1,9 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from erpnext.stock.doctype.item.test_item import make_item from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestCase, change_settings From 20f321a88980d231f52702e3d32e122022316152 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Feb 2022 12:01:07 +0530 Subject: [PATCH 043/133] fix: Incorrect tax template in Sales Invocie via data import --- erpnext/controllers/selling_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4ff851d7f94..bec0f8e2b80 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -74,7 +74,8 @@ class SellingController(StockController): doctype=self.doctype, company=self.company, posting_date=self.get('posting_date'), fetch_payment_terms_template=fetch_payment_terms_template, - party_address=self.customer_address, shipping_address=self.shipping_address_name) + party_address=self.customer_address, shipping_address=self.shipping_address_name, + company_address=self.get('company_address')) if not self.meta.get_field("sales_team"): party_details.pop("sales_team") self.update_if_missing(party_details) From 3342efc388417616804a3acdf9d18b5feb017ca9 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 4 Feb 2022 22:06:56 +0530 Subject: [PATCH 044/133] fix: Regenerate packed items on newly mapped doc - Cannot determine action on newly mapped DN that hasnt been inserted - Rows could have been deleted, updated, added, etc. before first save - In this case , reset packing table (cherry picked from commit bd41a99c8a5c9a8b49174727748ce237a1a87797) --- .../stock/doctype/packed_item/packed_item.py | 15 +++++---- .../doctype/packed_item/test_packed_item.py | 31 ++++++++++++++++--- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index e3b5795f4c9..07c2f1f0dd3 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -27,8 +27,7 @@ def make_packing_list(doc): stale_packed_items_table = get_indexed_packed_items_table(doc) - if not doc.is_new(): - reset = reset_packing_list_if_deleted_items_exist(doc) + reset = reset_packing_list(doc) for item_row in doc.get("items"): if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): @@ -64,20 +63,24 @@ def get_indexed_packed_items_table(doc): return indexed_table -def reset_packing_list_if_deleted_items_exist(doc): - doc_before_save = doc.get_doc_before_save() +def reset_packing_list(doc): + "Conditionally reset the table and return if it was reset or not." reset_table = False + doc_before_save = doc.get_doc_before_save() if doc_before_save: # reset table if: # 1. items were deleted # 2. if bundle item replaced by another item (same no. of items but different items) - # we maintain list to maintain repeated item rows as well + # we maintain list to track recurring item rows as well items_before_save = [item.item_code for item in doc_before_save.get("items")] items_after_save = [item.item_code for item in doc.get("items")] reset_table = items_before_save != items_after_save else: - reset_table = True # reset if via Update Items (cannot determine action) + # reset: if via Update Items OR + # if new mapped doc with packed items set (SO -> DN) + # (cannot determine action) + reset_table = True if reset_table: doc.set("packed_items", []) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index ed4eecde1da..5cbaa1ea669 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestCase, change_settings @@ -22,7 +23,7 @@ class TestPackedItem(ERPNextTestCase): qty=2 ) - def test_sales_order_adding_bundle_item(self): + def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, do_not_submit=True) @@ -32,7 +33,7 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1") self.assertEqual(so.packed_items[0].qty, 2) - def test_sales_order_updating_bundle_item(self): + def test_updating_bundle_item(self): "Test impact on packed items if bundle item row is updated." so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, do_not_submit=True) @@ -49,7 +50,7 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(so.packed_items), 0) - def test_sales_order_recurring_bundle_item(self): + def test_recurring_bundle_item(self): "Test impact on packed items if same bundle item is added and removed." so_items = [] for qty in [2, 4, 6, 8]: @@ -91,7 +92,7 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(so.packed_items[3].qty, 12) @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) - def test_sales_order_bundle_item_cumulative_price(self): + def test_bundle_item_cumulative_price(self): "Test if Bundle Item rate is cumulative from packed items." so = make_sales_order(item_code = "_Test Product Bundle X", qty=2, do_not_submit=True) @@ -102,3 +103,25 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(so.items[0].rate, 350) self.assertEqual(so.items[0].amount, 700) + + def test_newly_mapped_doc_packed_items(self): + "Test impact on packed items in newly mapped DN from SO." + so_items = [] + for qty in [2, 4]: + so_items.append({ + "item_code": "_Test Product Bundle X", + "qty": qty, + "rate": 400, + "warehouse": "_Test Warehouse - _TC" + }) + + # create SO with recurring bundle item + so = make_sales_order(item_list=so_items) + + dn = make_delivery_note(so.name) + dn.items[1].qty = 3 # change second row qty for inserting doc + dn.save() + + self.assertEqual(len(dn.packed_items), 4) + self.assertEqual(dn.packed_items[2].qty, 6) + self.assertEqual(dn.packed_items[3].qty, 6) \ No newline at end of file From 2628e2faffd7734c5fdf351775602da9928daa58 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 4 Feb 2022 23:02:45 +0530 Subject: [PATCH 045/133] fix: enable Allow on Submit for 'Is Active' field in Salary Structure (backport #29630) (#29643) Co-authored-by: Rucha Mahabal --- .../payroll/doctype/salary_structure/salary_structure.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json index 5dd1d701f02..8df995769d3 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -58,6 +58,7 @@ "width": "50%" }, { + "allow_on_submit": 1, "default": "Yes", "fieldname": "is_active", "fieldtype": "Select", @@ -232,10 +233,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-03-31 15:41:12.342380", + "modified": "2022-02-03 23:50:10.205676", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -271,5 +273,6 @@ ], "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 689f8cb537ca210cbfd1480192aecaee92575a68 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 18:29:47 +0530 Subject: [PATCH 046/133] fix: ignore cancelled svd while updating GLE for PR This happens because LCV cancels and reposts entries so unless filtered by non-cancelled entries you can randomly get old values. (cherry picked from commit 8858c703a894ec35f5e44a91b82a7cc1f40ba2b4) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 7 +++++-- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 165f6000ce7..c9be49f6868 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -535,8 +535,11 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: - for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): + stock_ledger_entries = frappe.get_all("Stock Ledger Entry", + fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], + filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0} + ) + for d in stock_ledger_entries: voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) valuation_tax_accounts = [d.account_head for d in self.get("taxes") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 47c81280f6b..efac6e4dc44 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -281,7 +281,7 @@ class PurchaseReceipt(BuyingController): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", "voucher_no": self.name, - "voucher_detail_no": d.name, "warehouse": d.warehouse}, "stock_value_difference") + "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") if not stock_value_diff: continue From 65d4670f457320006d4245c01ec085a09e4097e5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 20:15:39 +0530 Subject: [PATCH 047/133] test: regression test for LCV GL entries (cherry picked from commit 69c65afd729d87115ee469f2bf8240af43d55e65) --- .../test_landed_cost_voucher.py | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) 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 9204842b8f6..df8cadd7f86 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,10 +4,11 @@ import frappe -from frappe.utils import flt +from frappe.utils import add_to_date, flt, now from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.utils import update_gl_entries_after from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, @@ -28,7 +29,8 @@ class TestLandedCostVoucher(ERPNextTestCase): "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", + "is_cancelled": 0, }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) @@ -41,14 +43,39 @@ class TestLandedCostVoucher(ERPNextTestCase): "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", + "is_cancelled": 0, }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) - self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0) + # assert after submit + self.assertPurchaseReceiptLCVGLEntries(pr) + + # Mess up cancelled SLE modified timestamp to check + # if they aren't effective in any business logic. + frappe.db.set_value("Stock Ledger Entry", + { + "is_cancelled": 1, + "voucher_type": pr.doctype, + "voucher_no": pr.name + }, + "is_cancelled", 1, + modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True) + ) + + items, warehouses = pr.get_items_and_warehouses() + update_gl_entries_after(pr.posting_date, pr.posting_time, + warehouses, items, company=pr.company) + + # reassert after reposting + self.assertPurchaseReceiptLCVGLEntries(pr) + + + def assertPurchaseReceiptLCVGLEntries(self, pr): + gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) @@ -74,8 +101,8 @@ class TestLandedCostVoucher(ERPNextTestCase): for gle in gl_entries: if not gle.get('is_cancelled'): - self.assertEqual(expected_values[gle.account][0], gle.debit) - self.assertEqual(expected_values[gle.account][1], gle.credit) + self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}") + self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}") def test_landed_cost_voucher_against_purchase_invoice(self): From f5f1afde157a84a4846f835c709c3023f47102f8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Feb 2022 20:13:20 +0530 Subject: [PATCH 048/133] fix: Ignore linked invoices on Journal Entry cancel (cherry picked from commit 6f7ae6290779181d1ad6db11f716a50f8c94072a) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index a83ea65541c..3798b0fbdf8 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; }, refresh: function(frm) { From 3b47a55932a704e886657380fa1fd57868e117d4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 6 Feb 2022 11:35:23 +0530 Subject: [PATCH 049/133] fix: Billing status for zero amount ref doc (cherry picked from commit d6796924728e998c3331e3af8c58556b0bbd7db1) --- erpnext/controllers/status_updater.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 76a7cdab516..affde4aa8ab 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -400,6 +400,16 @@ class StatusUpdater(Document): ref_doc = frappe.get_doc(ref_dt, ref_dn) ref_doc.db_set("per_billed", per_billed) + + # set billling status + if hasattr(ref_doc, 'billing_status'): + if ref_doc.per_billed < 0.001: + ref_doc.db_set("billing_status", "Not Billed") + elif ref_doc.per_billed > 99.999999: + ref_doc.db_set("billing_status", "Fully Billed") + else: + ref_doc.db_set("billing_status", "Partly Billed") + ref_doc.set_status(update=True) def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"): From 62b71f9eddc79f11d863bfa70a22754d8c245a3c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 6 Feb 2022 11:35:40 +0530 Subject: [PATCH 050/133] fix: Add test case (cherry picked from commit c5726fd3da410874c4532887d2c625c54bf6de6a) --- .../doctype/sales_order/test_sales_order.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 9c0150ef77c..c9d857d951f 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1271,6 +1271,30 @@ class TestSalesOrder(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) + def test_zero_amount_sales_order_billing_status(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + so = make_sales_order(uom="Nos", do_not_save=1) + so.items[0].rate = 0 + so.save() + so.submit() + + self.assertEqual(so.net_total, 0) + self.assertEqual(so.billing_status, 'Not Billed') + + si = create_sales_invoice(qty=10, do_not_save=1) + si.price_list = '_Test Price List' + si.items[0].rate = 0 + si.items[0].price_list_rate = 0 + si.items[0].sales_order = so.name + si.items[0].so_detail = so.items[0].name + si.save() + si.submit() + + self.assertEqual(si.net_total, 0) + so.load_from_db() + self.assertEqual(so.billing_status, 'Fully Billed') + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable From d10423ef1f396b198958f629cd9579afdf409e61 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Feb 2022 22:35:36 +0530 Subject: [PATCH 051/133] fix: Incorrect provisional profit and loss in balance sheet (cherry picked from commit ed2c6b6637960fe67846f933049d288078993b92) --- erpnext/accounts/report/balance_sheet/balance_sheet.py | 6 +++--- erpnext/accounts/report/financial_statements.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index dc1f7aae42e..f10a5eab102 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity): opening_balance = 0 float_precision = cint(frappe.db.get_default("float_precision")) or 2 if asset: - opening_balance = flt(asset[0].get("opening_balance", 0), float_precision) + opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision) if liability: - opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision) + opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision) if equity: - opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision) + opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision) opening_balance = flt(opening_balance, float_precision) if opening_balance: diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7bba2fcbe7d..03afd1e5268 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -285,7 +285,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency total_row = { "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), - "currency": company_currency + "currency": company_currency, + "opening_balance": 0.0 } for row in out: @@ -297,6 +298,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency total_row.setdefault("total", 0.0) total_row["total"] += flt(row["total"]) + total_row["opening_balance"] += row["opening_balance"] row["total"] = "" if "total" in total_row: From aca788cbe0e2f3162ce30797693d30cf943673ad Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 3 Feb 2022 12:13:43 +0530 Subject: [PATCH 052/133] fix: Zero rated exports in GSTR-3B report (cherry picked from commit 233e6449fc7fe1d09b1a034bb3491aca0b39028d) --- .../regional/doctype/gstr_3b_report/gstr_3b_report.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 8445408e640..6b31bcc05fc 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -296,6 +296,10 @@ class GSTR3BReport(Document): inter_state_supply_details = {} for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): + gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') + place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' + export_type = self.invoice_detail_map.get(inv, {}).get('export_type') + for rate, items in items_based_on_rate.items(): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: @@ -303,9 +307,8 @@ class GSTR3BReport(Document): self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value elif item_code in self.is_non_gst: self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value - elif rate == 0: + elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'): self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value - #self.report_dict['sup_details']['osup_zero'][key] += tax_amount else: if inv in self.cgst_sgst_invoices: tax_rate = rate/2 @@ -316,9 +319,6 @@ class GSTR3BReport(Document): self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100) self.report_dict['sup_details']['osup_det']['txval'] += taxable_value - gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') - place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' - if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: inter_state_supply_details.setdefault((gst_category, place_of_supply), { From 3e79533112b00499b9c3b17c9f226f3b90ca79a2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 18:47:21 +0530 Subject: [PATCH 053/133] fix: dont show cancelled PO items in plan report (cherry picked from commit 6459ceaea14cc8422de83dbca060e1c69b0e1d13) --- .../production_planning_report.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 8368db6374b..e1e7225e057 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -172,10 +172,15 @@ class ProductionPlanReport(object): self.purchase_details = {} - for d in frappe.get_all("Purchase Order Item", + purchased_items = frappe.get_all("Purchase Order Item", fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"], - filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}, - group_by = "item_code, warehouse"): + filters={ + "item_code": ("in", self.item_codes), + "warehouse": ("in", self.warehouses), + "docstatus": 1, + }, + group_by = "item_code, warehouse") + for d in purchased_items: key = (d.item_code, d.warehouse) if key not in self.purchase_details: self.purchase_details.setdefault(key, d) From 8cadc2668d8ac739180a9f07d69079ba45dc73c5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 13:20:34 +0530 Subject: [PATCH 054/133] fix(ux): make stock entry type the title field (#29674) (#29675) (cherry picked from commit 5e6227e3d87cc54e842cb6f71f18083055c9a51a) Co-authored-by: Ankush Menat --- .../doctype/stock_entry/stock_entry.json | 19 ++++++------------- .../stock/doctype/stock_entry/stock_entry.py | 9 --------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 2f377788961..c38dfaa1c84 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -8,7 +8,6 @@ "engine": "InnoDB", "field_order": [ "items_section", - "title", "naming_series", "stock_entry_type", "outgoing_stock_entry", @@ -83,14 +82,6 @@ "fieldtype": "Section Break", "oldfieldtype": "Section Break" }, - { - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -353,9 +344,9 @@ }, { "fieldname": "scan_barcode", - "options": "Barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -628,10 +619,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-08-20 19:19:31.514846", + "modified": "2022-02-07 12:55:14.614077", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -698,6 +690,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "title", + "states": [], + "title_field": "stock_entry_type", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a5bf2397411..dc49f984f13 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -77,7 +77,6 @@ class StockEntry(StockController): self.validate_posting_time() self.validate_purpose() - self.set_title() self.validate_item() self.validate_customer_provided_item() self.validate_qty() @@ -1837,14 +1836,6 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) - def set_title(self): - if frappe.flags.in_import and self.title: - # Allow updating title during data import/update - return - - self.title = self.purpose - - @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): From 946e560c55b6cb2dda3ae87da5cb3e519f4412e2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Jan 2022 19:59:33 +0530 Subject: [PATCH 055/133] feat: Refund entry against loans (cherry picked from commit c68c70f8bc88d9b05d64774ba070a34c059b7d30) # Conflicts: # erpnext/loan_management/doctype/loan/loan.json # erpnext/patches.txt --- erpnext/loan_management/doctype/loan/loan.js | 21 +++++++- .../loan_management/doctype/loan/loan.json | 17 ++++++- erpnext/loan_management/doctype/loan/loan.py | 48 ++++++++++++++++--- .../loan_management/doctype/loan/test_loan.py | 24 ++++++++-- .../loan_disbursement/loan_disbursement.py | 4 +- .../doctype/loan_type/loan_type.js | 2 +- .../doctype/loan_type/loan_type.json | 18 +++++-- erpnext/patches.txt | 8 +++- .../v13_0/update_disbursement_account.py | 22 +++++++++ 9 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 erpnext/patches/v13_0/update_disbursement_account.py diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index f9c201ab603..940a1bbc000 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', { }); }); - $.each(["payment_account", "loan_account"], function (i, field) { + $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) { frm.set_query(field, function () { return { "filters": { @@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', { frm.add_custom_button(__('Loan Write Off'), function() { frm.trigger("make_loan_write_off_entry"); },__('Create')); + + frm.add_custom_button(__('Loan Refund'), function() { + frm.trigger("make_loan_refund"); + },__('Create')); } } frm.trigger("toggle_fields"); @@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', { }) }, + make_loan_refund: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name + }, + method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv", + callback: function (r) { + if (r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }) + }, + request_loan_closure: function(frm) { frappe.confirm(__("Do you really want to close this loan"), function() { diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index fe94e2cadd6..c8bd71d19ab 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "ACC-LOAN-.YYYY.-.#####", - "creation": "2019-08-29 17:29:18.176786", + "creation": "2022-01-25 10:30:02.294967", "doctype": "DocType", "document_type": "Document", "editable_grid": 1, @@ -34,6 +34,7 @@ "is_term_loan", "account_info", "mode_of_payment", + "disbursement_account", "payment_account", "column_break_9", "loan_account", @@ -356,12 +357,25 @@ "fieldtype": "Date", "label": "Closure Date", "read_only": 1 + }, + { + "fetch_from": "loan_type.disbursement_account", + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "read_only": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-10-20 08:28:16.796105", +======= + "modified": "2022-01-25 16:29:16.325501", +>>>>>>> c68c70f8bc (feat: Refund entry against loans) "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", @@ -391,5 +405,6 @@ "search_fields": "posting_date", "sort_field": "creation", "sort_order": "DESC", + "states": [], "track_changes": 1 } diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 94e0c55a775..f3914d51286 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -11,6 +11,7 @@ from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, n from six import string_types import erpnext +from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry from erpnext.controllers.accounts_controller import AccountsController from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( @@ -234,17 +235,15 @@ def request_loan_closure(loan, posting_date=None): loan_type = frappe.get_value('Loan', loan, 'loan_type') write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') - # checking greater than 0 as there may be some minor precision error - if not pending_amount: - frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') - elif pending_amount < write_off_limit: + if pending_amount and abs(pending_amount) < write_off_limit: # Auto create loan write off and update status as loan closure requested write_off = make_loan_write_off(loan) write_off.submit() - frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') - else: + elif pending_amount > 0: frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) + frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + @frappe.whitelist() def get_loan_application(loan_application): loan = frappe.get_doc("Loan Application", loan_application) @@ -401,4 +400,39 @@ def add_single_month(date): if getdate(date) == get_last_day(date): return get_last_day(add_months(date, 1)) else: - return add_months(date, 1) \ No newline at end of file + return add_months(date, 1) + +@frappe.whitelist() +def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0): + loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant', + 'loan_account', 'payment_account', 'posting_date', 'company', 'name', + 'total_payment', 'total_principal_paid'], as_dict=1) + + loan_details.doctype = 'Loan' + loan_details[loan_details.applicant_type.lower()] = loan_details.applicant + + if not amount: + amount = flt(loan_details.total_principal_paid - loan_details.total_payment) + + if amount < 0: + frappe.throw(_('No excess amount pending for refund')) + + refund_jv = get_payment_entry(loan_details, { + "party_type": loan_details.applicant_type, + "party_account": loan_details.loan_account, + "amount_field_party": 'debit_in_account_currency', + "amount_field_bank": 'credit_in_account_currency', + "amount": amount, + "bank_account": loan_details.payment_account + }) + + if reference_number: + refund_jv.cheque_no = reference_number + + if reference_date: + refund_jv.cheque_date = reference_date + + if submit: + refund_jv.submit() + + return refund_jv \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 1676c218c87..64156897829 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -42,16 +42,17 @@ class TestLoan(unittest.TestCase): create_loan_type("Personal Loan", 500000, 8.4, is_term_loan=1, mode_of_payment='Cash', + disbursement_account='Disbursement Account - _TC', payment_account='Payment Account - _TC', loan_account='Loan Account - _TC', interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') create_loan_security_type() create_loan_security() @@ -790,6 +791,18 @@ def create_loan_accounts(): "account_type": "Bank", }).insert(ignore_permissions=True) + if not frappe.db.exists("Account", "Disbursement Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Disbursement Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) + if not frappe.db.exists("Account", "Interest Income Account - _TC"): frappe.get_doc({ "doctype": "Account", @@ -815,7 +828,7 @@ def create_loan_accounts(): }).insert(ignore_permissions=True) def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, - mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, + mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, repayment_method=None, repayment_periods=None): if not frappe.db.exists("Loan Type", loan_name): @@ -829,6 +842,7 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i "penalty_interest_rate": penalty_interest_rate, "grace_period_in_days": grace_period_in_days, "mode_of_payment": mode_of_payment, + "disbursement_account": disbursement_account, "payment_account": payment_account, "loan_account": loan_account, "interest_income_account": interest_income_account, diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index e2d758b1b90..df3aadfb18d 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -122,7 +122,7 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ "account": loan_details.loan_account, - "against": loan_details.payment_account, + "against": loan_details.disbursement_account, "debit": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", @@ -137,7 +137,7 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ - "account": loan_details.payment_account, + "account": loan_details.disbursement_account, "against": loan_details.loan_account, "credit": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount, diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js index 04c89c45499..9f9137cfbcd 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.js +++ b/erpnext/loan_management/doctype/loan_type/loan_type.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Loan Type', { }); }); - $.each(["payment_account", "loan_account"], function (i, field) { + $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) { frm.set_query(field, function () { return { "filters": { diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index c0a5d2cda12..00337e4b4c3 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -19,9 +19,10 @@ "description", "account_details_section", "mode_of_payment", + "disbursement_account", "payment_account", - "loan_account", "column_break_12", + "loan_account", "interest_income_account", "penalty_income_account", "amended_from" @@ -79,7 +80,7 @@ { "fieldname": "payment_account", "fieldtype": "Link", - "label": "Payment Account", + "label": "Repayment Account", "options": "Account", "reqd": 1 }, @@ -149,15 +150,23 @@ "fieldtype": "Currency", "label": "Auto Write Off Amount ", "options": "Company:company:default_currency" + }, + { + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:10:57.368490", + "modified": "2022-01-25 16:23:57.009349", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -181,5 +190,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7cbb6fa0ae9..255caea5dca 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -342,8 +342,14 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning +<<<<<<< HEAD erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against -erpnext.patches.v13_0.enable_provisional_accounting \ No newline at end of file +erpnext.patches.v13_0.enable_provisional_accounting +======= +erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v13_0.update_exchange_rate_settings +erpnext.patches.v13_0.update_disbursement_account +>>>>>>> c68c70f8bc (feat: Refund entry against loans) diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py new file mode 100644 index 00000000000..c56fa8fdc62 --- /dev/null +++ b/erpnext/patches/v13_0/update_disbursement_account.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + + frappe.reload_doc("loan_management", "doctype", "loan_type") + frappe.reload_doc("loan_management", "doctype", "loan") + + loan_type = frappe.qb.DocType("Loan Type") + loan = frappe.qb.DocType("Loan") + + frappe.qb.update( + loan_type + ).set( + loan_type.disbursement_account, loan_type.payment_account + ).run() + + frappe.qb.update( + loan + ).set( + loan.disbursement_account, loan.payment_account + ).run() \ No newline at end of file From f4e2c1fad3e2fdfb7d430ec84755cbbff5092de9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 13:50:31 +0530 Subject: [PATCH 056/133] fix: Add disbursement accounts to tests (cherry picked from commit 8ece2845f2dd6cc382c21c7794ee9d533cf4ea9b) --- .../doctype/loan_application/test_loan_application.py | 2 +- .../doctype/loan_disbursement/test_loan_disbursement.py | 4 ++-- .../loan_interest_accrual/test_loan_interest_accrual.py | 4 ++-- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 1 + erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index d367e92ac49..640709c095f 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -15,7 +15,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( class TestLoanApplication(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) self.applicant = make_employee("kate_loan@loan.com", "_Test Company") make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 94ec84ea5db..10be750b449 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -44,8 +44,8 @@ class TestLoanDisbursement(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') create_loan_security_type() create_loan_security() diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 46aaaad9fd2..e8c77506fcb 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -30,8 +30,8 @@ class TestLoanInterestAccrual(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') create_loan_security_type() create_loan_security() diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c6f38972880..56a8bf727f2 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -197,6 +197,7 @@ class TestPayrollEntry(unittest.TestCase): create_loan_type("Car Loan", 500000, 8.4, is_term_loan=1, mode_of_payment='Cash', + disbursement_account='Disbursement Account - _TC', payment_account='Payment Account - _TC', loan_account='Loan Account - _TC', interest_income_account='Interest Income Account - _TC', diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index cc011a50c6b..8ef4fbb4565 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -374,6 +374,7 @@ class TestSalarySlip(unittest.TestCase): create_loan_type("Car Loan", 500000, 8.4, is_term_loan=1, mode_of_payment='Cash', + disbursement_account='Disbursement Account - _TC', payment_account='Payment Account - _TC', loan_account='Loan Account - _TC', interest_income_account='Interest Income Account - _TC', From 501ce907967228448c098f12a2fa0f2c471d81b3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 18 Jan 2022 21:15:27 +0100 Subject: [PATCH 057/133] feat: option to disable Item Tax Template (cherry picked from commit f60e040d69aa56f71a632b3d77474f60657be689) --- .../item_tax_template/item_tax_template.json | 22 +++++++++++++++++-- erpnext/controllers/queries.py | 5 +++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index 77c9e95b759..b42d712d88a 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "creation": "2018-11-22 22:45:00.370913", + "creation": "2022-01-19 01:09:13.297137", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, @@ -10,6 +10,9 @@ "field_order": [ "title", "company", + "column_break_3", + "disabled", + "section_break_5", "taxes" ], "fields": [ @@ -36,10 +39,24 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" } ], "links": [], - "modified": "2021-03-08 19:50:21.416513", + "modified": "2022-01-18 21:11:23.105589", "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", @@ -82,6 +99,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 9324f07ec97..f5c566023c6 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -740,6 +740,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_doc = frappe.get_cached_doc('Item', filters.get('item_code')) item_group = filters.get('item_group') + company = filters.get('company') taxes = item_doc.taxes or [] while item_group: @@ -748,7 +749,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_group = item_group_doc.parent_item_group if not taxes: - return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) + return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True) else: valid_from = filters.get('valid_from') valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from @@ -757,7 +758,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): 'item_code': filters.get('item_code'), 'posting_date': valid_from, 'tax_category': filters.get('tax_category'), - 'company': filters.get('company') + 'company': company } taxes = _get_item_tax_template(args, taxes, for_validate=True) From ec55251f6aee13876cf39da949b4af27d8246acc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 18 Jan 2022 21:16:29 +0100 Subject: [PATCH 058/133] feat: option to disable tax category (cherry picked from commit 663c594ead54ced046887bbd94a6a0c88fa7b8e8) --- .../doctype/tax_category/tax_category.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index f7145af44c3..44a339f31df 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -2,12 +2,13 @@ "actions": [], "allow_rename": 1, "autoname": "field:title", - "creation": "2018-11-22 23:38:39.668804", + "creation": "2022-01-19 01:09:28.920486", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "title" + "title", + "disabled" ], "fields": [ { @@ -18,14 +19,21 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-03 11:50:38.748872", + "modified": "2022-01-18 21:13:41.161017", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -65,5 +73,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 9396143009bce58305ae1e5b3affc74c66467b97 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 6 Feb 2022 16:55:24 +0530 Subject: [PATCH 059/133] fix: missing key in loan (cherry picked from commit d0043bdbace8cac51527c9998435aa5c4f438b25) --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 65099da49b4..c79ea30d67b 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -126,7 +126,7 @@ class LoanRepayment(AccountsController): def update_paid_amount(self): loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable', + 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', 'written_off_amount'], as_dict=1) loan.update({ From 4196f04f498c38bf2fa4337d4e05af7a4a5bab18 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 17:00:30 +0530 Subject: [PATCH 060/133] test: Add test case for repayment against partially disbursed loans (cherry picked from commit ef69d1fd385bfe740ce1765f1a4998d27456ce79) --- .../loan_management/doctype/loan/test_loan.py | 23 +++++++++++++++++++ .../doctype/loan_repayment/loan_repayment.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 1676c218c87..cb7337e8d62 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -679,6 +679,29 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEqual(loan.status, "Loan Closure Requested") + def test_loan_repayment_against_partially_disbursed_loan(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date) + + loan.load_from_db() + + self.assertEqual(loan.status, "Partially Disbursed") + create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + flt(loan.loan_amount/3)) + def test_loan_amount_write_off(self): pledge = [{ "loan_security": "Test Security 1", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c79ea30d67b..56ee2c0225c 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -154,7 +154,7 @@ class LoanRepayment(AccountsController): def mark_as_unpaid(self): loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable', + 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', 'written_off_amount'], as_dict=1) no_of_repayments = len(self.repayment_details) From 066ceb06cd29642e5c948f074d3fbe68df7a3fb4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 18:46:53 +0530 Subject: [PATCH 061/133] fix: Resolve conflicts --- erpnext/loan_management/doctype/loan/loan.json | 4 ---- erpnext/patches.txt | 7 +------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index c8bd71d19ab..dd723f38bdf 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -371,11 +371,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-10-20 08:28:16.796105", -======= "modified": "2022-01-25 16:29:16.325501", ->>>>>>> c68c70f8bc (feat: Refund entry against loans) "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 255caea5dca..0a7eb33f66b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -342,14 +342,9 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning -<<<<<<< HEAD erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting -======= -erpnext.patches.v14_0.delete_agriculture_doctypes -erpnext.patches.v13_0.update_exchange_rate_settings -erpnext.patches.v13_0.update_disbursement_account ->>>>>>> c68c70f8bc (feat: Refund entry against loans) +erpnext.patches.v13_0.update_disbursement_account \ No newline at end of file From c137b141e8ae3b3dcaf5fed78458fff1baa193df Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 7 Feb 2022 19:43:29 +0530 Subject: [PATCH 062/133] fix: use item_code instead of parent field in bom_stock_calculated report (#29684) (cherry picked from commit 72a812f18bfd27842156d7b1afb1f301fbead7ed) --- .../report/bom_stock_calculated/bom_stock_calculated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index 090a3e74fc8..26933523246 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -89,10 +89,10 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) def get_manufacturer_records(): - details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) + details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"]) manufacture_details = frappe._dict() for detail in details: - dic = manufacture_details.setdefault(detail.get('parent'), {}) + dic = manufacture_details.setdefault(detail.get('item_code'), {}) dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) From bd401810e4eabfd744c81b8e8eb75d92122ab17c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 17:28:21 +0530 Subject: [PATCH 063/133] fix: consider packed items too when reposting (cherry picked from commit c56d07dee3f0c35e211424d82fec628aea22d2e9) --- erpnext/controllers/stock_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8d17683953e..92f8ff0d20c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -184,8 +184,8 @@ class StockController(AccountsController): def get_items_and_warehouses(self): items, warehouses = [], [] - if hasattr(self, "items"): - item_doclist = self.get("items") + if hasattr(self, "items") or hasattr(self, "packed_items"): + item_doclist = (self.get("items") or []) + (self.get("packed_items") or []) elif self.doctype == "Stock Reconciliation": item_doclist = [] data = json.loads(self.reconciliation_json) From eeb8f4874d0aa858139fd24bd458d3457b121ad4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:02:34 +0530 Subject: [PATCH 064/133] refactor: simplify get_items_and_warehouses Also remove dead code related to stock reconciliation_json. (cherry picked from commit e6ab8df8f2c48932a7368c5ac69ebcac14cf015c) --- erpnext/controllers/stock_controller.py | 40 +++++++++++-------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 92f8ff0d20c..9be5c0d03f1 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -3,6 +3,7 @@ import json from collections import defaultdict +from typing import List, Tuple import frappe from frappe import _ @@ -181,33 +182,28 @@ class StockController(AccountsController): return details - def get_items_and_warehouses(self): - items, warehouses = [], [] + def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]: + """Get list of items and warehouses affected by a transaction""" - if hasattr(self, "items") or hasattr(self, "packed_items"): - item_doclist = (self.get("items") or []) + (self.get("packed_items") or []) - elif self.doctype == "Stock Reconciliation": - item_doclist = [] - data = json.loads(self.reconciliation_json) - for row in data[data.index(self.head_row)+1:]: - d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row)) - item_doclist.append(d) + if not (hasattr(self, "items") or hasattr(self, "packed_items")): + return [], [] - if item_doclist: - for d in item_doclist: - if d.item_code and d.item_code not in items: - items.append(d.item_code) + item_rows = (self.get("items") or []) + (self.get("packed_items") or []) - if d.get("warehouse") and d.warehouse not in warehouses: - warehouses.append(d.warehouse) + items = {d.item_code for d in item_rows if d.item_code} - if self.doctype == "Stock Entry": - if d.get("s_warehouse") and d.s_warehouse not in warehouses: - warehouses.append(d.s_warehouse) - if d.get("t_warehouse") and d.t_warehouse not in warehouses: - warehouses.append(d.t_warehouse) + warehouses = set() + for d in item_rows: + if d.get("warehouse"): + warehouses.add(d.warehouse) - return items, warehouses + if self.doctype == "Stock Entry": + if d.get("s_warehouse"): + warehouses.add(d.s_warehouse) + if d.get("t_warehouse"): + warehouses.add(d.t_warehouse) + + return list(items), list(warehouses) def get_stock_ledger_details(self): stock_ledger = {} From 6f6b474333da09d11b9e6a3b683da52f6a7c7754 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:29:40 +0530 Subject: [PATCH 065/133] fix: merge stock ledger item warehouse with doc's (cherry picked from commit 1022db04745b3c6f16710b43720fef4f0fd0d295) --- .../repost_item_valuation/repost_item_valuation.py | 13 ++++++++++--- 1 file changed, 10 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 3b76301b4cc..c6baa46c5eb 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -13,7 +13,7 @@ from erpnext.accounts.utils import ( check_if_stock_and_account_balance_synced, update_gl_entries_after, ) -from erpnext.stock.stock_ledger import repost_future_sle +from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle class RepostItemValuation(Document): @@ -138,13 +138,20 @@ def repost_gl_entries(doc): if doc.based_on == 'Transaction': ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) - items, warehouses = ref_doc.get_items_and_warehouses() + doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() + + sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no) + sle_items = [sle.item_code for sle in sles] + sle_warehouse = [sle.warehouse for sle in sles] + + items = list(set(doc_items).union(set(sle_items))) + warehouses = list(set(doc_warehouses).union(set(sle_warehouse))) else: items = [doc.item_code] warehouses = [doc.warehouse] update_gl_entries_after(doc.posting_date, doc.posting_time, - warehouses, items, company=doc.company) + for_warehouses=warehouses, for_items=items, company=doc.company) def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") From 86d30ed583d5269b62008e79b6b90e8038c68ad3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:42:44 +0530 Subject: [PATCH 066/133] test: move bundle info to class variables (cherry picked from commit 853e658dccf1a71fad03e23bf3b7d8f9d0784c37) --- .../doctype/packed_item/test_packed_item.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 5cbaa1ea669..fcb4727f6b8 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -5,6 +5,7 @@ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_prod from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order 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, change_settings @@ -12,31 +13,29 @@ class TestPackedItem(ERPNextTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: - make_item("_Test Product Bundle X", {"is_stock_item": 0}) - make_item("_Test Bundle Item 1", {"is_stock_item": 1}) - make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + cls.bundle = "_Test Product Bundle X" + cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] + make_item(cls.bundle, {"is_stock_item": 0}) + for item in cls.bundle_items: + make_item(item, {"is_stock_item": 1}) + make_item("_Test Normal Stock Item", {"is_stock_item": 1}) - make_product_bundle( - "_Test Product Bundle X", - ["_Test Bundle Item 1", "_Test Bundle Item 2"], - qty=2 - ) + make_product_bundle(cls.bundle, cls.bundle_items, qty=2) def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, + so = make_sales_order(item_code = self.bundle, qty=1, do_not_submit=True) self.assertEqual(so.items[0].qty, 1) self.assertEqual(len(so.packed_items), 2) - self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1") + self.assertEqual(so.packed_items[0].item_code, self.bundle_items[0]) self.assertEqual(so.packed_items[0].qty, 2) def test_updating_bundle_item(self): "Test impact on packed items if bundle item row is updated." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) so.items[0].qty = 2 # change qty so.save() @@ -55,7 +54,7 @@ class TestPackedItem(ERPNextTestCase): so_items = [] for qty in [2, 4, 6, 8]: so_items.append({ - "item_code": "_Test Product Bundle X", + "item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC" @@ -66,7 +65,7 @@ class TestPackedItem(ERPNextTestCase): # check alternate rows for qty self.assertEqual(len(so.packed_items), 8) - self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2") + self.assertEqual(so.packed_items[1].item_code, self.bundle_items[1]) self.assertEqual(so.packed_items[1].qty, 4) self.assertEqual(so.packed_items[3].qty, 8) self.assertEqual(so.packed_items[5].qty, 12) @@ -94,8 +93,7 @@ class TestPackedItem(ERPNextTestCase): @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) def test_bundle_item_cumulative_price(self): "Test if Bundle Item rate is cumulative from packed items." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=2, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=2, do_not_submit=True) so.packed_items[0].rate = 150 so.packed_items[1].rate = 200 @@ -109,7 +107,7 @@ class TestPackedItem(ERPNextTestCase): so_items = [] for qty in [2, 4]: so_items.append({ - "item_code": "_Test Product Bundle X", + "item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC" @@ -124,4 +122,4 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(dn.packed_items), 4) self.assertEqual(dn.packed_items[2].qty, 6) - self.assertEqual(dn.packed_items[3].qty, 6) \ No newline at end of file + self.assertEqual(dn.packed_items[3].qty, 6) From 19f99a1c5f9b32198d0b8a74ec06b23e52e71ae7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 14:19:58 +0530 Subject: [PATCH 067/133] test: product bundle reposting (cherry picked from commit 699519f7b69b0b5eaa53a0db2fb35857b080261c) --- .../doctype/packed_item/test_packed_item.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index fcb4727f6b8..2521ac9fe72 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,10 +1,13 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from frappe.utils import add_to_date, nowdate + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order 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 @@ -13,6 +16,7 @@ class TestPackedItem(ERPNextTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: + super().setUpClass() cls.bundle = "_Test Product Bundle X" cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] make_item(cls.bundle, {"is_stock_item": 0}) @@ -123,3 +127,32 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(dn.packed_items), 4) self.assertEqual(dn.packed_items[2].qty, 6) self.assertEqual(dn.packed_items[3].qty, 6) + + def test_reposting_packed_items(self): + warehouse = "Stores - TCP1" + company = "_Test Company with perpetual inventory" + + today = nowdate() + yesterday = add_to_date(today, days=-1, as_string=True) + + for item in self.bundle_items: + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today) + + so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + gles = get_gl_entries(dn.doctype, dn.name) + credit_before_repost = sum(gle.credit for gle in gles) + + # backdated stock entry + for item in self.bundle_items: + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) + + # assert correct reposting + gles = get_gl_entries(dn.doctype, dn.name) + 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) From 29d3a189cb3eb02ab7249b31a9114ecca386b834 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 14:28:55 +0530 Subject: [PATCH 068/133] chore: drop dead field from stock reconciliation (cherry picked from commit 43f8ee1dd1525b6cf3e88154bec314aca7b23ca5) --- .../stock_reconciliation.json | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 3402972bb89..a882a61e5a5 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -18,7 +18,6 @@ "items", "section_break_9", "expense_account", - "reconciliation_json", "column_break_13", "difference_amount", "amended_from", @@ -111,15 +110,6 @@ "label": "Cost Center", "options": "Cost Center" }, - { - "fieldname": "reconciliation_json", - "fieldtype": "Long Text", - "hidden": 1, - "label": "Reconciliation JSON", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "column_break_13", "fieldtype": "Column Break" @@ -155,7 +145,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-30 01:33:51.437194", + "modified": "2022-02-06 14:28:19.043905", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", @@ -178,5 +168,6 @@ "search_fields": "posting_date", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 8e660890f4e6202e2d336ef89c4d5b1d9a333eae Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 15:18:00 +0530 Subject: [PATCH 069/133] test: commit item/warehouse creation to db (cherry picked from commit f82d7eb73fe6dd70a98fa6656e90ed9c6565be16) --- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 428370cc758..86af0a0cf3b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -25,8 +25,8 @@ from erpnext.tests.utils import ERPNextTestCase, change_settings class TestStockReconciliation(ERPNextTestCase): @classmethod def setUpClass(cls): - super().setUpClass() create_batch_or_serial_no_items() + super().setUpClass() frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) def tearDown(self): From 52a74921c0ca8c113a3d71602473aa7c05f3fd05 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 10:49:37 +0530 Subject: [PATCH 070/133] chore: show credit/debit-to account in error message (cherry picked from commit e93bb8a3364c04c8a30b47f681e53747140e4fd9) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c9be49f6868..f3452e1cf81 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -176,8 +176,8 @@ class PurchaseInvoice(BuyingController): if self.supplier and account.account_type != "Payable": frappe.throw( - _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account") + _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.") + .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account") ) self.party_account_currency = account.account_currency diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 8927ce3d6ac..0317656e83b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -587,7 +587,10 @@ class SalesInvoice(SellingController): frappe.throw(msg, title=_("Invalid Account")) if self.customer and account.account_type != "Receivable": - msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " " + msg = _("Please ensure {} account {} is a Receivable account.").format( + frappe.bold("Debit To"), + frappe.bold(self.debit_to) + ) + " " msg += _("Change the account type to Receivable or select a different account.") frappe.throw(msg, title=_("Invalid Account")) From c494ed087a2aa4cbefbee2caf1491fae8aa5ce4c Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 2 Feb 2022 20:13:33 +0530 Subject: [PATCH 071/133] fix: Coupon code item pricing dynamic updation issue (cherry picked from commit ccf63124d62139c586a3d6737460a67a942956b1) --- erpnext/public/js/controllers/transaction.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 47454b9a789..3730f15078e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2269,7 +2269,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ () => this.frm.doc.ignore_pricing_rule=1, () => me.ignore_pricing_rule(), () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() + () => me.apply_pricing_rule(), + () => this.frm.save() ]); } else { frappe.run_serially([ From cc5107eab312d7854dbcf9b803b79d997aca91db Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 13:52:28 +0530 Subject: [PATCH 072/133] fix: ignore cancelled SLEs (#29679) (#29698) (cherry picked from commit 0ca60afc3fb7190cbba58ef42b84c51bffb9d660) Co-authored-by: Ankush Menat --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9be5c0d03f1..c8e5eddfeac 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -215,7 +215,7 @@ class StockController(AccountsController): from `tabStock Ledger Entry` where - voucher_type=%s and voucher_no=%s + voucher_type=%s and voucher_no=%s and is_cancelled = 0 """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: From bd9a07ed4ce1d79d5100f6c6414b009574acb02f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 13:52:41 +0530 Subject: [PATCH 073/133] fix: dont ignore items that dont have SVD (backport #29656) (#29694) * fix: dont ignore items that dont have SVD When items go from negative to positive stock value diff can be zero but item might have taxes / need divisional loss adjustment. (cherry picked from commit c88c368880a134b12ad82d7674cb5e12d5a858ba) * test: check when PR moves stock from neg to pos (cherry picked from commit d1f57538855eaa8c6598a553a8b776b3ab511171) Co-authored-by: Ankush Menat --- .../purchase_receipt/purchase_receipt.py | 3 -- .../purchase_receipt/test_purchase_receipt.py | 33 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index efac6e4dc44..afaa8b02a89 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -283,9 +283,6 @@ class PurchaseReceipt(BuyingController): {"voucher_type": "Purchase Receipt", "voucher_no": self.name, "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") - if not stock_value_diff: - continue - warehouse_account_name = warehouse_account[d.warehouse]["account"] warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 556535317d4..89f11ca78d4 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4,6 +4,7 @@ import json import unittest +from collections import defaultdict import frappe from frappe.utils import add_days, cint, cstr, flt, today @@ -17,7 +18,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.tests.utils import ERPNextTestCase +from erpnext.tests.utils import ERPNextTestCase, change_settings class TestPurchaseReceipt(ERPNextTestCase): @@ -1367,6 +1368,36 @@ class TestPurchaseReceipt(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) + @change_settings("Stock Settings", {"allow_negative_stock": 1}) + def test_neg_to_positive(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item_code = "_TestNegToPosItem" + warehouse = "Stores - TCP1" + company = "_Test Company with perpetual inventory" + account = "Stock Received But Not Billed - TCP1" + + make_item(item_code) + se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) + se.items[0].allow_zero_valuation_rate = 1 + se.save() + se.submit() + + pr = make_purchase_receipt( + qty=50, + rate=1, + item_code=item_code, + warehouse=warehouse, + get_taxes_and_charges=True, + company=company, + ) + gles = get_gl_entries(pr.doctype, pr.name) + + for gle in gles: + if gle.account == account: + self.assertEqual(gle.credit, 50) + + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 08b9ee7b369bd665712c446e32c92159a9c2f668 Mon Sep 17 00:00:00 2001 From: Bhavesh Maheshwari <34086262+bhavesh95863@users.noreply.github.com> Date: Tue, 8 Feb 2022 14:01:20 +0530 Subject: [PATCH 074/133] fix: ignore rate validation for work order (#29690) * fix: ignore rate validation for work order * test: validate on save instead of on creation Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 3 ++- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 3 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 f3beabddcf9..0e150fee23a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -700,7 +700,8 @@ class TestWorkOrder(ERPNextTestCase): wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, company=company) - self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture')) + self.assertRaises(frappe.ValidationError, stock_entry.save) def test_wo_completion_with_pl_bom(self): from erpnext.manufacturing.doctype.bom.test_bom import ( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index dc49f984f13..f7109ab6b0d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1116,7 +1116,7 @@ class StockEntry(StockController): self.set_actual_qty() self.update_items_for_process_loss() self.validate_customer_provided_item() - self.calculate_rate_and_amount() + self.calculate_rate_and_amount(raise_error_if_no_rate=False) def set_scrap_items(self): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: From e5112e16a85c6ede892025477b3a279227eba9ea Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Feb 2022 14:14:04 +0530 Subject: [PATCH 075/133] fix: typeerror on invoice creation from SO/PO (cherry picked from commit 9bd56b0f79af4970ce6c1762d647725fba4ebbf9) --- erpnext/controllers/accounts_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b7b198eac4c..582818406b9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1319,6 +1319,9 @@ class AccountsController(TransactionBase): payment_schedule['discount_type'] = schedule.discount_type payment_schedule['discount'] = schedule.discount + if not schedule.invoice_portion: + payment_schedule['payment_amount'] = schedule.payment_amount + self.append("payment_schedule", payment_schedule) def set_due_date(self): From 5363b1922ac2812ed2920f106ce1ed9c13cd2768 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 5 Feb 2022 16:05:46 +0530 Subject: [PATCH 076/133] fix: earned leaves not allocated if assignment is created on month-end (cherry picked from commit 25c7f850b14f1f423631225725ad7d2e9647049f) # Conflicts: # erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py --- .../leave_policy_assignment.py | 23 +++++++++++++++++-- erpnext/hr/utils.py | 22 ++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index c79216a275d..0ae368f9df4 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,8 +8,12 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate from six import string_types +======= +from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate +>>>>>>> 25c7f850b1 (fix: earned leaves not allocated if assignment is created on month-end) class LeavePolicyAssignment(Document): @@ -109,8 +113,8 @@ class LeavePolicyAssignment(Document): def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): from erpnext.hr.utils import get_monthly_earned_leave - current_month = get_datetime().month - current_year = get_datetime().year + current_month = get_datetime(frappe.flags.current_date).month or get_datetime().month + current_year = get_datetime(frappe.flags.current_date).year or get_datetime().year from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date") if getdate(date_of_joining) > getdate(from_date): @@ -120,10 +124,14 @@ class LeavePolicyAssignment(Document): from_date_year = get_datetime(from_date).year months_passed = 0 + if current_year == from_date_year and current_month > from_date_month: months_passed = current_month - from_date_month + months_passed = add_current_month_if_applicable(months_passed) + elif current_year > from_date_year: months_passed = (12 - from_date_month) + current_month + months_passed = add_current_month_if_applicable(months_passed) if months_passed > 0: monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, @@ -135,6 +143,17 @@ class LeavePolicyAssignment(Document): return new_leaves_allocated +def add_current_month_if_applicable(months_passed): + date = getdate(frappe.flags.current_date) or getdate() + last_day_of_month = get_last_day(date) + + # if its the last day of the month, then that month should also be considered + if last_day_of_month == date: + months_passed += 1 + + return months_passed + + @frappe.whitelist() def create_assignment_for_multiple_employees(employees, data): diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 0b2f99c358e..6cf7df371e7 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -393,9 +393,12 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type new_allocation = e_leave_type.max_leaves_allowed if new_allocation != allocation.total_leaves_allocated: - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) today_date = today() - create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + + if not is_earned_leave_already_allocated(allocation, annual_allocation): + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + def get_monthly_earned_leave(annual_leaves, frequency, rounding): earned_leaves = 0.0 @@ -413,6 +416,21 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding): return earned_leaves +def is_earned_leave_already_allocated(allocation, annual_allocation): + from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import get_leave_type_details + + leave_type_details = get_leave_type_details() + date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") + + assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) + leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, + annual_allocation, leave_type_details, date_of_joining) + + if allocation.total_leaves_allocated >= leaves_for_passed_months: + return True + return False + + def get_leave_allocations(date, leave_type): return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy from `tabLeave Allocation` From d26a8d47e4a4ad6faa47bcf67700dba28399efae Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 5 Feb 2022 16:05:54 +0530 Subject: [PATCH 077/133] test: earned leave allocation for passed months and allocation on month-end (cherry picked from commit 63ee4f1b64b0110d6d97f4114605db2732dcb224) --- .../test_leave_policy_assignment.py | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 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 8953a51e8bb..6dd589182fc 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, getdate +from frappe.utils import add_months, get_first_day, get_last_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -124,6 +124,69 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 0) + def test_earned_leave_allocation_for_passed_months(self): + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=get_first_day(add_months(getdate(), -1))) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + # Case 1: assignment created one month after the leave period, should allocate 1 leave + frappe.flags.current_date = get_first_day(getdate()) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 1) + + def test_earned_leave_allocation_for_passed_months_on_month_end(self): + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=get_first_day(add_months(getdate(), -2))) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + # Case 2: assignment created on the last day of the leave period's latter month + # should allocate 1 leave for current month even though the month has not ended + # since the daily job might have already executed + frappe.flags.current_date = get_last_day(getdate()) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + def tearDown(self): frappe.db.rollback() @@ -136,14 +199,14 @@ def create_earned_leave_type(leave_type): doctype="Leave Type", is_earned_leave=1, earned_leave_frequency="Monthly", - rounding=0.5, - max_leaves_allowed=6 + rounding=0.5 )).insert() -def create_leave_period(name): +def create_leave_period(name, start_date=None): frappe.delete_doc_if_exists("Leave Period", name, force=1) - start_date = get_first_day(getdate()) + if not start_date: + start_date = get_first_day(getdate()) return frappe.get_doc(dict( name=name, From ce26759a95b21d776e3cf24efd92d3b99ca776ff Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 5 Feb 2022 16:40:55 +0530 Subject: [PATCH 078/133] fix: linter (cherry picked from commit a52ba0a5447df1998b7900230ce1cdb4a0a3dace) --- erpnext/hr/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 6cf7df371e7..4ddfaafd104 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -417,7 +417,9 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding): def is_earned_leave_already_allocated(allocation, annual_allocation): - from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import get_leave_type_details + from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( + get_leave_type_details, + ) leave_type_details = get_leave_type_details() date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") From aa04e027107a2a5652a6b2b61fc54ca137b767f5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 6 Feb 2022 20:30:46 +0530 Subject: [PATCH 079/133] fix(test): add ignore duplicates flag to allocation function (cherry picked from commit e25544f94e20f675befd71e58df8156649cbf1f0) --- .../doctype/leave_application/test_leave_application.py | 4 ++-- erpnext/hr/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 93f4176bd5f..39356bdcf18 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -545,7 +545,7 @@ class TestLeaveApplication(unittest.TestCase): from erpnext.hr.utils import allocate_earned_leaves i = 0 while(i<14): - allocate_earned_leaves() + allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) @@ -553,7 +553,7 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0) i = 0 while(i<6): - allocate_earned_leaves() + allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 4ddfaafd104..622ee3f9389 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -353,7 +353,7 @@ def generate_leave_encashment(): create_leave_encashment(leave_allocation=leave_allocation) -def allocate_earned_leaves(): +def allocate_earned_leaves(ignore_duplicates=False): '''Allocate earned leaves to Employees''' e_leave_types = get_earned_leaves() today = getdate() @@ -381,9 +381,9 @@ def allocate_earned_leaves(): from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): - update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) + update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates) -def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): +def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False): earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding) allocation = frappe.get_doc('Leave Allocation', allocation.name) @@ -395,7 +395,7 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type if new_allocation != allocation.total_leaves_allocated: today_date = today() - if not is_earned_leave_already_allocated(allocation, annual_allocation): + if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) From a5f016a7cd7ac952fde70f7127d52a61703bb22f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 14:36:31 +0530 Subject: [PATCH 080/133] fix: handle carry forwarded leaves while checking for duplicate allocation (cherry picked from commit bd1555bd230c0932bc0b7476f1ca68092a697e51) --- .../test_leave_policy_assignment.py | 55 ++++++++++++++++++- erpnext/hr/utils.py | 7 ++- 2 files changed, 60 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 6dd589182fc..26821f58e2d 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 @@ -187,6 +187,58 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) + def test_earned_leave_allocation_for_passed_months_with_carry_forwarded_leaves(self): + from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation + + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=get_first_day(add_months(getdate(), -2))) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + # initial leave allocation = 5 + leave_allocation = create_leave_allocation( + employee=employee.name, + employee_name=employee.employee_name, + leave_type=leave_type.name, + from_date=add_months(getdate(), -12), + to_date=add_months(getdate(), -3), + new_leaves_allocated=5, + carry_forward=0) + leave_allocation.submit() + + # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding + frappe.flags.current_date = get_last_day(add_months(getdate(), -1)) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name, + "carry_forward": 1 + } + # carry forwarded leaves = 5, 3 leaves allocated for passed months + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + details = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True) + self.assertEqual(details.new_leaves_allocated, 2) + self.assertEqual(details.unused_leaves, 5) + self.assertEqual(details.total_leaves_allocated, 7) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import is_earned_leave_already_allocated + frappe.flags.current_date = get_last_day(getdate()) + + allocation = frappe.get_doc('Leave Allocation', details.name) + # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves + self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + def tearDown(self): frappe.db.rollback() @@ -199,7 +251,8 @@ def create_earned_leave_type(leave_type): doctype="Leave Type", is_earned_leave=1, earned_leave_frequency="Monthly", - rounding=0.5 + rounding=0.5, + is_carry_forward=1 )).insert() diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 622ee3f9389..6032d0c1bd3 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -428,7 +428,12 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, annual_allocation, leave_type_details, date_of_joining) - if allocation.total_leaves_allocated >= leaves_for_passed_months: + # exclude carry-forwarded leaves while checking for leave allocation for passed months + num_allocations = allocation.total_leaves_allocated + if allocation.unused_leaves: + num_allocations -= allocation.unused_leaves + + if num_allocations >= leaves_for_passed_months: return True return False From cc867d6338e42c76c976e0632d926ec7eda2b024 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 15:55:44 +0530 Subject: [PATCH 081/133] fix: conflicts --- .../leave_policy_assignment/leave_policy_assignment.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 0ae368f9df4..fe96f3db4f2 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,12 +8,7 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate -from six import string_types -======= from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate ->>>>>>> 25c7f850b1 (fix: earned leaves not allocated if assignment is created on month-end) class LeavePolicyAssignment(Document): From e3246efa5db6c9de199083f6973c1aabf64d1f9f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 16:12:24 +0530 Subject: [PATCH 082/133] fix: conflicts --- .../doctype/leave_policy_assignment/leave_policy_assignment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index fe96f3db4f2..2115b05e92f 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -9,6 +9,7 @@ import frappe from frappe import _, bold from frappe.model.document import Document from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate +from six import string_types class LeavePolicyAssignment(Document): From df870e4b22e1217d6ed2960c4304cddb70581855 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 15:50:41 +0530 Subject: [PATCH 083/133] fix: currency in bank reconciliation chart (cherry picked from commit 0fc5d2278d2785385051d576e895c8286ad7a3a2) --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index dd7409f4b01..398e4576a68 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", { }); }, + onload: function (frm) { + frm.trigger('bank_account'); + }, + refresh: function (frm) { frappe.require("assets/js/bank-reconciliation-tool.min.js", () => frm.trigger("make_reconciliation_tool") @@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { bank_account: function (frm) { frappe.db.get_value( "Bank Account", - frm.bank_account, + frm.doc.bank_account, "account", (r) => { frappe.db.get_value( From e69f70f264e35f38dfa2d8c70f7a29d1b0dcf4ce Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 21:54:07 +0530 Subject: [PATCH 084/133] fix: fetch image form item (backport #29523) (#29571) * fix: fetch image form item (#29523) (cherry picked from commit 319322228aeb39e2d76c8b7bfd23a335f725579a) # Conflicts: # erpnext/assets/doctype/asset/asset.json * chore: resolve merge conflicts Co-authored-by: Himanshu Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/assets/doctype/asset/asset.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index de060757e2e..411e1efe8ea 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -141,6 +141,7 @@ }, { "allow_on_submit": 1, + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach Image", "hidden": 1, @@ -502,7 +503,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-24 14:58:51.097908", + "modified": "2022-01-30 20:19:24.680027", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -544,4 +545,4 @@ "sort_order": "DESC", "title_field": "asset_name", "track_changes": 1 -} \ No newline at end of file +} From 2572480554db265e2e93a5dfba75749675b46d14 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Feb 2022 22:56:14 +0530 Subject: [PATCH 085/133] fix: Loan repayment via Salary Slip --- .../doctype/loan_repayment/loan_repayment.py | 6 ++++-- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 2 +- erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 65099da49b4..46c8b4ac548 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -346,7 +346,7 @@ class LoanRepayment(AccountsController): gle_map.append( self.get_gl_dict({ "account": loan_details.penalty_income_account, - "against": payment_account, + "against": loan_details.loan_account, "credit": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", @@ -368,7 +368,9 @@ class LoanRepayment(AccountsController): "against_voucher": self.against_loan, "remarks": remarks, "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) + "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 '' }) ) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c6f38972880..f7e5442ee0e 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -125,7 +125,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC") + company="_Test Company", parent_account="Current Liabilities - _TC", account_type=None) if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index cc011a50c6b..314f74edf70 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -730,7 +730,7 @@ def get_salary_component_account(sal_comp, company_list=None): }) sal_comp.save() -def create_account(account_name, company, parent_account): +def create_account(account_name, company, parent_account, account_type=None): company_abbr = frappe.get_cached_value('Company', company, 'abbr') account = frappe.db.get_value("Account", account_name + " - " + company_abbr) if not account: From bc80f34d92bd5d8335243813d907f4b2919602c4 Mon Sep 17 00:00:00 2001 From: aaronmenezes Date: Tue, 8 Feb 2022 19:25:49 +0530 Subject: [PATCH 086/133] fix: Reserved for Production calculation considered closed work orders (cherry picked from commit 6a8b7eeffecba15e8a664449b6d92f5a8aa8d2cf) # Conflicts: # erpnext/stock/doctype/bin/bin.py --- .../doctype/work_order/test_work_order.py | 13 ++++++++++ erpnext/stock/doctype/bin/bin.py | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0e150fee23a..7be816062ec 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -198,6 +198,19 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), cint(bin1_on_start_production.reserved_qty_for_production)) + def test_reserved_qty_for_production(self): + self.bin1_at_start = get_bin(self.item, self.warehouse) + self.bin1_at_start.update_reserved_qty_for_production() + self.test_reserved_qty_for_production_submit() + self.test_reserved_qty_for_production_cancel() + self.test_close_work_order() + self.wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, + source_warehouse=self.warehouse) + self.bin1_on_submit = get_bin(self.item, self.warehouse) + bin1_on_end_production = get_bin(self.item, self.warehouse) + self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), + cint(self.bin1_at_start.reserved_qty_for_production) + 2) + def test_backflush_qty_for_overpduction_manufacture(self): cancel_stock_entry = [] allow_overproduction("overproduction_percentage_for_work_order", 30) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 1d874cd06fb..e6412e9426c 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -31,6 +31,7 @@ class Bin(Document): def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' +<<<<<<< HEAD self.reserved_qty_for_production = frappe.db.sql(''' SELECT SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN @@ -47,6 +48,30 @@ class Bin(Document): and pro.status not in ("Stopped", "Completed") and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) ''', (self.item_code, self.warehouse))[0][0] +======= + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + self.reserved_qty_for_production = ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Sum(Case() + .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + .else_(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == self.item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == self.warehouse) + & (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 +>>>>>>> 6a8b7eeffe (fix: Reserved for Production calculation considered closed work orders) self.set_projected_qty() From bde3bece36e3753cf0386fc9a043b13bfe2a974d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:19:01 +0530 Subject: [PATCH 087/133] test: remove dependency on other tests (cherry picked from commit d2cc5f2482727ee82ef914ea768311a5c1f94996) --- .../doctype/work_order/test_work_order.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 7be816062ec..975216d1bd9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -198,18 +198,20 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), cint(bin1_on_start_production.reserved_qty_for_production)) - def test_reserved_qty_for_production(self): - self.bin1_at_start = get_bin(self.item, self.warehouse) - self.bin1_at_start.update_reserved_qty_for_production() - self.test_reserved_qty_for_production_submit() - self.test_reserved_qty_for_production_cancel() - self.test_close_work_order() - self.wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, + def test_reserved_qty_for_production_closed(self): + + wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) - self.bin1_on_submit = get_bin(self.item, self.warehouse) - bin1_on_end_production = get_bin(self.item, self.warehouse) - self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), - cint(self.bin1_at_start.reserved_qty_for_production) + 2) + item = wo1.required_items[0].item_code + bin_before = get_bin(item, self.warehouse) + bin_before.update_reserved_qty_for_production() + + make_wo_order_test_record(item="_Test FG Item", qty=2, + source_warehouse=self.warehouse) + close_work_order(wo1.name, "Closed") + + bin_after = get_bin(item, self.warehouse) + self.assertEqual(bin_before.reserved_qty_for_production, bin_after.reserved_qty_for_production) def test_backflush_qty_for_overpduction_manufacture(self): cancel_stock_entry = [] From 197bf8fbec45c9856d8bf120210cd01605cb5ebd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:26:03 +0530 Subject: [PATCH 088/133] refactor: move reserve quantity computation to work order (cherry picked from commit a8bf3a3f0d21ba8b841b69b2185c9d2bd46cd3f2) # Conflicts: # erpnext/stock/doctype/bin/bin.py --- .../doctype/work_order/work_order.py | 26 +++++++++++++++++++ erpnext/stock/doctype/bin/bin.py | 6 +++++ 2 files changed, 32 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c5daba58c3d..46e02b0cdd4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -8,6 +8,8 @@ from dateutil.relativedelta import relativedelta from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Case +from frappe.query_builder.functions import Sum from frappe.utils import ( cint, date_diff, @@ -1171,3 +1173,27 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): doc.set_item_locations() return doc + +def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: + """Get total reserved quantity for any item in specified warehouse""" + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + return ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Sum(Case() + .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + .else_(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == warehouse) + & (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index e6412e9426c..fe897bc0168 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -31,6 +31,7 @@ class Bin(Document): def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' +<<<<<<< HEAD <<<<<<< HEAD self.reserved_qty_for_production = frappe.db.sql(''' SELECT @@ -72,6 +73,11 @@ class Bin(Document): ) ).run()[0][0] or 0.0 >>>>>>> 6a8b7eeffe (fix: Reserved for Production calculation considered closed work orders) +======= + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + + self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) +>>>>>>> a8bf3a3f0d (refactor: move reserve quantity computation to work order) self.set_projected_qty() From 0ac16f9e63a6daa9299d16021af9863259697c6c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:38:52 +0530 Subject: [PATCH 089/133] fix: patch existing bins (cherry picked from commit e134524532db0473bef48b7e558e5d7a289f090e) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 8 +++++- .../v13_0/update_reserved_qty_closed_wo.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/update_reserved_qty_closed_wo.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0a7eb33f66b..3e91e9de4c9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,10 +341,16 @@ erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template +<<<<<<< HEAD erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting -erpnext.patches.v13_0.update_disbursement_account \ No newline at end of file +erpnext.patches.v13_0.update_disbursement_account +======= +erpnext.patches.v13_0.shopping_cart_to_ecommerce +erpnext.patches.v13_0.update_disbursement_account +erpnext.patches.v13_0.update_reserved_qty_closed_wo +>>>>>>> e134524532 (fix: patch existing bins) diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py new file mode 100644 index 00000000000..62f774ae06f --- /dev/null +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -0,0 +1,28 @@ +import frappe + +from erpnext.stock.utils import get_bin + + +def execute(self): + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + incorrect_item_wh = ( + frappe.qb + .from_(wo) + .join(wo_item).on(wo.name == wo_item.parent) + .select(wo_item.item_code, wo.source_warehouse).distinct() + .where( + (wo.status == "Closed") + & (wo.docstatus == 1) + & (wo.source_warehouse.notnull()) + ) + ).run(debug=True) + + for item_code, warehouse in incorrect_item_wh: + if not (item_code and warehouse): + continue + + bin = get_bin(item_code, warehouse) + bin.update_reserved_qty_for_production() From 94fc5f95db39fd2caa1bbc8fa6e0a6c4677dad4f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:53:14 +0530 Subject: [PATCH 090/133] fix: concflicts --- erpnext/patches.txt | 5 --- .../v13_0/update_reserved_qty_closed_wo.py | 4 +- erpnext/stock/doctype/bin/bin.py | 45 ------------------- 3 files changed, 2 insertions(+), 52 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3e91e9de4c9..4bbeb64ce1c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,7 +341,6 @@ erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template -<<<<<<< HEAD erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning @@ -349,8 +348,4 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail 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.shopping_cart_to_ecommerce -erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo ->>>>>>> e134524532 (fix: patch existing bins) diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py index 62f774ae06f..00926b09241 100644 --- a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -3,7 +3,7 @@ import frappe from erpnext.stock.utils import get_bin -def execute(self): +def execute(): wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") @@ -18,7 +18,7 @@ def execute(self): & (wo.docstatus == 1) & (wo.source_warehouse.notnull()) ) - ).run(debug=True) + ).run() for item_code, warehouse in incorrect_item_wh: if not (item_code and warehouse): diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index fe897bc0168..b2ec15690c2 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -31,54 +31,9 @@ class Bin(Document): def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' -<<<<<<< HEAD -<<<<<<< HEAD - self.reserved_qty_for_production = frappe.db.sql(''' - SELECT - SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN - item.required_qty - item.transferred_qty - ELSE - item.required_qty - item.consumed_qty END) - END - FROM `tabWork Order` pro, `tabWork Order Item` item - WHERE - item.item_code = %s - and item.parent = pro.name - and pro.docstatus = 1 - and item.source_warehouse = %s - and pro.status not in ("Stopped", "Completed") - and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) - ''', (self.item_code, self.warehouse))[0][0] -======= - - wo = frappe.qb.DocType("Work Order") - wo_item = frappe.qb.DocType("Work Order Item") - - self.reserved_qty_for_production = ( - frappe.qb - .from_(wo) - .from_(wo_item) - .select(Sum(Case() - .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) - .else_(wo_item.required_qty - wo_item.consumed_qty)) - ) - .where( - (wo_item.item_code == self.item_code) - & (wo_item.parent == wo.name) - & (wo.docstatus == 1) - & (wo_item.source_warehouse == self.warehouse) - & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ((wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty)) - ) - ).run()[0][0] or 0.0 ->>>>>>> 6a8b7eeffe (fix: Reserved for Production calculation considered closed work orders) -======= from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) ->>>>>>> a8bf3a3f0d (refactor: move reserve quantity computation to work order) - self.set_projected_qty() self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production)) From 78c5f34286a21c548489bac406337b015118cc3c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Jan 2022 12:22:56 +0530 Subject: [PATCH 091/133] fix: ignore pricing rule in all transactions (cherry picked from commit f6dda738dc99060090e703b21f7a77692887605b) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 4 +++- erpnext/public/js/controllers/transaction.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 23606cec53f..41faa2a509e 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -250,7 +250,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa "free_item_data": [], "parent": args.parent, "parenttype": args.parenttype, - "child_docname": args.get('child_docname') + "child_docname": args.get('child_docname'), + "price_list_rate": args.get('price_list_rate') }) if args.ignore_pricing_rule or not args.item_code: @@ -404,6 +405,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): if pricing_rule.rate_or_discount == 'Discount Percentage': item_details.discount_percentage = 0.0 item_details.discount_amount = 0.0 + item_details.rate = item_details.get('price_list_rate', 0) if pricing_rule.rate_or_discount == 'Discount Amount': item_details.discount_amount = 0.0 diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3730f15078e..76de31770a5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1443,7 +1443,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "item_code": d.item_code, "pricing_rules": d.pricing_rules, "parenttype": d.parenttype, - "parent": d.parent + "parent": d.parent, + "price_list_rate": d.price_list_rate }) } }); From fc67f02ffca032eb9e1e4f74746b2de040def9ce Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Jan 2022 13:06:08 +0530 Subject: [PATCH 092/133] test: item price on remove pricing rule (cherry picked from commit b8c41e303035993d98aeb406865052a968335afe) # Conflicts: # erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py --- .../doctype/pricing_rule/pricing_rule.py | 1 + .../doctype/pricing_rule/test_pricing_rule.py | 43 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 8 ++++ 3 files changed, 52 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 41faa2a509e..b5021ddfcea 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -424,6 +424,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): item_details.applied_on_items = ','.join(items) item_details.pricing_rules = '' + item_details.pricing_rule_removed = True return item_details diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 6571e1674c2..48f92589e40 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -630,6 +630,7 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() +<<<<<<< HEAD def test_multiple_pricing_rules_with_min_qty(self): make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") @@ -649,6 +650,48 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") +======= + def test_remove_pricing_rule(self): + item = make_item("Water Flask") + make_item_price("Water Flask", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "price_or_product_discount": "Price", + "items": [{ + "item_code": "Water Flask", + }], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Discount Percentage", + "discount_percentage": 20, + "company": "_Test Company" + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code="Water Flask") + si.selling_price_list = "_Test Price List" + si.save() + + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].discount_percentage, 20) + self.assertEqual(si.items[0].rate, 80) + + si.ignore_pricing_rule = 1 + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[0].rate, 100) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() + item.delete() + +>>>>>>> b8c41e3030 (test: item price on remove pricing rule) test_dependencies = ["Campaign"] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 582818406b9..b8306518b25 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -408,6 +408,14 @@ class AccountsController(TransactionBase): if item_qty != len(get_serial_nos(item.get('serial_no'))): item.set(fieldname, value) + elif ret.get("pricing_rule_removed") and value is not None \ + and fieldname in [ + 'discount_percentage', 'discount_amount', 'rate', + 'margin_rate_or_amount', 'margin_type', 'remove_free_item' + ]: + # reset pricing rule fields if pricing_rule_removed + item.set(fieldname, value) + if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) From 91f65da4ece6340b9e67915364c90ca4f1785721 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 16:32:08 +0530 Subject: [PATCH 093/133] fix(test): pass price_list_rate only if pricing rule has to be removed (cherry picked from commit 6fa406dd04d7538b38e076cb4636b5713994456d) --- .../doctype/pos_invoice/test_pos_invoice.py | 36 +++++++++++-------- .../doctype/pricing_rule/pricing_rule.py | 30 ++++++++++------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index ba751c081bb..cf8affdd010 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -586,23 +586,29 @@ class TestPOSInvoice(unittest.TestCase): item_price.insert() pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) pr.save() - pos_inv = create_pos_invoice(qty=1, do_not_submit=1) - pos_inv.items[0].rate = 300 - pos_inv.save() - self.assertEquals(pos_inv.items[0].discount_percentage, 10) - # rate shouldn't change - self.assertEquals(pos_inv.items[0].rate, 405) - pos_inv.ignore_pricing_rule = 1 - pos_inv.items[0].rate = 300 - pos_inv.save() - self.assertEquals(pos_inv.ignore_pricing_rule, 1) - # rate should change since pricing rules are ignored - self.assertEquals(pos_inv.items[0].rate, 300) + try: + pos_inv = create_pos_invoice(qty=1, do_not_submit=1) + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].discount_percentage, 10) + # rate shouldn't change + self.assertEquals(pos_inv.items[0].rate, 405) - item_price.delete() - pos_inv.delete() - pr.delete() + pos_inv.ignore_pricing_rule = 1 + pos_inv.save() + self.assertEquals(pos_inv.ignore_pricing_rule, 1) + # rate should reset since pricing rules are ignored + self.assertEquals(pos_inv.items[0].rate, 450) + + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].rate, 300) + + finally: + item_price.delete() + pos_inv.delete() + pr.delete() def create_pos_invoice(**args): diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index b5021ddfcea..ad60bbad950 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -251,13 +251,16 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get('child_docname'), - "price_list_rate": args.get('price_list_rate') }) if args.ignore_pricing_rule or not args.item_code: if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"): - item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), - item_details, args.get('item_code')) + item_details = remove_pricing_rule_for_item( + args.get("pricing_rules"), + item_details, + item_code=args.get("item_code"), + rate=args.get("price_list_rate"), + ) return item_details update_args_for_pricing_rule(args) @@ -310,8 +313,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if not doc: return item_details elif args.get("pricing_rules"): - item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), - item_details, args.get('item_code')) + item_details = remove_pricing_rule_for_item( + args.get("pricing_rules"), + item_details, + item_code=args.get("item_code"), + rate=args.get("price_list_rate"), + ) return item_details @@ -392,7 +399,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args): item_details[field] += (pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)) -def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): +def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, get_pricing_rule_items, @@ -405,7 +412,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): if pricing_rule.rate_or_discount == 'Discount Percentage': item_details.discount_percentage = 0.0 item_details.discount_amount = 0.0 - item_details.rate = item_details.get('price_list_rate', 0) + item_details.rate = rate or 0.0 if pricing_rule.rate_or_discount == 'Discount Amount': item_details.discount_amount = 0.0 @@ -436,9 +443,12 @@ def remove_pricing_rules(item_list): out = [] for item in item_list: item = frappe._dict(item) - if item.get('pricing_rules'): - out.append(remove_pricing_rule_for_item(item.get("pricing_rules"), - item, item.item_code)) + if item.get("pricing_rules"): + out.append( + remove_pricing_rule_for_item( + item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate") + ) + ) return out From 23a5b3df182e0e62d83dfc0edfd54c3ce0556c58 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 10:10:17 +0530 Subject: [PATCH 094/133] fix: ignore pricing rule in all transactions (cherry picked from commit ab36b27a94f5f88a71358292ef7b76103c7080b7) --- erpnext/controllers/accounts_controller.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b8306518b25..c5d354e445d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -408,11 +408,19 @@ class AccountsController(TransactionBase): if item_qty != len(get_serial_nos(item.get('serial_no'))): item.set(fieldname, value) - elif ret.get("pricing_rule_removed") and value is not None \ - and fieldname in [ - 'discount_percentage', 'discount_amount', 'rate', - 'margin_rate_or_amount', 'margin_type', 'remove_free_item' - ]: + elif ( + ret.get("pricing_rule_removed") + and value is not None + and fieldname + in [ + "discount_percentage", + "discount_amount", + "rate", + "margin_rate_or_amount", + "margin_type", + "remove_free_item", + ] + ): # reset pricing rule fields if pricing_rule_removed item.set(fieldname, value) From a54e0fe42b38f571396e88b3ebcc46fd3b389301 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 9 Feb 2022 13:44:14 +0530 Subject: [PATCH 095/133] test: Update account type in payroll payable account --- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index f7e5442ee0e..2e806f3c057 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -125,7 +125,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC", account_type=None) + company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable") if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": From 32b55e715a4589e03e15dd46a5e958860b1268da Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 11:26:23 +0530 Subject: [PATCH 096/133] fix(pos): incorrect grand_total in case of inclusive taxes on item (cherry picked from commit 0452d7de20a8eddc1403d20b5f6cfba12eb63e82) --- .../pos_invoice_merge_log.py | 26 +++- .../test_pos_invoice_merge_log.py | 115 ++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 3 + 3 files changed, 140 insertions(+), 4 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 0cd19549f60..5ec0d0a51e6 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,12 +85,21 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() + self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name + def write_off_fractional_amount(self, invoice, data): + pos_invoice_grand_total = sum(d.grand_total for d in data) + + if abs(pos_invoice_grand_total - invoice.grand_total) < 1: + + invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) + invoice.save() + def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -103,6 +112,7 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() + self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name @@ -136,9 +146,15 @@ class POSInvoiceMergeLog(Document): i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse): found = True i.qty = i.qty + item.qty + i.amount = i.amount + item.net_amount + i.net_amount = i.amount + i.base_amount = i.base_amount + item.base_net_amount + i.base_net_amount = i.base_amount if not found: item.rate = item.net_rate + item.amount = item.net_amount + item.base_amount = item.base_net_amount item.price_list_rate = 0 si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) items.append(si_item) @@ -170,10 +186,12 @@ class POSInvoiceMergeLog(Document): found = True if not found: payments.append(payment) - rounding_adjustment += doc.rounding_adjustment - rounded_total += doc.rounded_total - base_rounding_adjustment += doc.base_rounding_adjustment - base_rounded_total += doc.base_rounded_total + + if doc.rounding_adjustment or doc.base_rounding_adjustment: + rounding_adjustment += doc.rounding_adjustment + rounded_total += doc.rounded_total + base_rounding_adjustment += doc.base_rounding_adjustment + base_rounded_total += doc.base_rounded_total if loyalty_points_sum: 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 3555da83a40..928d26676dc 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 @@ -150,3 +150,118 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + + def test_consolidation_round_off_error_1(self): + ''' + Test case for bug: + Round off error in consolidated invoice creation if POS Invoice has inclusive tax + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + init_user_and_profile() + + inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True) + 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": 7.5, + "included_in_print_rate": 1 + }) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True) + inv2.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": 7.5, + "included_in_print_rate": 1 + }) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 + }) + 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.outstanding_amount, 0) + self.assertEqual(consolidated_invoice.status, 'Paid') + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_round_off_error_2(self): + ''' + Test the same case as above but with an Unpaid POS Invoice + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + init_user_and_profile() + + inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + 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": 7.5, + "included_in_print_rate": 1 + }) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + inv2.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": 7.5, + "included_in_print_rate": 1 + }) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 + }) + inv2.insert() + inv2.submit() + + inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) + inv3.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000 + }) + inv3.insert() + inv3.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.outstanding_amount, 800) + self.assertEqual(consolidated_invoice.status, 'Unpaid') + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 02b1b3b1734..f2fe00151d1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -106,6 +106,9 @@ class calculate_taxes_and_totals(object): self.doc.conversion_rate = flt(self.doc.conversion_rate) def calculate_item_values(self): + if self.doc.get('is_consolidated'): + return + if not self.discount_amount_applied: for item in self.doc.get("items"): self.doc.round_floats_in(item) From 96034c4396519de168080829d6d95c576bf6410a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 16:04:08 +0530 Subject: [PATCH 097/133] fix(test): ignore stock validation (cherry picked from commit afc5c26d1c7ba2973f8e74d57029e78db550946b) --- .../pos_invoice_merge_log/test_pos_invoice_merge_log.py | 4 ++++ 1 file changed, 4 insertions(+) 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 928d26676dc..fd1aaab2649 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 @@ -158,6 +158,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): Round off error in consolidated invoice creation if POS Invoice has inclusive tax ''' frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: init_user_and_profile() @@ -205,12 +206,14 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) def test_consolidation_round_off_error_2(self): ''' Test the same case as above but with an Unpaid POS Invoice ''' frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: init_user_and_profile() @@ -265,3 +268,4 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) From e2d8fce3c52b6f87d19a0b0e90294d00707bcd0e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 17:07:51 +0530 Subject: [PATCH 098/133] fix(test): case if write off is calculated as negative amount (cherry picked from commit c2b83a02837e8ab9c2e23596f22b7f75e420003f) --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 1 - .../test_pos_invoice_merge_log.py | 11 ++++++----- erpnext/controllers/taxes_and_totals.py | 6 +++--- 3 files changed, 9 insertions(+), 9 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 5ec0d0a51e6..08c3bc71553 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 @@ -96,7 +96,6 @@ class POSInvoiceMergeLog(Document): 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() 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 fd1aaab2649..fc14161456c 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 @@ -154,10 +154,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): def test_consolidation_round_off_error_1(self): ''' - Test case for bug: - Round off error in consolidated invoice creation if POS Invoice has inclusive tax + Test round off error in consolidated invoice creation if POS Invoice has inclusive tax ''' frappe.db.sql("delete from `tabPOS Invoice`") + allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: @@ -206,13 +206,14 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) def test_consolidation_round_off_error_2(self): ''' Test the same case as above but with an Unpaid POS Invoice ''' frappe.db.sql("delete from `tabPOS Invoice`") + allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: @@ -262,10 +263,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv.load_from_db() consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) self.assertEqual(consolidated_invoice.outstanding_amount, 800) - self.assertEqual(consolidated_invoice.status, 'Unpaid') + self.assertNotEqual(consolidated_invoice.status, 'Paid') finally: frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f2fe00151d1..e3e7fdde973 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -649,12 +649,12 @@ class calculate_taxes_and_totals(object): def calculate_change_amount(self): self.doc.change_amount = 0.0 self.doc.base_change_amount = 0.0 + grand_total = self.doc.rounded_total or self.doc.grand_total + base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total if self.doc.doctype == "Sales Invoice" \ - and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ + and self.doc.paid_amount > grand_total and not self.doc.is_return \ and any(d.type == "Cash" for d in self.doc.payments): - grand_total = self.doc.rounded_total or self.doc.grand_total - base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total self.doc.change_amount = flt(self.doc.paid_amount - grand_total + self.doc.write_off_amount, self.doc.precision("change_amount")) From 7835e86da00bf1630b6063c51b473b8ecc76457c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 10:05:06 +0530 Subject: [PATCH 099/133] fix(test): do not enable negative stock (cherry picked from commit 75256863c6e3ed917d3ff00a9435da9fa7115cbb) --- .../test_pos_invoice_merge_log.py | 22 ++++++++++++++----- erpnext/controllers/taxes_and_totals.py | 2 +- 2 files changed, 17 insertions(+), 7 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 fc14161456c..5930aa097f7 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 @@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( consolidate_pos_invoices, ) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPOSInvoiceMergeLog(unittest.TestCase): @@ -156,11 +157,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): ''' Test round off error in consolidated invoice creation if POS Invoice has inclusive tax ''' + frappe.db.sql("delete from `tabPOS Invoice`") - allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) 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=3, rate=10000, do_not_save=True) @@ -206,17 +213,21 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) def test_consolidation_round_off_error_2(self): ''' Test the same case as above but with an Unpaid POS Invoice ''' frappe.db.sql("delete from `tabPOS Invoice`") - allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) 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=6, rate=10000, do_not_save=True) @@ -269,4 +280,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e3e7fdde973..08d1dcea7dc 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -653,7 +653,7 @@ class calculate_taxes_and_totals(object): base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total if self.doc.doctype == "Sales Invoice" \ - and self.doc.paid_amount > grand_total and not self.doc.is_return \ + and self.doc.paid_amount > grand_total and not self.doc.is_return \ and any(d.type == "Cash" for d in self.doc.payments): self.doc.change_amount = flt(self.doc.paid_amount - grand_total + From 02cf4e9f25ecfa4aba51aad896e46252689bc162 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 12:06:59 +0530 Subject: [PATCH 100/133] fix: flaky point of sale test (cherry picked from commit 4bb557dbd84b109b83de12b2e77a60d953c292ea) --- erpnext/tests/test_point_of_sale.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index df2dc8b99a1..3299c8885f2 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -1,15 +1,25 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +import unittest + +import frappe from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.selling.page.point_of_sale.point_of_sale import get_items 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 TestPointOfSale(ERPNextTestCase): +class TestPointOfSale(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + frappe.db.savepoint('before_test_point_of_sale') + + @classmethod + def tearDownClass(cls) -> None: + frappe.db.rollback(save_point='before_test_point_of_sale') + def test_item_search(self): """ Test Stock and Service Item Search. From 41b7c5f92d0b828515868367537776f96385739e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 16:08:28 +0530 Subject: [PATCH 101/133] fix: cancelling of consolidated sales invoice that doesn't have closing entry (cherry picked from commit 0ebd32dc630daf03dc77f81a93944a1919f0c016) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0317656e83b..0c22a0e092b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -294,7 +294,7 @@ class SalesInvoice(SellingController): filters={ invoice_or_credit_note: self.name }, pluck="pos_closing_entry" ) - if pos_closing_entry: + if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( frappe.bold("Consolidated Sales Invoice"), get_link_to_form("POS Closing Entry", pos_closing_entry[0]) From d97fa58e4f5bac774811c1362325d3395721fbcc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 16:44:13 +0530 Subject: [PATCH 102/133] fix: merge conflicts --- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 48f92589e40..f3b3cd4df77 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -630,7 +630,6 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() -<<<<<<< HEAD def test_multiple_pricing_rules_with_min_qty(self): make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") @@ -650,7 +649,7 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") -======= + def test_remove_pricing_rule(self): item = make_item("Water Flask") make_item_price("Water Flask", "_Test Price List", 100) @@ -691,7 +690,6 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() ->>>>>>> b8c41e3030 (test: item price on remove pricing rule) test_dependencies = ["Campaign"] From 503cc65db4ed9cf4b920971195bb32109d7d92d2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Feb 2022 10:28:51 +0530 Subject: [PATCH 103/133] fix: restore missing change from develop branch --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 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 08c3bc71553..40ab0c50deb 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 @@ -186,11 +186,10 @@ class POSInvoiceMergeLog(Document): if not found: payments.append(payment) - if doc.rounding_adjustment or doc.base_rounding_adjustment: - rounding_adjustment += doc.rounding_adjustment - rounded_total += doc.rounded_total - base_rounding_adjustment += doc.base_rounding_adjustment - base_rounded_total += doc.base_rounded_total + rounding_adjustment += doc.rounding_adjustment + rounded_total += doc.rounded_total + base_rounding_adjustment += doc.base_rounding_adjustment + base_rounded_total += doc.base_rounded_total if loyalty_points_sum: From d0625045934479f28afde8093bebe7697ed038aa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Feb 2022 10:20:14 +0530 Subject: [PATCH 104/133] fix: restrict filetypes to csv for rename tool (cherry picked from commit c371b52d279c02af0632c9e783e45c13e30ebaac) --- erpnext/utilities/doctype/rename_tool/rename_tool.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js index 7823055e523..5553e44ef81 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.js +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js @@ -13,6 +13,12 @@ frappe.ui.form.on("Rename Tool", { }, refresh: function(frm) { frm.disable_save(); + + frm.get_field("file_to_rename").df.options = { + restrictions: { + allowed_file_types: [".csv"], + }, + }; if (!frm.doc.file_to_rename) { frm.get_field("rename_log").$wrapper.html(""); } From ffd5efe669e3a086dd76613bc629c1061c2bb4ea Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Feb 2022 10:52:13 +0530 Subject: [PATCH 105/133] ci: bump ci python version for patch test --- .github/workflows/patch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 8d29057b487..54b381d7f89 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 - name: Setup Node uses: actions/setup-node@v2 From 5a771dca4632713b38620694082a3117f1ac8be3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Feb 2022 11:29:37 +0530 Subject: [PATCH 106/133] fix: update bin modified timestamp when updating qty These timestamps are used for writing integrations hence whenever bin is updated timestamp should update to reliabily use Bin for integration logic. (cherry picked from commit 77be98295c836d6fba02ae34f91f36cd99c625a4) --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 5 +++-- erpnext/stock/stock_balance.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 9a63afc1303..645e97ee7c8 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -682,17 +682,18 @@ class TestPurchaseOrder(unittest.TestCase): bin1 = frappe.db.get_value("Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1) # Submit PO po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") bin2 = frappe.db.get_value("Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1) self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) + self.assertNotEqual(bin1.modified, bin2.modified) # Create stock transfer rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item", diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 6663458e651..35cad2ba305 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -3,7 +3,7 @@ import frappe -from frappe.utils import cstr, flt, nowdate, nowtime +from frappe.utils import cstr, flt, now, nowdate, nowtime from erpnext.controllers.stock_controller import create_repost_item_valuation_entry from erpnext.stock.utils import update_bin @@ -175,6 +175,7 @@ def update_bin_qty(item_code, warehouse, qty_dict=None): bin.set(field, flt(value)) mismatch = True + bin.modified = now() if mismatch: bin.set_projected_qty() bin.db_update() From 7c5480d729ec9ca9e1f4fafabc0774c14fd29b69 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 11 Feb 2022 12:32:45 +0530 Subject: [PATCH 107/133] fix: cannot jump to sales invoice in gross profit report (cherry picked from commit 78dd364b0be9913208d61c402a6c858eb578e210) --- erpnext/accounts/report/gross_profit/gross_profit.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 685f2d6176b..2ba649da07f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -42,6 +42,11 @@ frappe.query_reports["Gross Profit"] = { "parent_field": "parent_invoice", "initial_depth": 3, "formatter": function(value, row, column, data, default_formatter) { + if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) { + column._options = "Sales Invoice"; + } else { + column._options = "Item"; + } value = default_formatter(value, row, column, data); if (data && (data.indent == 0.0 || row[1].content == "Total")) { From 032e4e4486369af2e48793a51065631063b51ee6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 9 Feb 2022 17:53:33 +0100 Subject: [PATCH 108/133] fix: encode filters for URI (cherry picked from commit 5811d9e318de46095f85fb183583e61d14aff7ef) --- erpnext/regional/report/datev/datev.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 4124e3df190..03c729e6df4 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -40,7 +40,11 @@ frappe.query_reports["DATEV"] = { }); query_report.page.add_menu_item(__("Download DATEV File"), () => { - const filters = JSON.stringify(query_report.get_values()); + const filters = encodeURIComponent( + JSON.stringify( + query_report.get_values() + ) + ); window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); }); From adaa8bd32d1406619a38dc5a1dace6fea039fbbb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 10 Feb 2022 12:30:41 +0530 Subject: [PATCH 109/133] fix: time out error while making work orders from prodcution plan (cherry picked from commit eec2f87088e630a7ef2a918d64dd3cf2b78787d3) --- .../production_plan/production_plan.py | 79 +++++++++++-------- .../doctype/work_order/work_order.py | 5 +- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f06624fe92c..f6d2a14a074 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -342,6 +342,7 @@ class ProductionPlan(Document): def get_production_items(self): item_dict = {} + for d in self.po_items: item_details = { "production_item" : d.item_code, @@ -358,12 +359,12 @@ class ProductionPlan(Document): "production_plan" : self.name, "production_plan_item" : d.name, "product_bundle_item" : d.product_bundle_item, - "planned_start_date" : d.planned_start_date + "planned_start_date" : d.planned_start_date, + "project" : self.project } - item_details.update({ - "project": self.project or frappe.db.get_value("Sales Order", d.sales_order, "project") - }) + if not item_details['project'] and d.sales_order: + item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project") if self.get_items_from == "Material Request": item_details.update({ @@ -381,39 +382,59 @@ class ProductionPlan(Document): @frappe.whitelist() def make_work_order(self): + from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse + wo_list, po_list = [], [] subcontracted_po = {} + default_warehouses = get_default_warehouse() - self.validate_data() - self.make_work_order_for_finished_goods(wo_list) - self.make_work_order_for_subassembly_items(wo_list, subcontracted_po) + self.make_work_order_for_finished_goods(wo_list, default_warehouses) + self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) self.make_subcontracted_purchase_order(subcontracted_po, po_list) self.show_list_created_message('Work Order', wo_list) self.show_list_created_message('Purchase Order', po_list) - def make_work_order_for_finished_goods(self, wo_list): + def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() for key, item in items_data.items(): if self.sub_assembly_items: item['use_multi_level_bom'] = 0 + set_default_warehouses(item, default_warehouses) work_order = self.create_work_order(item) if work_order: wo_list.append(work_order) - def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po): + def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): for row in self.sub_assembly_items: if row.type_of_manufacturing == 'Subcontract': subcontracted_po.setdefault(row.supplier, []).append(row) continue - args = {} - self.prepare_args_for_sub_assembly_items(row, args) - work_order = self.create_work_order(args) + work_order_data = { + 'wip_warehouse': default_warehouses.get('wip_warehouse'), + 'fg_warehouse': default_warehouses.get('fg_warehouse') + } + + self.prepare_data_for_sub_assembly_items(row, work_order_data) + work_order = self.create_work_order(work_order_data) if work_order: wo_list.append(work_order) + def prepare_data_for_sub_assembly_items(self, row, wo_data): + for field in ["production_item", "item_name", "qty", "fg_warehouse", + "description", "bom_no", "stock_uom", "bom_level", + "production_plan_item", "schedule_date"]: + if row.get(field): + wo_data[field] = row.get(field) + + wo_data.update({ + "use_multi_level_bom": 0, + "production_plan": self.name, + "production_plan_sub_assembly_item": row.name + }) + def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): if not subcontracted_po: return @@ -424,7 +445,7 @@ class ProductionPlan(Document): po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() po.is_subcontracted = 'Yes' for row in po_list: - args = { + po_data = { 'item_code': row.production_item, 'warehouse': row.fg_warehouse, 'production_plan_sub_assembly_item': row.name, @@ -434,9 +455,9 @@ class ProductionPlan(Document): for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', 'description', 'production_plan_item']: - args[field] = row.get(field) + po_data[field] = row.get(field) - po.append('items', args) + po.append('items', po_data) po.set_missing_values() po.flags.ignore_mandatory = True @@ -453,24 +474,9 @@ class ProductionPlan(Document): doc_list = [get_link_to_form(doctype, p) for p in doc_list] msgprint(_("{0} created").format(comma_and(doc_list))) - def prepare_args_for_sub_assembly_items(self, row, args): - for field in ["production_item", "item_name", "qty", "fg_warehouse", - "description", "bom_no", "stock_uom", "bom_level", - "production_plan_item", "schedule_date"]: - args[field] = row.get(field) - - args.update({ - "use_multi_level_bom": 0, - "production_plan": self.name, - "production_plan_sub_assembly_item": row.name - }) - def create_work_order(self, item): - from erpnext.manufacturing.doctype.work_order.work_order import ( - OverProductionError, - get_default_warehouse, - ) - warehouse = get_default_warehouse() + from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + wo = frappe.new_doc("Work Order") wo.update(item) wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date') @@ -479,11 +485,11 @@ class ProductionPlan(Document): wo.fg_warehouse = item.get("warehouse") wo.set_work_order_operations() + wo.set_required_items() - if not wo.fg_warehouse: - wo.fg_warehouse = warehouse.get('fg_warehouse') try: wo.flags.ignore_mandatory = True + wo.flags.ignore_validate = True wo.insert() return wo.name except OverProductionError: @@ -1024,3 +1030,8 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): if d.value: get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) + +def set_default_warehouses(row, default_warehouses): + for field in ['wip_warehouse', 'fg_warehouse']: + if not row.get(field): + row[field] = default_warehouses.get(field) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 46e02b0cdd4..8a08c2c6248 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -76,7 +76,6 @@ class WorkOrder(Document): self.set_required_items(reset_only_qty = len(self.get("required_items"))) - def validate_sales_order(self): if self.sales_order: self.check_sales_order_on_hold_or_close() @@ -543,7 +542,7 @@ class WorkOrder(Document): if node.is_bom: operations.extend(_get_operations(node.name, qty=node.exploded_qty)) - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) for correct_index, operation in enumerate(operations, start=1): @@ -623,7 +622,7 @@ class WorkOrder(Document): frappe.delete_doc("Job Card", d.name) def validate_production_item(self): - if frappe.db.get_value("Item", self.production_item, "has_variants"): + if frappe.get_cached_value("Item", self.production_item, "has_variants"): frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError) if self.production_item: From 87decb3734fdc3fd3c03c28dc5054bb861fc9111 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 11 Feb 2022 17:38:37 +0530 Subject: [PATCH 110/133] Revert "fix(India): Tax calculation for overseas suppliers" (cherry picked from commit ea20c63182ba0b380aa46bab438ed45db0f19e8a) --- erpnext/regional/india/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0126b090fca..2287714a008 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -221,7 +221,6 @@ def get_regional_address_details(party_details, doctype, company): if not party_details.place_of_supply: return party_details if not party_details.company_gstin: return party_details - if not party_details.supplier_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", From 2b4e53222676cb805324da394bc21928caff1d63 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 17:14:11 +0530 Subject: [PATCH 111/133] fix: Earned Leave allocation based on joining date not working (cherry picked from commit 7326d57966d09ababc9fd02d32980dae8d51dc3c) --- erpnext/hr/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 6032d0c1bd3..416366de9c0 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -377,10 +377,10 @@ def allocate_earned_leaves(ignore_duplicates=False): from_date=allocation.from_date - if e_leave_type.based_on_date_of_joining_date: + if e_leave_type.based_on_date_of_joining: from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): + if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining): update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates) def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False): @@ -421,10 +421,13 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): get_leave_type_details, ) + assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) + if assignment.assignment_based_on == "Joining Date": + return False + leave_type_details = get_leave_type_details() date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, annual_allocation, leave_type_details, date_of_joining) @@ -459,7 +462,7 @@ def create_additional_leave_ledger_entry(allocation, leaves, date): allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() -def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date): +def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining): import calendar from dateutil import relativedelta @@ -470,7 +473,7 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining #last day of month last_day = calendar.monthrange(to_date.year, to_date.month)[1] - if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day): + if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day): if frequency == "Monthly": return True elif frequency == "Quarterly" and rd.months % 3: From 3fea6fd9e87aa170eba925c4d8f9af9af8c3c729 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Feb 2022 11:28:14 +0530 Subject: [PATCH 112/133] fix: consider based on DOJ config while calculating leaves for passed months (cherry picked from commit 89fa0bb73f1a192c2bfe8bc0a87956cb12ff6352) --- .../leave_policy_assignment.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 2115b05e92f..0618b9de14b 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -120,14 +120,15 @@ class LeavePolicyAssignment(Document): from_date_year = get_datetime(from_date).year months_passed = 0 + based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining - if current_year == from_date_year and current_month > from_date_month: + if current_year == from_date_year and current_month >= from_date_month: months_passed = current_month - from_date_month - months_passed = add_current_month_if_applicable(months_passed) + months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) elif current_year > from_date_year: months_passed = (12 - from_date_month) + current_month - months_passed = add_current_month_if_applicable(months_passed) + months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) if months_passed > 0: monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, @@ -139,13 +140,20 @@ class LeavePolicyAssignment(Document): return new_leaves_allocated -def add_current_month_if_applicable(months_passed): +def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj): date = getdate(frappe.flags.current_date) or getdate() - last_day_of_month = get_last_day(date) - # if its the last day of the month, then that month should also be considered - if last_day_of_month == date: - months_passed += 1 + if based_on_doj: + # if leave type allocation is based on DOJ, + # and the date of assignment creation is same as DOJ, + # then the month should be considered + if date == date_of_joining: + months_passed += 1 + else: + last_day_of_month = get_last_day(date) + # if its the last day of the month, then that month should be considered + if last_day_of_month == date: + months_passed += 1 return months_passed @@ -184,7 +192,7 @@ def create_assignment_for_multiple_employees(employees, data): def get_leave_type_details(): leave_type_details = frappe._dict() leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", + fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining", "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"]) for d in leave_types: leave_type_details.setdefault(d.name, d) From 9b6040979a4131c63a98d38eb0fa4f0ca0653d53 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 13:52:46 +0530 Subject: [PATCH 113/133] fix: consider leaves for past months if assignment is based on joining date too (cherry picked from commit c7be9ef5d24a3e03efde64a45302baca76e8107f) # Conflicts: # erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py --- .../leave_policy_assignment.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 0618b9de14b..158b576bdb6 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,8 +8,12 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate from six import string_types +======= +from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate +>>>>>>> c7be9ef5d2 (fix: consider leaves for past months if assignment is based on joining date too) class LeavePolicyAssignment(Document): @@ -95,10 +99,12 @@ class LeavePolicyAssignment(Document): new_leaves_allocated = 0 elif leave_type_details.get(leave_type).is_earned_leave == 1: - if self.assignment_based_on == "Leave Period": - new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining) - else: + if not self.assignment_based_on: new_leaves_allocated = 0 + else: + # get leaves for past months if assignment is based on Leave Period / Joining Date + new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining) + # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period elif getdate(date_of_joining) > getdate(self.effective_from): remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) @@ -109,25 +115,23 @@ class LeavePolicyAssignment(Document): def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): from erpnext.hr.utils import get_monthly_earned_leave - current_month = get_datetime(frappe.flags.current_date).month or get_datetime().month - current_year = get_datetime(frappe.flags.current_date).year or get_datetime().year + current_date = frappe.flags.current_date or getdate() + if current_date > getdate(self.effective_to): + current_date = getdate(self.effective_to) - from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date") - if getdate(date_of_joining) > getdate(from_date): - from_date = date_of_joining - - from_date_month = get_datetime(from_date).month - from_date_year = get_datetime(from_date).year + from_date = getdate(self.effective_from) + if getdate(date_of_joining) > from_date: + from_date = getdate(date_of_joining) months_passed = 0 based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining - if current_year == from_date_year and current_month >= from_date_month: - months_passed = current_month - from_date_month + if current_date.year == from_date.year and current_date.month >= from_date.month: + months_passed = current_date.month - from_date.month months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) - elif current_year > from_date_year: - months_passed = (12 - from_date_month) + current_month + elif current_date.year > from_date.year: + months_passed = (12 - from_date.month) + current_date.month months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) if months_passed > 0: @@ -144,8 +148,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj date = getdate(frappe.flags.current_date) or getdate() if based_on_doj: - # if leave type allocation is based on DOJ, - # and the date of assignment creation is same as DOJ, + # if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ, # then the month should be considered if date == date_of_joining: months_passed += 1 From c0ea6d07214acca4473014f6c813d17267b7658b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 17:40:20 +0530 Subject: [PATCH 114/133] chore: clean-up leave policy assignment tests (cherry picked from commit 51e608682934610d3414e40f9524e529c4a36f49) --- .../leave_policy_assignment.py | 1 - .../test_leave_policy_assignment.py | 116 ++++++------------ 2 files changed, 36 insertions(+), 81 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 158b576bdb6..81d06062602 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -17,7 +17,6 @@ from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate class LeavePolicyAssignment(Document): - def validate(self): self.validate_policy_assignment_overlap() self.set_dates() 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 26821f58e2d..2d4fcf91f72 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 @@ -22,34 +22,27 @@ class TestLeavePolicyAssignment(unittest.TestCase): for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + self.employee = get_employee() + def test_grant_leaves(self): leave_period = get_leave_period() - employee = get_employee() - - # create the leave policy with leave type "_Test Leave Type", allocation = 10 + # allocation = 10 leave_policy = create_leave_policy() leave_policy.submit() - data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, "leave_period": leave_period.name } - - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) - - leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) - leave_policy_assignment_doc.reload() - - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": employee.name, + "employee": self.employee.name, "leave_policy":leave_policy.name, "leave_policy_assignment": leave_policy_assignments[0], "docstatus": 1})[0] - leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) @@ -61,49 +54,32 @@ class TestLeavePolicyAssignment(unittest.TestCase): def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self): leave_period = get_leave_period() - employee = get_employee() - # create the leave policy with leave type "_Test Leave Type", allocation = 10 leave_policy = create_leave_policy() leave_policy.submit() - data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, "leave_period": leave_period.name } - - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) - - leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) - leave_policy_assignment_doc.reload() - + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) # every leave is allocated no more leave can be granted now - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) - + self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": employee.name, + "employee": self.employee.name, "leave_policy":leave_policy.name, "leave_policy_assignment": leave_policy_assignments[0], "docstatus": 1})[0] leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) - - # User all allowed to grant leave when there is no allocation against assignment leave_alloc_doc.cancel() leave_alloc_doc.delete() - - leave_policy_assignment_doc.reload() - - - # User are now allowed to grant leave - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0) def test_earned_leave_allocation(self): leave_period = create_leave_period("Test Earned Leave Period") - employee = get_employee() leave_type = create_earned_leave_type("Test Earned Leave") leave_policy = frappe.get_doc({ @@ -116,7 +92,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency leaves_allocated = frappe.db.get_value("Leave Allocation", { @@ -124,16 +100,8 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 0) - def test_earned_leave_allocation_for_passed_months(self): - employee = get_employee() - leave_type = create_earned_leave_type("Test Earned Leave") - leave_period = create_leave_period("Test Earned Leave Period", - start_date=get_first_day(add_months(getdate(), -1))) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() + def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1))) # Case 1: assignment created one month after the leave period, should allocate 1 leave frappe.flags.current_date = get_first_day(getdate()) @@ -142,24 +110,15 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + 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") self.assertEqual(leaves_allocated, 1) - def test_earned_leave_allocation_for_passed_months_on_month_end(self): - employee = get_employee() - leave_type = create_earned_leave_type("Test Earned Leave") - leave_period = create_leave_period("Test Earned Leave Period", - start_date=get_first_day(add_months(getdate(), -2))) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() - + def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) # Case 2: assignment created on the last day of the leave period's latter month # should allocate 1 leave for current month even though the month has not ended # since the daily job might have already executed @@ -170,7 +129,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + 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] @@ -187,33 +146,17 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - def test_earned_leave_allocation_for_passed_months_with_carry_forwarded_leaves(self): + def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation - employee = get_employee() - leave_type = create_earned_leave_type("Test Earned Leave") - leave_period = create_leave_period("Test Earned Leave Period", - start_date=get_first_day(add_months(getdate(), -2))) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() - + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) # initial leave allocation = 5 - leave_allocation = create_leave_allocation( - employee=employee.name, - employee_name=employee.employee_name, - leave_type=leave_type.name, - from_date=add_months(getdate(), -12), - to_date=add_months(getdate(), -3), - new_leaves_allocated=5, - carry_forward=0) + leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave", + from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0) leave_allocation.submit() # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding frappe.flags.current_date = get_last_day(add_months(getdate(), -1)) - data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, @@ -221,7 +164,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): "carry_forward": 1 } # carry forwarded leaves = 5, 3 leaves allocated for passed months - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) details = frappe.db.get_value("Leave Allocation", { "leave_policy_assignment": leave_policy_assignments[0] @@ -268,4 +211,17 @@ def create_leave_period(name, start_date=None): to_date=add_months(start_date, 12), company="_Test Company", is_active=1 - )).insert() \ No newline at end of file + )).insert() + + +def setup_leave_period_and_policy(start_date): + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=start_date) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + return leave_period, leave_policy \ No newline at end of file From c261621dcaecf026f38c85b309ea968f3e7dd4c7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 20:08:01 +0530 Subject: [PATCH 115/133] test: earned leave allocations based on DOJ (cherry picked from commit 9b0f9c344282c9cad5334c6e3b46aa1c74826f9b) --- .../leave_policy_assignment.py | 2 +- .../test_leave_policy_assignment.py | 134 ++++++++++++++++-- erpnext/hr/utils.py | 5 +- 3 files changed, 128 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 81d06062602..b70b59fe1b0 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -149,7 +149,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj if based_on_doj: # if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ, # then the month should be considered - if date == date_of_joining: + if date.day == date_of_joining.day: months_passed += 1 else: last_day_of_month = get_last_day(date) 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 2d4fcf91f72..678468ffc2d 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 @@ -20,7 +20,7 @@ test_dependencies = ["Employee"] class TestLeavePolicyAssignment(unittest.TestCase): def setUp(self): for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: - frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + frappe.db.delete(doctype) self.employee = get_employee() @@ -85,7 +85,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): leave_policy = frappe.get_doc({ "doctype": "Leave Policy", "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] - }).insert() + }).submit() data = { "assignment_based_on": "Leave Period", @@ -117,7 +117,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 1) - def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self): leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) # Case 2: assignment created on the last day of the leave period's latter month # should allocate 1 leave for current month even though the month has not ended @@ -178,15 +178,132 @@ class TestLeavePolicyAssignment(unittest.TestCase): from erpnext.hr.utils import is_earned_leave_already_allocated frappe.flags.current_date = get_last_day(getdate()) - allocation = frappe.get_doc('Leave Allocation', details.name) + allocation = frappe.get_doc("Leave Allocation", details.name) # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): + # tests leave alloc for earned leaves for assignment based on joining date in policy assignment + leave_type = create_earned_leave_type("Test Earned Leave") + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).submit() + + # joining date set to 2 months back + doj = self.employee.date_of_joining + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the last day of the current month + frappe.flags.current_date = get_last_day(getdate()) + data = { + "assignment_based_on": "Joining Date", + "leave_policy": leave_policy.name + } + 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") + effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + self.assertEqual(effective_from, self.employee.date_of_joining) + self.assertEqual(leaves_allocated, 3) + + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_last_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # reset DOJ + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) + + def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): + # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) + + # joining date set to 2 months back + doj = self.employee.date_of_joining + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the same day of the current month, should allocate leaves including the current month + frappe.flags.current_date = get_first_day(getdate()) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + 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") + self.assertEqual(leaves_allocated, 3) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # reset DOJ + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) + + def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): + # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type + leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).submit() + + # joining date set to 2 months back + # leave should be allocated for current month too since this day is same as the joining day + doj = self.employee.date_of_joining + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the first day of the current month + frappe.flags.current_date = get_first_day(getdate()) + data = { + "assignment_based_on": "Joining Date", + "leave_policy": leave_policy.name + } + 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") + effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + self.assertEqual(effective_from, self.employee.date_of_joining) + self.assertEqual(leaves_allocated, 3) + + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # reset DOJ + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) + def tearDown(self): frappe.db.rollback() -def create_earned_leave_type(leave_type): +def create_earned_leave_type(leave_type, based_on_doj=False): frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) return frappe.get_doc(dict( @@ -195,7 +312,8 @@ def create_earned_leave_type(leave_type): is_earned_leave=1, earned_leave_frequency="Monthly", rounding=0.5, - is_carry_forward=1 + is_carry_forward=1, + based_on_date_of_joining=based_on_doj )).insert() @@ -214,8 +332,8 @@ def create_leave_period(name, start_date=None): )).insert() -def setup_leave_period_and_policy(start_date): - leave_type = create_earned_leave_type("Test Earned Leave") +def setup_leave_period_and_policy(start_date, based_on_doj=False): + leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj) leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date) leave_policy = frappe.get_doc({ diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 416366de9c0..46bcadcf536 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -421,13 +421,10 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): get_leave_type_details, ) - assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) - if assignment.assignment_based_on == "Joining Date": - return False - leave_type_details = get_leave_type_details() date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") + assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, annual_allocation, leave_type_details, date_of_joining) From 6a1c27a18f6747822b6352d865c961e969131e73 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 20:59:19 +0530 Subject: [PATCH 116/133] fix(test): reset test setup (cherry picked from commit cbaadcf1138cba113cc18c6d2bc2690e144cf9d0) --- .../test_leave_policy_assignment.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 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 678468ffc2d..8d7b27ee5af 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 @@ -22,7 +22,9 @@ class TestLeavePolicyAssignment(unittest.TestCase): for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.delete(doctype) - self.employee = get_employee() + employee = get_employee() + self.original_doj = employee.date_of_joining + self.employee = employee def test_grant_leaves(self): leave_period = get_leave_period() @@ -192,7 +194,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): }).submit() # joining date set to 2 months back - doj = self.employee.date_of_joining self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) self.employee.save() @@ -218,15 +219,11 @@ class TestLeavePolicyAssignment(unittest.TestCase): "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - # reset DOJ - frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) - def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) # joining date set to 2 months back - doj = self.employee.date_of_joining self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) self.employee.save() @@ -256,9 +253,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - # reset DOJ - frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) - def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) @@ -270,7 +264,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): # joining date set to 2 months back # leave should be allocated for current month too since this day is same as the joining day - doj = self.employee.date_of_joining self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) self.employee.save() @@ -296,11 +289,10 @@ class TestLeavePolicyAssignment(unittest.TestCase): "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - # reset DOJ - frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) - def tearDown(self): frappe.db.rollback() + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) + frappe.flags.current_date = None def create_earned_leave_type(leave_type, based_on_doj=False): From 0e3788e34656fc0d41cf47161282aace4ec2591a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 21:27:44 +0530 Subject: [PATCH 117/133] fix: conflicts --- .../leave_policy_assignment/leave_policy_assignment.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index b70b59fe1b0..6e6943f71aa 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,12 +8,8 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate -from six import string_types -======= from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate ->>>>>>> c7be9ef5d2 (fix: consider leaves for past months if assignment is based on joining date too) +from six import string_types class LeavePolicyAssignment(Document): From b7b5531ed030de9596f8a7edfc20cf664bb6425a Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 11 Feb 2022 15:12:25 +0530 Subject: [PATCH 118/133] chore: remove deprecated print format (cherry picked from commit d93d2a80b10c94cc2d7f8b5a3601d0efec8cbf2d) --- .../print_format/gst_pos_invoice/__init__.py | 0 .../gst_pos_invoice/gst_pos_invoice.json | 23 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 erpnext/accounts/print_format/gst_pos_invoice/__init__.py delete mode 100644 erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/accounts/print_format/gst_pos_invoice/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json b/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json deleted file mode 100644 index 1aa1c02968f..00000000000 --- a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "align_labels_right": 0, - "creation": "2017-08-08 12:33:04.773099", - "custom_format": 1, - "disabled": 0, - "doc_type": "Sales Invoice", - "docstatus": 0, - "doctype": "Print Format", - "font": "Default", - "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n

\n\t{{ doc.company }}
\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"
\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t{{ _(\"GSTIN\") }}:{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"
GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t
\n\t{% if doc.docstatus == 0 %}\n\t\t{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}
\n\t{% else %}\n\t\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t{% endif %}\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"
\", \" \") %}\n\t\t{{ _(\"Customer\") }}:
\n\t\t{{ doc.customer_name }}
\n\t\t{{ customer_address }}\n\t{% endif %}\n

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

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

\n

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

", - "idx": 0, - "line_breaks": 0, - "modified": "2020-04-29 16:39:12.936215", - "modified_by": "Administrator", - "module": "Accounts", - "name": "GST POS Invoice", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file From 894a406ed406f8e6fa3efed9315609ffc33075f6 Mon Sep 17 00:00:00 2001 From: Govind S Menokee Date: Tue, 8 Feb 2022 23:53:20 +0530 Subject: [PATCH 119/133] Bug: fix list mutation within loop Prevent list mutation within loop leading to incorrect data --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 99dfc231c74..4ef29848bc6 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -479,11 +479,12 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) def remove_payrolled_employees(emp_list, start_date, end_date): + new_emp_list = [] for employee_details in emp_list: - if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): - emp_list.remove(employee_details) + if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): + new_emp_list.append(employee_details) - return emp_list + return new_emp_list @frappe.whitelist() def get_start_end_dates(payroll_frequency, start_date=None, company=None): From c7fbeb3aac3724f991b2e3819ea87adcd55b81b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 12 Feb 2022 20:55:27 +0530 Subject: [PATCH 120/133] fix(India): Report GSTR-1 minor fixes (#29768) * fix(India): Report GSTR-1 minor fixes (cherry picked from commit 6e679a5ad2f82f6c97deb4446590abe0d5c3ab46) * fix: cleaner implementation for `get_invoice_type` (cherry picked from commit 2bc157a95cff5d13f492fddf7c177b3e67fe62a8) Co-authored-by: Smit Vora Co-authored-by: Sagar Vora --- erpnext/regional/report/gstr_1/gstr_1.py | 58 ++++++++++-------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 17b11073346..8805678dc7d 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -29,7 +29,7 @@ class Gstr1Report(object): posting_date, base_grand_total, base_rounded_total, - COALESCE(NULLIF(customer_gstin,''), NULLIF(billing_address_gstin, '')) as customer_gstin, + NULLIF(billing_address_gstin, '') as billing_address_gstin, place_of_supply, ecommerce_gstin, reverse_charge, @@ -260,7 +260,7 @@ class Gstr1Report(object): if self.filters.get("type_of_business") == "B2B": - conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" + conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Registered Composition', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') @@ -384,7 +384,7 @@ class Gstr1Report(object): for invoice, items in iteritems(self.invoice_items): if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ - and self.invoices.get(invoice, {}).get('gst_category') == "Overseas": + and self.invoices.get(invoice, {}).get('gst_category') in ("Overseas", "SEZ"): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): @@ -410,7 +410,7 @@ class Gstr1Report(object): if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { - "fieldname": "customer_gstin", + "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", "width": 150 @@ -517,7 +517,7 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "CDNR-REG": self.invoice_columns = [ { - "fieldname": "customer_gstin", + "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", "width": 150 @@ -818,7 +818,7 @@ def get_json(filters, report_name, data): res = {} if filters["type_of_business"] == "B2B": for item in report_data[:-1]: - res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) out = get_b2b_json(res, gstin) gst_json["b2b"] = out @@ -842,7 +842,7 @@ def get_json(filters, report_name, data): gst_json["exp"] = out elif filters["type_of_business"] == "CDNR-REG": for item in report_data[:-1]: - res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) out = get_cdnr_reg_json(res, gstin) gst_json["cdnr"] = out @@ -876,7 +876,7 @@ def get_json(filters, report_name, data): } def get_b2b_json(res, gstin): - inv_type, out = {"Registered Regular": "R", "Deemed Export": "DE", "URD": "URD", "SEZ": "SEZ"}, [] + out = [] for gst_in in res: b2b_item, inv = {"ctin": gst_in, "inv": []}, [] if not gst_in: continue @@ -890,7 +890,7 @@ def get_b2b_json(res, gstin): inv_item = get_basic_invoice_detail(invoice[0]) inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]) inv_item["rchrg"] = invoice[0]["reverse_charge"] - inv_item["inv_typ"] = inv_type.get(invoice[0].get("gst_category", ""),"") + inv_item["inv_typ"] = get_invoice_type(invoice[0]) if inv_item["pos"]=="00": continue inv_item["itms"] = [] @@ -1045,7 +1045,7 @@ def get_cdnr_reg_json(res, gstin): "ntty": invoice[0]["document_type"], "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_typ": get_invoice_type_for_cdnr(invoice[0]) + "inv_typ": get_invoice_type(invoice[0]) } inv_item["itms"] = [] @@ -1070,7 +1070,7 @@ def get_cdnr_unreg_json(res, gstin): "val": abs(flt(items[0]["invoice_value"])), "ntty": items[0]["document_type"], "pos": "%02d" % int(items[0]["place_of_supply"].split('-')[0]), - "typ": get_invoice_type_for_cdnrur(items[0]) + "typ": get_invoice_type(items[0]) } inv_item["itms"] = [] @@ -1111,29 +1111,21 @@ def get_exempted_json(data): return out -def get_invoice_type_for_cdnr(row): - if row.get('gst_category') == 'SEZ': - if row.get('export_type') == 'WPAY': - invoice_type = 'SEWP' - else: - invoice_type = 'SEWOP' - elif row.get('gst_category') == 'Deemed Export': - invoice_type = 'DE' - elif row.get('gst_category') == 'Registered Regular': - invoice_type = 'R' +def get_invoice_type(row): + gst_category = row.get('gst_category') - return invoice_type + if gst_category == 'SEZ': + return 'SEWP' if row.get('export_type') == 'WPAY' else 'SEWOP' -def get_invoice_type_for_cdnrur(row): - if row.get('gst_category') == 'Overseas': - if row.get('export_type') == 'WPAY': - invoice_type = 'EXPWP' - else: - invoice_type = 'EXPWOP' - elif row.get('gst_category') == 'Unregistered': - invoice_type = 'B2CL' + if gst_category == 'Overseas': + return 'EXPWP' if row.get('export_type') == 'WPAY' else 'EXPWOP' - return invoice_type + return ({ + 'Deemed Export': 'DE', + 'Registered Regular': 'R', + 'Registered Composition': 'R', + 'Unregistered': 'B2CL' + }).get(gst_category) def get_basic_invoice_detail(row): return { @@ -1155,7 +1147,7 @@ def get_rate_and_tax_details(row, gstin): # calculate tax amount added tax = flt((row["taxable_value"]*rate)/100.0, 2) frappe.errprint([tax, tax/2]) - if row.get("customer_gstin") and gstin[0:2] == row["customer_gstin"][0:2]: + if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]: itm_det.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) else: itm_det.update({"iamt": tax}) @@ -1200,4 +1192,4 @@ def is_inter_state(invoice_detail): if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: return True else: - return False \ No newline at end of file + return False From e5b50557e64628937d9a8fbfaa875cfc5b36a3cf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:09:39 +0530 Subject: [PATCH 121/133] fix: show user id in emp group table (backport #29776) (#29777) Co-authored-by: Dany Robert --- .../doctype/employee_group_table/employee_group_table.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee_group_table/employee_group_table.json b/erpnext/hr/doctype/employee_group_table/employee_group_table.json index 4e0045cdeb8..54eb8c6da91 100644 --- a/erpnext/hr/doctype/employee_group_table/employee_group_table.json +++ b/erpnext/hr/doctype/employee_group_table/employee_group_table.json @@ -27,12 +27,13 @@ "fetch_from": "employee.user_id", "fieldname": "user_id", "fieldtype": "Data", + "in_list_view": 1, "label": "ERPNext User ID", "read_only": 1 } ], "istable": 1, - "modified": "2019-06-06 10:41:20.313756", + "modified": "2022-02-13 19:44:21.302938", "modified_by": "Administrator", "module": "HR", "name": "Employee Group Table", @@ -42,4 +43,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 47f8b44cfe442abdd6605b2c51b0c7fd06de1d3a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Feb 2022 11:33:34 +0530 Subject: [PATCH 122/133] fix: incorrect pricing rule filtering on selecting first item (cherry picked from commit 3713ae75ab16ea7ca469ab82d529da571583cea2) --- erpnext/stock/get_item_details.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 339d1b60839..e7b4ca2de38 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -344,6 +344,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): args.conversion_factor = out.conversion_factor out.stock_qty = out.qty * out.conversion_factor + args.stock_qty = out.stock_qty # calculate last purchase rate if args.get('doctype') in purchase_doctypes: From 7fa46f77e0bdbc516b3c0cb0fb20594ee7fa398b Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 14 Feb 2022 19:54:29 +0530 Subject: [PATCH 123/133] revert: "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount" (#29782) * Revert "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount" This reverts commit 038f94955006c88209f9df28e3a785c59a4ddb28, reversing changes made to c7b491843476bca89be02851ccafb7e409876609. * fix: linter --- .../doctype/sales_invoice/sales_invoice.py | 6 +++--- erpnext/patches.txt | 2 +- .../stock/doctype/delivery_note/delivery_note.py | 16 ++++------------ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0c22a0e092b..42da6b7708f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1263,14 +1263,14 @@ class SalesInvoice(SellingController): def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): - if d.so_detail: - updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) - elif d.dn_detail: + if d.dn_detail: billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` where dn_detail=%s and docstatus=1""", d.dn_detail) billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) updated_delivery_notes.append(d.delivery_note) + elif d.so_detail: + updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4bbeb64ce1c..f4a6e089637 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -348,4 +348,4 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail 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 +erpnext.patches.v13_0.update_reserved_qty_closed_wo \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c3247fbe3e8..00836fc8157 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -342,25 +342,21 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum # Billed against Sales Order directly - si = frappe.qb.DocType("Sales Invoice").as_("si") si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") sum_amount = Sum(si_item.amount).as_("amount") - billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where( - (si_item.parent == si.name) & + billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where( (si_item.so_detail == so_detail) & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & - (si_item.docstatus == 1) & - (si.update_stock == 0) + (si_item.docstatus == 1) ).run() billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row - dn = frappe.qb.DocType("Delivery Note").as_("dn") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") - dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where( + dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where( (dn.name == dn_item.parent) & (dn_item.so_detail == so_detail) & (dn.docstatus == 1) & @@ -385,11 +381,7 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): # Distribute billed amount directly against SO between DNs based on FIFO if billed_against_so and billed_amt_agianst_dn < dnd.amount: - if dnd.returned_qty: - pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty - else: - pending_to_bill = flt(dnd.amount) - pending_to_bill -= billed_amt_agianst_dn + pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn if pending_to_bill <= billed_against_so: billed_amt_agianst_dn += pending_to_bill billed_against_so -= pending_to_bill From c7b9df5aee0992ffd45ca8b2c4e2e7308e92b36e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 12:40:33 +0530 Subject: [PATCH 124/133] fix: Set Pending Qty in Prod Plan after updating Work Order (cherry picked from commit 7116d7ae0eabe9c31e03b84466ba74751e9479a9) --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f6d2a14a074..c7e5d5a8614 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -280,6 +280,7 @@ class ProductionPlan(Document): for data in self.po_items: if data.name == production_plan_item: data.produced_qty = produced_qty + data.pending_qty = flt(data.planned_qty - produced_qty) data.db_update() self.calculate_total_produced_qty() From 6800c3efaeb58c9898faf362788ae71ba298fd3a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 17:55:38 +0530 Subject: [PATCH 125/133] fix: Initialise pending qty as planned qty for independent item rows in Prod Plan - Rows that are not fetched from MR or SO, had pending qty 0 throughout - Initialise pending qty on save only. - After submit this field will be updated by work order/stock entry - Bring functions in `validate()` closer to the top (cherry picked from commit eaccef6116f051bfa8c65934c1b45767e7465aaa) --- .../production_plan/production_plan.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c7e5d5a8614..d3a228d7aeb 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -29,9 +29,24 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults class ProductionPlan(Document): def validate(self): + self.set_pending_qty_in_row_without_reference() self.calculate_total_planned_qty() self.set_status() + def set_pending_qty_in_row_without_reference(self): + "Set Pending Qty in independent rows (not from SO or MR)." + if self.docstatus > 0: # set only to initialise value before submit + return + + for item in self.po_items: + if not item.get("sales_order") or not item.get("material_request"): + item.pending_qty = item.planned_qty + + def calculate_total_planned_qty(self): + self.total_planned_qty = 0 + for d in self.po_items: + self.total_planned_qty += flt(d.planned_qty) + def validate_data(self): for d in self.get('po_items'): if not d.bom_no: @@ -264,11 +279,6 @@ class ProductionPlan(Document): 'qty': so_detail['qty'] }) - def calculate_total_planned_qty(self): - self.total_planned_qty = 0 - for d in self.po_items: - self.total_planned_qty += flt(d.planned_qty) - def calculate_total_produced_qty(self): self.total_produced_qty = 0 for d in self.po_items: From 1df4e2c44a27b22f308935d64700f7abcd468a6a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Feb 2022 20:14:28 +0530 Subject: [PATCH 126/133] test: Production Plan Pending Qty impact tests - Two tests to check impact on pending qty: From SO and independent Prod Plan - Added docstring to each test case for brief summary - Changed helper function args to fallback to 0 instead of 1 if no arg is passed - Removed unnecessary `get_doc()` - Made helper function actions optional depending on args passed (cherry picked from commit 86ca41b14af45f44ec63a27ed10580b161a33b4c) --- .../production_plan/production_plan.py | 2 +- .../production_plan/test_production_plan.py | 253 ++++++++++++++---- .../doctype/work_order/work_order.py | 2 +- 3 files changed, 209 insertions(+), 48 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d3a228d7aeb..1ccd9dc7ad7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -286,7 +286,7 @@ class ProductionPlan(Document): self.db_set("total_produced_qty", self.total_produced_qty, update_modified=False) - def update_produced_qty(self, produced_qty, production_plan_item): + def update_produced_pending_qty(self, produced_qty, production_plan_item): for data in self.po_items: if data.name == production_plan_item: data.produced_qty = produced_qty diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 276e70859e6..3aa5c9f008d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) @@ -36,15 +37,21 @@ class TestProductionPlan(ERPNextTestCase): if not frappe.db.get_value('BOM', {'item': item}): make_bom(item = item, raw_materials = raw_materials) - def test_production_plan(self): + def test_production_plan_mr_creation(self): + "Test if MRs are created for unavailable raw materials." pln = create_production_plan(item_code='Test Production Item 1') self.assertTrue(len(pln.mr_items), 2) - pln.make_material_request() - pln = frappe.get_doc('Production Plan', pln.name) + pln.make_material_request() + pln.reload() self.assertTrue(pln.status, 'Material Requested') - material_requests = frappe.get_all('Material Request Item', fields = ['distinct parent'], - filters = {'production_plan': pln.name}, as_list=1) + + material_requests = frappe.get_all( + 'Material Request Item', + fields = ['distinct parent'], + filters = {'production_plan': pln.name}, + as_list=1 + ) self.assertTrue(len(material_requests), 2) @@ -66,27 +73,42 @@ class TestProductionPlan(ERPNextTestCase): pln.cancel() def test_production_plan_start_date(self): + "Test if Work Order has same Planned Start Date as Prod Plan." planned_date = add_to_date(date=None, days=3) - plan = create_production_plan(item_code='Test Production Item 1', planned_start_date=planned_date) + plan = create_production_plan( + item_code='Test Production Item 1', + planned_start_date=planned_date + ) plan.make_work_order() - work_orders = frappe.get_all('Work Order', fields = ['name', 'planned_start_date'], - filters = {'production_plan': plan.name}) + work_orders = frappe.get_all( + 'Work Order', + fields = ['name', 'planned_start_date'], + filters = {'production_plan': plan.name} + ) self.assertEqual(work_orders[0].planned_start_date, planned_date) for wo in work_orders: frappe.delete_doc('Work Order', wo.name) - frappe.get_doc('Production Plan', plan.name).cancel() + plan.reload() + plan.cancel() def test_production_plan_for_existing_ordered_qty(self): + """ + - Enable 'ignore_existing_ordered_qty'. + - Test if MR Planning table pulls Raw Material Qty even if it is in stock. + """ sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=110) sr2 = create_stock_reconciliation(item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120) - pln = create_production_plan(item_code='Test Production Item 1', ignore_existing_ordered_qty=0) + pln = create_production_plan( + item_code='Test Production Item 1', + ignore_existing_ordered_qty=1 + ) self.assertTrue(len(pln.mr_items), 1) self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) @@ -95,23 +117,39 @@ class TestProductionPlan(ERPNextTestCase): pln.cancel() def test_production_plan_with_non_stock_item(self): - pln = create_production_plan(item_code='Test Production Item 1', include_non_stock_items=0) + "Test if MR Planning table includes Non Stock RM." + pln = create_production_plan( + item_code='Test Production Item 1', + include_non_stock_items=1 + ) self.assertTrue(len(pln.mr_items), 3) pln.cancel() def test_production_plan_without_multi_level(self): - pln = create_production_plan(item_code='Test Production Item 1', use_multi_level_bom=0) + "Test MR Planning table for non exploded BOM." + pln = create_production_plan( + item_code='Test Production Item 1', + use_multi_level_bom=0 + ) self.assertTrue(len(pln.mr_items), 2) pln.cancel() def test_production_plan_without_multi_level_for_existing_ordered_qty(self): + """ + - Disable 'ignore_existing_ordered_qty'. + - Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for + non exploded BOM. + """ sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130) sr2 = create_stock_reconciliation(item_code="Subassembly Item 1", target="_Test Warehouse - _TC", qty=1, rate=140) - pln = create_production_plan(item_code='Test Production Item 1', - use_multi_level_bom=0, ignore_existing_ordered_qty=0) + pln = create_production_plan( + item_code='Test Production Item 1', + use_multi_level_bom=0, + ignore_existing_ordered_qty=0 + ) self.assertTrue(len(pln.mr_items), 0) sr1.cancel() @@ -119,6 +157,7 @@ class TestProductionPlan(ERPNextTestCase): pln.cancel() def test_production_plan_sales_orders(self): + "Test if previously fulfilled SO (with WO) is pulled into Prod Plan." item = 'Test Production Item 1' so = make_sales_order(item_code=item, qty=1) sales_order = so.name @@ -166,24 +205,25 @@ class TestProductionPlan(ERPNextTestCase): self.assertEqual(sales_orders, []) def test_production_plan_combine_items(self): + "Test combining FG items in Production Plan." item = 'Test Production Item 1' - so = make_sales_order(item_code=item, qty=1) + so1 = make_sales_order(item_code=item, qty=1) pln = frappe.new_doc('Production Plan') - pln.company = so.company + pln.company = so1.company pln.get_items_from = 'Sales Order' pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total + 'sales_order': so1.name, + 'sales_order_date': so1.transaction_date, + 'customer': so1.customer, + 'grand_total': so1.grand_total }) - so = make_sales_order(item_code=item, qty=2) + so2 = make_sales_order(item_code=item, qty=2) pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total + 'sales_order': so2.name, + 'sales_order_date': so2.transaction_date, + 'customer': so2.customer, + 'grand_total': so2.grand_total }) pln.combine_items = 1 pln.get_items() @@ -214,28 +254,37 @@ class TestProductionPlan(ERPNextTestCase): so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') self.assertEqual(so_wo_qty, 0.0) - latest_plan = frappe.get_doc('Production Plan', pln.name) - latest_plan.cancel() + pln.reload() + pln.cancel() def test_pp_to_mr_customer_provided(self): - #Material Request from Production Plan for Customer Provided + " Test Material Request from Production Plan for Customer Provided Item." create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('Production Item CUST') + for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items(): if not frappe.db.get_value('BOM', {'item': item}): make_bom(item = item, raw_materials = raw_materials) production_plan = create_production_plan(item_code = 'Production Item CUST') production_plan.make_material_request() - material_request = frappe.db.get_value('Material Request Item', {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, 'parent') + + material_request = frappe.db.get_value( + 'Material Request Item', + {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, + 'parent' + ) mr = frappe.get_doc('Material Request', material_request) + self.assertTrue(mr.material_request_type, 'Customer Provided') self.assertTrue(mr.customer, '_Test Customer') def test_production_plan_with_multi_level_bom(self): - #|Item Code | Qty | - #|Test BOM 1 | 1 | - #| Test BOM 2 | 2 | - #| Test BOM 3 | 3 | + """ + Item Code | Qty | + |Test BOM 1 | 1 | + |Test BOM 2 | 2 | + |Test BOM 3 | 3 | + """ for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]: create_item(item_code, is_stock_item=1) @@ -264,15 +313,18 @@ class TestProductionPlan(ERPNextTestCase): pln.make_work_order() #last level sub-assembly work order produce qty - to_produce_qty = frappe.db.get_value("Work Order", - {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty") + to_produce_qty = frappe.db.get_value( + "Work Order", + {"production_plan": pln.name, "production_item": "Test BOM 3"}, + "qty" + ) self.assertEqual(to_produce_qty, 18.0) pln.cancel() frappe.delete_doc("Production Plan", pln.name) def test_get_warehouse_list_group(self): - """Check if required warehouses are returned""" + "Check if required child warehouses are returned." warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' warehouses = set(get_warehouse_list(warehouse_json)) @@ -284,6 +336,7 @@ class TestProductionPlan(ERPNextTestCase): msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") def test_get_warehouse_list_single(self): + "Check if same warehouse is returned in absence of child warehouses." warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' warehouses = set(get_warehouse_list(warehouse_json)) @@ -292,6 +345,7 @@ class TestProductionPlan(ERPNextTestCase): self.assertEqual(warehouses, expected_warehouses) def test_get_sales_order_with_variant(self): + "Check if Template BOM is fetched in absence of Variant BOM." rm_item = create_item('PIV_RM', valuation_rate = 100) if not frappe.db.exists('Item', {"item_code": 'PIV'}): item = create_item('PIV', valuation_rate = 100) @@ -348,7 +402,7 @@ class TestProductionPlan(ERPNextTestCase): frappe.db.rollback() def test_subassmebly_sorting(self): - """ Test subassembly sorting in case of multiple items with nested BOMs""" + "Test subassembly sorting in case of multiple items with nested BOMs." from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom prefix = "_TestLevel_" @@ -386,6 +440,7 @@ class TestProductionPlan(ERPNextTestCase): self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) def test_multiple_work_order_for_production_plan_item(self): + "Test producing Prod Plan (making WO) in parts." def create_work_order(item, pln, qty): # Get Production Items items_data = pln.get_production_items() @@ -441,7 +496,98 @@ class TestProductionPlan(ERPNextTestCase): pln.reload() self.assertEqual(pln.po_items[0].ordered_qty, 0) + def test_production_plan_pending_qty_with_sales_order(self): + """ + Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel) + """ + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + make_stock_entry(item_code="Raw Material Item 1", + target="_Test Warehouse - _TC", + qty=2, basic_rate=100 + ) + make_stock_entry(item_code="Raw Material Item 2", + target="_Test Warehouse - _TC", + qty=2, basic_rate=100 + ) + + item = 'Test Production Item 1' + so = make_sales_order(item_code=item, qty=1) + + pln = create_production_plan( + company=so.company, + get_items_from="Sales Order", + sales_order=so, + skip_getting_mr_items=True + ) + self.assertEqual(pln.po_items[0].pending_qty, 1) + + wo = make_wo_order_test_record( + item_code=item, qty=1, + company=so.company, + wip_warehouse='Work In Progress - _TC', + fg_warehouse='Finished Goods - _TC', + skip_transfer=1, + do_not_submit=True + ) + wo.production_plan = pln.name + wo.production_plan_item = pln.po_items[0].name + wo.submit() + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) + se.submit() + + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 0) + + se.cancel() + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 1) + + def test_production_plan_pending_qty_independent_items(self): + "Test Prod Plan impact if items are added independently (no from SO or MR)." + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + pln = create_production_plan( + item_code='Test Production Item 1', + skip_getting_mr_items=True + ) + self.assertEqual(pln.po_items[0].pending_qty, 1) + + wo = make_wo_order_test_record( + item_code='Test Production Item 1', qty=1, + company=pln.company, + wip_warehouse='Work In Progress - _TC', + fg_warehouse='Finished Goods - _TC', + skip_transfer=1, + do_not_submit=True + ) + wo.production_plan = pln.name + wo.production_plan_item = pln.po_items[0].name + wo.submit() + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) + se.submit() + + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 0) + + se.cancel() + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 1) + def create_production_plan(**args): + """ + sales_order (obj): Sales Order Doc Object + get_items_from (str): Sales Order/Material Request + skip_getting_mr_items (bool): Whether or not to plan for new MRs + """ args = frappe._dict(args) pln = frappe.get_doc({ @@ -449,20 +595,35 @@ def create_production_plan(**args): 'company': args.company or '_Test Company', 'customer': args.customer or '_Test Customer', 'posting_date': nowdate(), - 'include_non_stock_items': args.include_non_stock_items or 1, - 'include_subcontracted_items': args.include_subcontracted_items or 1, - 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 1, - 'po_items': [{ + 'include_non_stock_items': args.include_non_stock_items or 0, + 'include_subcontracted_items': args.include_subcontracted_items or 0, + 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0, + 'get_items_from': 'Sales Order' + }) + + if not args.get("sales_order"): + pln.append('po_items', { 'use_multi_level_bom': args.use_multi_level_bom or 1, 'item_code': args.item_code, 'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'), 'planned_qty': args.planned_qty or 1, 'planned_start_date': args.planned_start_date or now_datetime() - }] - }) - mr_items = get_items_for_material_requests(pln.as_dict()) - for d in mr_items: - pln.append('mr_items', d) + }) + + if args.get("get_items_from") == "Sales Order" and args.get("sales_order"): + so = args.get("sales_order") + pln.append('sales_orders', { + 'sales_order': so.name, + 'sales_order_date': so.transaction_date, + 'customer': so.customer, + 'grand_total': so.grand_total + }) + pln.get_items() + + if not args.get("skip_getting_mr_items"): + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append('mr_items', d) if not args.do_not_save: pln.insert() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8a08c2c6248..f50c82c66b6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -272,7 +272,7 @@ class WorkOrder(Document): produced_qty = total_qty[0][0] if total_qty else 0 - production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) + production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) def before_submit(self): self.create_serial_no_batch_no() From 698f910a300f283a3cfa332ef491fba2e44110f6 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Feb 2022 21:00:51 +0530 Subject: [PATCH 127/133] fix: Server Tests and sider (cherry picked from commit e46a1bc80fd2aaf01be4298af0d2b9e93fbdcd24) --- .../production_plan/test_production_plan.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 3aa5c9f008d..afa1501efcd 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -138,7 +138,7 @@ class TestProductionPlan(ERPNextTestCase): """ - Disable 'ignore_existing_ordered_qty'. - Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for - non exploded BOM. + non exploded BOM. """ sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130) @@ -506,11 +506,11 @@ class TestProductionPlan(ERPNextTestCase): ) make_stock_entry(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", + target="Work In Progress - _TC", qty=2, basic_rate=100 ) make_stock_entry(item_code="Raw Material Item 2", - target="_Test Warehouse - _TC", + target="Work In Progress - _TC", qty=2, basic_rate=100 ) @@ -554,6 +554,15 @@ class TestProductionPlan(ERPNextTestCase): make_stock_entry as make_se_from_wo, ) + make_stock_entry(item_code="Raw Material Item 1", + target="Work In Progress - _TC", + qty=2, basic_rate=100 + ) + make_stock_entry(item_code="Raw Material Item 2", + target="Work In Progress - _TC", + qty=2, basic_rate=100 + ) + pln = create_production_plan( item_code='Test Production Item 1', skip_getting_mr_items=True From 3d45ea0c1f9842620222ed4f1f3b2d134d3492d8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Feb 2022 18:14:28 +0530 Subject: [PATCH 128/133] fix: Generate Wh wise FIFO Queue and later aggregate if required - Back to back stock recos cause incorrect qty calculation across warehouses - Hard to differentiate how much of the qty is reset by the reco - Maintain Queue and balances warehouse wise and later aggregate for accurate values (cherry picked from commit f62b3207ff4c947f2f45006755134761c30bec96) --- .../stock/report/stock_ageing/stock_ageing.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index e6dfc97a998..a89a4038c20 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -252,6 +252,7 @@ class FIFOSlots: key, fifo_queue, transferred_item_key = self.__init_key_stores(d) if d.voucher_type == "Stock Reconciliation": + # get difference in qty shift as actual qty prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) @@ -264,12 +265,16 @@ class FIFOSlots: self.__update_balances(d, key) + if not self.filters.get("show_warehouse_wise_stock"): + # (Item 1, WH 1), (Item 1, WH 2) => (Item 1) + self.item_details = self.__aggregate_details_by_item(self.item_details) + return self.item_details def __init_key_stores(self, row: Dict) -> Tuple: "Initialise keys and FIFO Queue." - key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name + key = (row.name, row.warehouse) self.item_details.setdefault(key, {"details": row, "fifo_queue": []}) fifo_queue = self.item_details[key]["fifo_queue"] @@ -338,6 +343,27 @@ class FIFOSlots: self.item_details[key]["has_serial_no"] = row.has_serial_no + def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: + "Aggregate Item-Wh wise data into single Item entry." + item_aggregated_data = {} + for key,row in wh_wise_data.items(): + item = key[0] + if not item_aggregated_data.get(item): + item_aggregated_data.setdefault(item, { + "details": frappe._dict(), + "fifo_queue": [], + "qty_after_transaction": 0.0, + "total_qty": 0.0 + }) + item_row = item_aggregated_data.get(item) + item_row["details"].update(row["details"]) + item_row["fifo_queue"].extend(row["fifo_queue"]) + item_row["qty_after_transaction"] += flt(row["qty_after_transaction"]) + item_row["total_qty"] += flt(row["total_qty"]) + item_row["has_serial_no"] = row["has_serial_no"] + + return item_aggregated_data + def __get_stock_ledger_entries(self) -> List[Dict]: sle = frappe.qb.DocType("Stock Ledger Entry") item = self.__get_item_query() # used as derived table in sle query From e9ef1cbd3db30b7db912240aa31c7d63f79377c2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Feb 2022 20:14:14 +0530 Subject: [PATCH 129/133] test: Cover back to back recos from different warehouses (cherry picked from commit f221a0d253c6c4a2dc1faf4b41f371bf5a7e86ad) --- .../stock_ageing/stock_ageing_fifo_logic.md | 1 + .../report/stock_ageing/test_stock_ageing.py | 127 +++++++++++++++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md index 5ffe97fd742..9e9bed48e3e 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md +++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md @@ -15,6 +15,7 @@ Here, the balance qty is 70. 50 qty is (today-the 1st) days old 20 qty is (today-the 2nd) days old +> Note: We generate FIFO slots warehouse wise as stock reconciliations from different warehouses can cause incorrect values. ### Calculation of FIFO Slots #### Case 1: Outward from sufficient balance qty diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 949bb7c15a8..66d2f6b7539 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -15,11 +15,12 @@ class TestStockAgeing(ERPNextTestCase): ) def test_normal_inward_outward_queue(self): - "Reference: Case 1 in stock_ageing_fifo_logic.md" + "Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)" sle = [ frappe._dict( name="Flask Item", actual_qty=30, qty_after_transaction=30, + warehouse="WH 1", posting_date="2021-12-01", voucher_type="Stock Entry", voucher_no="001", has_serial_no=False, serial_no=None @@ -27,6 +28,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=20, qty_after_transaction=50, + warehouse="WH 1", posting_date="2021-12-02", voucher_type="Stock Entry", voucher_no="002", has_serial_no=False, serial_no=None @@ -34,6 +36,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=(-10), qty_after_transaction=40, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="003", has_serial_no=False, serial_no=None @@ -50,11 +53,12 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[0][0], 20.0) def test_insufficient_balance(self): - "Reference: Case 3 in stock_ageing_fifo_logic.md" + "Reference: Case 3 in stock_ageing_fifo_logic.md (same wh)" sle = [ frappe._dict( name="Flask Item", actual_qty=(-30), qty_after_transaction=(-30), + warehouse="WH 1", posting_date="2021-12-01", voucher_type="Stock Entry", voucher_no="001", has_serial_no=False, serial_no=None @@ -62,6 +66,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=20, qty_after_transaction=(-10), + warehouse="WH 1", posting_date="2021-12-02", voucher_type="Stock Entry", voucher_no="002", has_serial_no=False, serial_no=None @@ -69,6 +74,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=20, qty_after_transaction=10, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="003", has_serial_no=False, serial_no=None @@ -76,6 +82,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=10, qty_after_transaction=20, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="004", has_serial_no=False, serial_no=None @@ -91,11 +98,16 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[0][0], 10.0) self.assertEqual(queue[1][0], 10.0) - def test_stock_reconciliation(self): + def test_basic_stock_reconciliation(self): + """ + Ledger (same wh): [+30, reco reset >> 50, -10] + Bal: 40 + """ sle = [ frappe._dict( name="Flask Item", actual_qty=30, qty_after_transaction=30, + warehouse="WH 1", posting_date="2021-12-01", voucher_type="Stock Entry", voucher_no="001", has_serial_no=False, serial_no=None @@ -103,6 +115,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=0, qty_after_transaction=50, + warehouse="WH 1", posting_date="2021-12-02", voucher_type="Stock Reconciliation", voucher_no="002", has_serial_no=False, serial_no=None @@ -110,6 +123,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=(-10), qty_after_transaction=40, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="003", has_serial_no=False, serial_no=None @@ -122,5 +136,112 @@ class TestStockAgeing(ERPNextTestCase): queue = result["fifo_queue"] self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(result["total_qty"], 40.0) self.assertEqual(queue[0][0], 20.0) self.assertEqual(queue[1][0], 20.0) + + def test_sequential_stock_reco_same_warehouse(self): + """ + Test back to back stock recos (same warehouse). + Ledger: [reco opening >> +1000, reco reset >> 400, -10] + Bal: 390 + """ + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=1000, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-02", voucher_type="Stock Reconciliation", + voucher_no="003", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), qty_after_transaction=390, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="003", + has_serial_no=False, serial_no=None + ) + ] + slots = FIFOSlots(self.filters, sle).generate() + + result = slots["Flask Item"] + queue = result["fifo_queue"] + + self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(result["total_qty"], 390.0) + self.assertEqual(queue[0][0], 390.0) + + def test_sequential_stock_reco_different_warehouse(self): + """ + Ledger: + WH | Voucher | Qty + ------------------- + WH1 | Reco | 1000 + WH2 | Reco | 400 + WH1 | SE | -10 + + Bal: WH1 bal + WH2 bal = 990 + 400 = 1390 + """ + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=1000, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=400, + warehouse="WH 2", + posting_date="2021-12-02", voucher_type="Stock Reconciliation", + voucher_no="003", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), qty_after_transaction=990, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="004", + has_serial_no=False, serial_no=None + ) + ] + + item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots( + filters=self.filters,sle=sle + ) + + # test without 'show_warehouse_wise_stock' + item_result = item_wise_slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"]) + self.assertEqual(item_result["total_qty"], 1390.0) + self.assertEqual(queue[0][0], 990.0) + self.assertEqual(queue[1][0], 400.0) + + # test with 'show_warehouse_wise_stock' checked + item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] + self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) + +def generate_item_and_item_wh_wise_slots(filters, sle): + "Return results with and without 'show_warehouse_wise_stock'" + item_wise_slots = FIFOSlots(filters, sle).generate() + + filters.show_warehouse_wise_stock = True + 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 From 5d608edc54553a13ac3b90d95dc3c4cc8060cc57 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Feb 2022 18:38:02 +0530 Subject: [PATCH 130/133] fix: disable rounded total in opening invoice creation tool (cherry picked from commit 18d0a59a9d4d4fd35ce997f2d23aa7ced930b00e) --- .../opening_invoice_creation_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 00eecd3a4f4..5ba0131f6c0 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -167,7 +167,8 @@ class OpeningInvoiceCreationTool(Document): "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "update_stock": 0, - "invoice_number": row.invoice_number + "invoice_number": row.invoice_number, + "disable_rounded_total": 1 }) accounting_dimension = get_accounting_dimensions() From 83ab63ba814fd2e622f74e567f8401a3591e1351 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 15 Feb 2022 11:37:29 +0530 Subject: [PATCH 131/133] chore: warning for Amazon MWS integration deprecation (#29792) * chore: warning for amazon mws integration deprecation * style: spaces to tabs [skip ci] --- .../amazon_mws_settings/amazon_mws_settings.js | 8 ++++++++ erpnext/patches.txt | 3 ++- .../v13_0/amazon_mws_deprecation_warning.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/amazon_mws_deprecation_warning.py diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js index f5ea8047c6a..a15558bc2b6 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js @@ -1,2 +1,10 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt + + +frappe.ui.form.on('Amazon MWS Settings', { + refresh: function (frm) { + let app_link = "Ecommerce Integrations" + frm.dashboard.add_comment(__("Amazon MWS Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); + } +}); diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f4a6e089637..b3366f3cba7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -348,4 +348,5 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail 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 \ No newline at end of file +erpnext.patches.v13_0.update_reserved_qty_closed_wo +erpnext.patches.v13_0.amazon_mws_deprecation_warning \ No newline at end of file diff --git a/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py new file mode 100644 index 00000000000..5eb6ff44702 --- /dev/null +++ b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py @@ -0,0 +1,15 @@ +import click +import frappe + + +def execute(): + + frappe.reload_doc("erpnext_integrations", "doctype", "amazon_mws_settings") + if not frappe.db.get_single_value("Amazon MWS Settings", "enable_amazon"): + return + + click.secho( + "Amazon MWS Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", + fg="yellow", + ) \ No newline at end of file From ea3acb09beac8f4f86dff90dcab466052e4dda4f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Feb 2022 19:51:58 +0530 Subject: [PATCH 132/133] fix: Fixes in TDS payable monthly report (cherry picked from commit 2ff6b3560e6ec8820a6ba8cccba24945e089d7d2) --- .../tds_payable_monthly/tds_payable_monthly.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index caee1a10bbb..9eeeb3a6804 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -23,7 +23,7 @@ def validate_filters(filters): def get_result(filters, tds_docs, tds_accounts, tax_category_map): supplier_map = get_supplier_pan_map() tax_rate_map = get_tax_rate_map(filters) - gle_map = get_gle_map(filters, tds_docs) + gle_map = get_gle_map(tds_docs) out = [] for name, details in gle_map.items(): @@ -78,7 +78,7 @@ def get_supplier_pan_map(): return supplier_map -def get_gle_map(filters, documents): +def get_gle_map(documents): # create gle_map of the form # {"purchase_invoice": list of dict of all gle created for this invoice} gle_map = {} @@ -86,7 +86,7 @@ def get_gle_map(filters, documents): gle = frappe.db.get_all('GL Entry', { "voucher_no": ["in", documents], - "credit": (">", 0) + "is_cancelled": 0 }, ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"], ) @@ -184,21 +184,25 @@ def get_tds_docs(filters): payment_entries = [] journal_entries = [] tax_category_map = {} + or_filters={} tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, pluck="account") query_filters = { - "credit": ('>', 0), "account": ("in", tds_accounts), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), "is_cancelled": 0 } - if filters.get('supplier'): - query_filters.update({'against': filters.get('supplier')}) + if filters.get("supplier"): + del query_filters["account"] + or_filters = { + "against": filters.get('supplier'), + "party": filters.get('supplier') + } - tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"]) + tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"]) for d in tds_docs: if d.voucher_type == "Purchase Invoice": From 038d729462303ddfa52a7a2c0505fdd3a1424dc4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Feb 2022 20:38:04 +0530 Subject: [PATCH 133/133] fix: Filter out bank payment entries (cherry picked from commit 04cbde2e52bc9839b8ce3d6446c870f9957b614d) --- .../report/tds_payable_monthly/tds_payable_monthly.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 9eeeb3a6804..57f79748f0a 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -184,7 +184,8 @@ def get_tds_docs(filters): payment_entries = [] journal_entries = [] tax_category_map = {} - or_filters={} + or_filters = {} + bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name") tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, pluck="account") @@ -192,11 +193,13 @@ def get_tds_docs(filters): query_filters = { "account": ("in", tds_accounts), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), - "is_cancelled": 0 + "is_cancelled": 0, + "against": ("not in", bank_accounts) } if filters.get("supplier"): del query_filters["account"] + del query_filters["against"] or_filters = { "against": filters.get('supplier'), "party": filters.get('supplier')