From 2cf82561f6c36702654cd745c6fb6ecfd757f0cc Mon Sep 17 00:00:00 2001 From: Markus Lobedann Date: Wed, 3 Jul 2024 12:21:12 +0200 Subject: [PATCH 01/20] refactor: remove obsolete function call (#42162) (cherry picked from commit 45124328160862e4f2c1dd7523c474bb35db302d) --- .../doctype/import_supplier_invoice/import_supplier_invoice.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js index 7aa8012f0b6..80dd246eb8a 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js @@ -36,7 +36,6 @@ frappe.ui.form.on("Import Supplier Invoice", { toggle_read_only_fields: function (frm) { if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) { cur_frm.set_read_only(); - cur_frm.refresh_fields(); frm.set_df_property("import_invoices", "hidden", 1); } else { frm.set_df_property("import_invoices", "hidden", 0); From 701dd9e19bb13d5e8e33337a3d77b4322635a803 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 Jul 2024 15:58:19 +0530 Subject: [PATCH 02/20] fix: multiple free items on same Item Group (cherry picked from commit c4ae0d283fe24ef0bba45266df8c2f4d1cdf22ba) --- erpnext/public/js/controllers/transaction.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e3dae3e6f97..6828dc9c745 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1614,12 +1614,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe apply_product_discount(args) { const items = this.frm.doc.items.filter(d => (d.is_free_item)) || []; - const exist_items = items.map(row => (row.item_code, row.pricing_rules)); + const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};}); args.free_item_data.forEach(pr_row => { let row_to_modify = {}; - if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) { + // If there are no free items, or if the current free item doesn't exist in the table, add it + if (!items || !exist_items.filter(e_row => { + return e_row.item_code == pr_row.item_code && e_row.pricing_rules == pr_row.pricing_rules; + }).length) { row_to_modify = frappe.model.add_child(this.frm.doc, this.frm.doc.doctype + ' Item', 'items'); From 454e1475925ce0975479b2939273e7b4f48f3a5d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:28:18 +0530 Subject: [PATCH 03/20] fix: manual pick allow to pick more than available stock (backport #42155) (#42158) * fix: manual pick allow to pick more than available stock (#42155) (cherry picked from commit 938dd4b2aa4c3d85c9c29692331a9df54fed0cbc) # Conflicts: # erpnext/stock/doctype/pick_list/test_pick_list.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/pick_list/pick_list.py | 59 ++++++++++++++++++- .../stock/doctype/pick_list/test_pick_list.py | 42 +++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 99858835fed..2a5e527f591 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -6,7 +6,7 @@ from collections import OrderedDict, defaultdict from itertools import groupby import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case @@ -26,6 +26,9 @@ from erpnext.stock.get_item_details import get_conversion_factor class PickList(Document): def validate(self): self.validate_for_qty() + if self.pick_manually and self.get("locations"): + self.validate_stock_qty() + self.check_serial_no_status() def before_save(self): self.update_status() @@ -35,6 +38,60 @@ class PickList(Document): if self.get("locations"): self.validate_sales_order_percentage() + def validate_stock_qty(self): + from erpnext.stock.doctype.batch.batch import get_batch_qty + + for row in self.get("locations"): + if row.batch_no and not row.qty: + batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code) + + if row.qty > batch_qty: + frappe.throw( + _( + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}." + ).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)), + title=_("Insufficient Stock"), + ) + + continue + + bin_qty = frappe.db.get_value( + "Bin", + {"item_code": row.item_code, "warehouse": row.warehouse}, + "actual_qty", + ) + + if row.qty > bin_qty: + frappe.throw( + _( + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." + ).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)), + title=_("Insufficient Stock"), + ) + + def check_serial_no_status(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + for row in self.get("locations"): + if not row.serial_no: + continue + + picked_serial_nos = get_serial_nos(row.serial_no) + validated_serial_nos = frappe.get_all( + "Serial No", + filters={"name": ("in", picked_serial_nos), "warehouse": row.warehouse}, + pluck="name", + ) + + incorrect_serial_nos = set(picked_serial_nos) - set(validated_serial_nos) + if incorrect_serial_nos: + frappe.throw( + _("The Serial No at Row #{0}: {1} is not available in warehouse {2}.").format( + row.idx, ", ".join(incorrect_serial_nos), row.warehouse + ), + title=_("Incorrect Warehouse"), + ) + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 4a9a1a50ac6..162fcadde7a 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -802,3 +802,45 @@ class TestPickList(FrappeTestCase): a = set(picked_serial_no) b = set([x for x in location.serial_no.split("\n") if x]) self.assertSetEqual(b, b.difference(a)) + + def test_validate_picked_qty_with_manual_option(self): + warehouse = "_Test Warehouse - _TC" + non_serialized_item = make_item( + "Test Non Serialized Pick List Item For Manual Option", properties={"is_stock_item": 1} + ).name + + serialized_item = make_item( + "Test Serialized Pick List Item For Manual Option", + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-HSNMSPLI-.####"}, + ).name + + batched_item = make_item( + "Test Batched Pick List Item For Manual Option", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "SN-HBNMSPLI-.####", + "create_new_batch": 1, + }, + ).name + + make_stock_entry(item=non_serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100) + make_stock_entry(item=serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100) + make_stock_entry(item=batched_item, to_warehouse=warehouse, qty=10, basic_rate=100) + + so = make_sales_order( + item_code=non_serialized_item, qty=10, rate=100, do_not_save=True, warehouse=warehouse + ) + so.append("items", {"item_code": serialized_item, "qty": 10, "rate": 100, "warehouse": warehouse}) + so.append("items", {"item_code": batched_item, "qty": 10, "rate": 100, "warehouse": warehouse}) + so.set_missing_values() + so.save() + so.submit() + + pl = create_pick_list(so.name) + pl.pick_manually = 1 + + for row in pl.locations: + row.qty = row.qty + 10 + + self.assertRaises(frappe.ValidationError, pl.save) From 49fb6bec6a5ff5a3cda68fd20856e85ece9543e0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 2 Jul 2024 15:44:54 +0530 Subject: [PATCH 04/20] refactor: validation to prevent recursion with mixed conditions (cherry picked from commit 406dfd528f2cabc60ece0c917fb02092abed50cc) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 5 +++++ .../doctype/promotional_scheme/promotional_scheme.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 30dbb14f84c..6a2f3a2a045 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -31,6 +31,7 @@ class PricingRule(Document): self.validate_price_list_with_currency() self.validate_dates() self.validate_condition() + self.validate_mixed_with_recursion() if not self.margin_type: self.margin_rate_or_amount = 0.0 @@ -201,6 +202,10 @@ class PricingRule(Document): ): frappe.throw(_("Invalid condition expression")) + def validate_mixed_with_recursion(self): + if self.mixed_conditions and self.is_recursive: + frappe.throw(_("Recursive Discounts with Mixed condition is not supported by the system")) + # -------------------------------------------------------------------------------- diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index e3278098f1a..f5bc450f6dc 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -77,6 +77,7 @@ class PromotionalScheme(Document): self.validate_applicable_for() self.validate_pricing_rules() + self.validate_mixed_with_recursion() def validate_applicable_for(self): if self.applicable_for: @@ -108,6 +109,7 @@ class PromotionalScheme(Document): frappe.delete_doc("Pricing Rule", docname.name) def on_update(self): + self.validate() pricing_rules = ( frappe.get_all( "Pricing Rule", @@ -119,6 +121,15 @@ class PromotionalScheme(Document): ) self.update_pricing_rules(pricing_rules) + def validate_mixed_with_recursion(self): + if self.mixed_conditions: + if self.product_discount_slabs: + for slab in self.product_discount_slabs: + if slab.is_recursive: + frappe.throw( + _("Recursive Discounts with Mixed condition is not supported by the system") + ) + def update_pricing_rules(self, pricing_rules): rules = {} count = 0 From 9fde7330e0d5a4e2a24bc7ae5a2277f913a9b972 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 Jul 2024 17:05:13 +0530 Subject: [PATCH 05/20] fix: use standard method to get `_doc_before_save` (cherry picked from commit 9d7be293ae9d7d2a7cf9f81fa1aadcc071bc29e6) --- .../accounts/doctype/promotional_scheme/promotional_scheme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index f5bc450f6dc..857ee0e7187 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -95,7 +95,7 @@ class PromotionalScheme(Document): docnames = [] # If user has changed applicable for - if self._doc_before_save.applicable_for == self.applicable_for: + if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for: return docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) From 99317768f68cf44263b71ab2079b237923d43cb5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 Jul 2024 17:11:32 +0530 Subject: [PATCH 06/20] test: validation on mixed condition with recursion (cherry picked from commit 9bd4e7b7094830f56fdb6fd492d1a0873bf5b977) # Conflicts: # erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py --- .../test_promotional_scheme.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 9e576fb8775..e5b82304d11 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -107,6 +107,50 @@ class TestPromotionalScheme(unittest.TestCase): price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) +<<<<<<< HEAD +======= + def test_pricing_rule_for_product_discount_slabs(self): + ps = make_promotional_scheme() + ps.set("price_discount_slabs", []) + ps.set( + "product_discount_slabs", + [ + { + "rule_description": "12+1", + "min_qty": 12, + "free_item": "_Test Item 2", + "free_qty": 1, + "is_recursive": 1, + "recurse_for": 12, + } + ], + ) + ps.save() + pr = frappe.get_doc("Pricing Rule", {"promotional_scheme_id": ps.product_discount_slabs[0].name}) + self.assertSequenceEqual( + [pr.min_qty, pr.free_item, pr.free_qty, pr.recurse_for], [12, "_Test Item 2", 1, 12] + ) + + def test_validation_on_recurse_with_mixed_condition(self): + ps = make_promotional_scheme() + ps.set("price_discount_slabs", []) + ps.set( + "product_discount_slabs", + [ + { + "rule_description": "12+1", + "min_qty": 12, + "free_item": "_Test Item 2", + "free_qty": 1, + "is_recursive": 1, + "recurse_for": 12, + } + ], + ) + ps.mixed_conditions = True + self.assertRaises(frappe.ValidationError, ps.save) + +>>>>>>> 9bd4e7b709 (test: validation on mixed condition with recursion) def make_promotional_scheme(**args): args = frappe._dict(args) From 71cbebd31b367a9085dc5e57c7a171dfe92f1c7c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 Jul 2024 17:54:41 +0530 Subject: [PATCH 07/20] test: validation on mixed condition and recursion on pricing rule (cherry picked from commit eb4af58bf0371437ba7bca36f345488c6c03331b) # Conflicts: # erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py --- .../doctype/pricing_rule/test_pricing_rule.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 6f1cee61637..48fae36aaf8 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -1087,6 +1087,83 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") +<<<<<<< HEAD +======= + def test_pricing_rules_with_and_without_apply_multiple(self): + item = make_item("PR Item 99") + + test_records = [ + { + "doctype": "Pricing Rule", + "title": "_Test discount on item group", + "name": "_Test discount on item group", + "apply_on": "Item Group", + "item_groups": [ + { + "item_group": "Products", + } + ], + "selling": 1, + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Percentage", + "discount_percentage": 60, + "has_priority": 1, + "company": "_Test Company", + "apply_multiple_pricing_rules": True, + }, + { + "doctype": "Pricing Rule", + "title": "_Test fixed rate on item code", + "name": "_Test fixed rate on item code", + "apply_on": "Item Code", + "items": [ + { + "item_code": item.name, + } + ], + "selling": 1, + "price_or_product_discount": "Price", + "rate_or_discount": "Rate", + "rate": 25, + "has_priority": 1, + "company": "_Test Company", + "apply_multiple_pricing_rules": False, + }, + ] + + for item_group_priority, item_code_priority in [(2, 4), (4, 2)]: + item_group_rule = frappe.get_doc(test_records[0].copy()) + item_group_rule.priority = item_group_priority + item_group_rule.insert() + + item_code_rule = frappe.get_doc(test_records[1].copy()) + item_code_rule.priority = item_code_priority + item_code_rule.insert() + + si = create_sales_invoice(qty=5, customer="_Test Customer 1", item=item.name, do_not_submit=True) + si.save() + self.assertEqual(len(si.pricing_rules), 1) + # Item Code rule should've applied as it has higher priority + expected_rule = item_group_rule if item_group_priority > item_code_priority else item_code_rule + self.assertEqual(si.pricing_rules[0].pricing_rule, expected_rule.name) + + si.delete() + item_group_rule.delete() + item_code_rule.delete() + + def test_validation_on_mixed_condition_with_recursion(self): + pricing_rule = make_pricing_rule( + discount_percentage=10, + selling=1, + priority=2, + min_qty=4, + title="_Test Pricing Rule with Min Qty - 2", + ) + pricing_rule.mixed_conditions = True + pricing_rule.is_recursive = True + self.assertRaises(frappe.ValidationError, pricing_rule.save) + +>>>>>>> eb4af58bf0 (test: validation on mixed condition and recursion on pricing rule) test_dependencies = ["Campaign"] From d5fa968078a901758c5ca9ef8f64333dcd64bc03 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 Jul 2024 20:58:50 +0530 Subject: [PATCH 08/20] chore: resolve conflicts --- .../doctype/pricing_rule/test_pricing_rule.py | 65 ------------------- .../test_promotional_scheme.py | 25 ------- 2 files changed, 90 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 48fae36aaf8..46c874b533c 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -1087,70 +1087,6 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") -<<<<<<< HEAD -======= - def test_pricing_rules_with_and_without_apply_multiple(self): - item = make_item("PR Item 99") - - test_records = [ - { - "doctype": "Pricing Rule", - "title": "_Test discount on item group", - "name": "_Test discount on item group", - "apply_on": "Item Group", - "item_groups": [ - { - "item_group": "Products", - } - ], - "selling": 1, - "price_or_product_discount": "Price", - "rate_or_discount": "Discount Percentage", - "discount_percentage": 60, - "has_priority": 1, - "company": "_Test Company", - "apply_multiple_pricing_rules": True, - }, - { - "doctype": "Pricing Rule", - "title": "_Test fixed rate on item code", - "name": "_Test fixed rate on item code", - "apply_on": "Item Code", - "items": [ - { - "item_code": item.name, - } - ], - "selling": 1, - "price_or_product_discount": "Price", - "rate_or_discount": "Rate", - "rate": 25, - "has_priority": 1, - "company": "_Test Company", - "apply_multiple_pricing_rules": False, - }, - ] - - for item_group_priority, item_code_priority in [(2, 4), (4, 2)]: - item_group_rule = frappe.get_doc(test_records[0].copy()) - item_group_rule.priority = item_group_priority - item_group_rule.insert() - - item_code_rule = frappe.get_doc(test_records[1].copy()) - item_code_rule.priority = item_code_priority - item_code_rule.insert() - - si = create_sales_invoice(qty=5, customer="_Test Customer 1", item=item.name, do_not_submit=True) - si.save() - self.assertEqual(len(si.pricing_rules), 1) - # Item Code rule should've applied as it has higher priority - expected_rule = item_group_rule if item_group_priority > item_code_priority else item_code_rule - self.assertEqual(si.pricing_rules[0].pricing_rule, expected_rule.name) - - si.delete() - item_group_rule.delete() - item_code_rule.delete() - def test_validation_on_mixed_condition_with_recursion(self): pricing_rule = make_pricing_rule( discount_percentage=10, @@ -1163,7 +1099,6 @@ class TestPricingRule(unittest.TestCase): pricing_rule.is_recursive = True self.assertRaises(frappe.ValidationError, pricing_rule.save) ->>>>>>> eb4af58bf0 (test: validation on mixed condition and recursion on pricing rule) test_dependencies = ["Campaign"] diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index e5b82304d11..0d08fd98139 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -107,30 +107,6 @@ class TestPromotionalScheme(unittest.TestCase): price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) -<<<<<<< HEAD -======= - def test_pricing_rule_for_product_discount_slabs(self): - ps = make_promotional_scheme() - ps.set("price_discount_slabs", []) - ps.set( - "product_discount_slabs", - [ - { - "rule_description": "12+1", - "min_qty": 12, - "free_item": "_Test Item 2", - "free_qty": 1, - "is_recursive": 1, - "recurse_for": 12, - } - ], - ) - ps.save() - pr = frappe.get_doc("Pricing Rule", {"promotional_scheme_id": ps.product_discount_slabs[0].name}) - self.assertSequenceEqual( - [pr.min_qty, pr.free_item, pr.free_qty, pr.recurse_for], [12, "_Test Item 2", 1, 12] - ) - def test_validation_on_recurse_with_mixed_condition(self): ps = make_promotional_scheme() ps.set("price_discount_slabs", []) @@ -150,7 +126,6 @@ class TestPromotionalScheme(unittest.TestCase): ps.mixed_conditions = True self.assertRaises(frappe.ValidationError, ps.save) ->>>>>>> 9bd4e7b709 (test: validation on mixed condition with recursion) def make_promotional_scheme(**args): args = frappe._dict(args) From 4d6a71ab4ba0f008e1a6816ed99e890fae347016 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Thu, 4 Jul 2024 01:45:01 +0530 Subject: [PATCH 09/20] fix: fetch expence account from asset category --- erpnext/assets/doctype/asset/test_asset.py | 4 +-- erpnext/controllers/accounts_controller.py | 3 +++ erpnext/stock/doctype/item/test_item.py | 27 +++++++++++++++++++ .../purchase_receipt/purchase_receipt.py | 10 +------ erpnext/stock/get_item_details.py | 24 +++++++++++++---- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index dc58222008a..7007f212827 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1689,12 +1689,12 @@ def create_asset(**args): return asset -def create_asset_category(): +def create_asset_category(enable_cwip=1): asset_category = frappe.new_doc("Asset Category") asset_category.asset_category_name = "Computers" asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 - asset_category.enable_cwip_accounting = 1 + asset_category.enable_cwip_accounting = enable_cwip asset_category.append( "accounts", { diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 86e4cf684a3..792a0c02caf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -742,6 +742,9 @@ class AccountsController(TransactionBase): # reset pricing rule fields if pricing_rule_removed item.set(fieldname, value) + elif fieldname == "expense_account" and not item.get("expense_account"): + item.expense_account = value + if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field( "is_fixed_asset" ): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 827261d6540..98fac9fe2e8 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -156,6 +156,33 @@ class TestItem(FrappeTestCase): for key, value in to_check.items(): self.assertEqual(value, details.get(key), key) + def test_get_asset_item_details(self): + from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item + + create_asset_category(0) + create_fixed_asset_item() + + details = get_item_details( + { + "item_code": "Macbook Pro", + "company": "_Test Company", + "currency": "INR", + "doctype": "Purchase Receipt", + } + ) + self.assertEqual(details.get("expense_account"), "_Test Fixed Asset - _TC") + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", "1") + details = get_item_details( + { + "item_code": "Macbook Pro", + "company": "_Test Company", + "currency": "INR", + "doctype": "Purchase Receipt", + } + ) + self.assertEqual(details.get("expense_account"), "CWIP Account - _TC") + def test_item_tax_template(self): expected_item_tax_template = [ { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7b906764946..9afc9bbe9f3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -558,15 +558,7 @@ class PurchaseReceipt(BuyingController): landed_cost_entries = get_item_account_wise_additional_cost(self.name) if d.is_fixed_asset: - account_type = ( - "capital_work_in_progress_account" - if is_cwip_accounting_enabled(d.asset_category) - else "fixed_asset_account" - ) - - stock_asset_account_name = get_asset_account( - account_type, asset_category=d.asset_category, company=self.company - ) + stock_asset_account_name = d.expense_account stock_value_diff = ( flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 64715d28ce8..364a681cff4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -327,12 +327,26 @@ def get_basic_details(args, item, overwrite_warehouse=True): expense_account = None - if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset: - from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account + if item.is_fixed_asset: + from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled - expense_account = get_asset_category_account( - fieldname="fixed_asset_account", item=args.item_code, company=args.company - ) + if is_cwip_accounting_enabled(item.asset_category): + expense_account = get_asset_account( + "capital_work_in_progress_account", + asset_category=item.asset_category, + company=args.company, + ) + elif args.get("doctype") in ( + "Purchase Invoice", + "Purchase Receipt", + "Purchase Order", + "Material Request", + ): + from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account + + expense_account = get_asset_category_account( + fieldname="fixed_asset_account", item=args.item_code, company=args.company + ) # Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master if not args.get("uom"): From 62ad466a3bdc2bccaabaf3c671cf8ad32f249e53 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:49:40 +0530 Subject: [PATCH 10/20] fix: group by in item-wise purchase register (cherry picked from commit 3fab00135b1391c5f505fee599dac7234b0e1992) --- .../item_wise_purchase_register/item_wise_purchase_register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 06afb4c0e8f..c5d732ed697 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -306,7 +306,7 @@ def apply_conditions(query, pi, pii, filters): query = query.orderby(pi.posting_date, order=Order.desc) query = query.orderby(pii.item_group, order=Order.desc) else: - query = apply_group_by_conditions(filters, "Purchase Invoice") + query = apply_group_by_conditions(query, pi, pii, filters) return query From 13895fa060653fae3b18c394f2ea7e3bd9d66ade Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 4 Jul 2024 15:01:35 +0530 Subject: [PATCH 11/20] fix: empty item-wise sales/purchase register reports on initial load (cherry picked from commit ee862126e4ba2a38a6eaa4eba8267360f9b04a04) --- .../item_wise_purchase_register/item_wise_purchase_register.js | 2 +- .../report/item_wise_sales_register/item_wise_sales_register.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js index 19ce9ffc607..0daf5c581cc 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js @@ -46,7 +46,7 @@ frappe.query_reports["Item-wise Purchase Register"] = { label: __("Group By"), fieldname: "group_by", fieldtype: "Select", - options: ["Supplier", "Item Group", "Item", "Invoice"], + options: ["", "Supplier", "Item Group", "Item", "Invoice"], }, ], formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js index 1f155de63a0..d019b718ffb 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js @@ -64,7 +64,7 @@ frappe.query_reports["Item-wise Sales Register"] = { label: __("Group By"), fieldname: "group_by", fieldtype: "Select", - options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"], + options: ["", "Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"], }, ], formatter: function (value, row, column, data, default_formatter) { From e2f8e02c735c0b169273620bc5a18ee39657ca8a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:06:23 +0530 Subject: [PATCH 12/20] fix: stock qty validation in SCR (backport #42124) (#42224) * fix: stock qty validation in SCR (#42124) (cherry picked from commit 99f2735ad3440aa3690bb514b32d7d9610753041) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- .../subcontracting_receipt.py | 6 ++++++ .../test_subcontracting_receipt.py | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index aeae7c84679..595620b745b 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -240,6 +240,12 @@ class SubcontractingReceipt(SubcontractingController): ) def validate_available_qty_for_consumption(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + return + for item in self.get("supplied_items"): precision = item.precision("consumed_qty") if ( diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 1b1d823a920..1cbab424803 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -75,6 +75,7 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) def test_available_qty_for_consumption(self): + set_backflush_based_on("BOM") make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) make_stock_entry( item_code="_Test Item Home Desktop 100", @@ -119,7 +120,7 @@ class TestSubcontractingReceipt(FrappeTestCase): ) scr = make_subcontracting_receipt(sco.name) scr.save() - self.assertRaises(frappe.ValidationError, scr.submit) + scr.submit() def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -556,6 +557,21 @@ class TestSubcontractingReceipt(FrappeTestCase): # consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6 self.assertEqual(scr.supplied_items[0].consumed_qty, 6) + # Do not transfer materials to the supplier warehouse and check whether system allows to consumed directly from the supplier's warehouse + sco = get_subcontracting_order(service_items=service_items) + + # Transfer RM's + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items, warehouse="_Test Warehouse 1 - _TC") + + # Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.submit() + self.assertEqual(scr.docstatus, 1) + + for item in scr.supplied_items: + self.assertFalse(item.available_qty_for_consumption) + def test_supplied_items_cost_after_reposting(self): # Set Backflush Based On as "BOM" set_backflush_based_on("BOM") From d5c1c62622c3f412a5e1b74f1a21ad7fd6b7148e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:34:39 +0200 Subject: [PATCH 13/20] fix: add missing german translations (cherry picked from commit 2f89461ace6af52e17134cd2cb5ab535e3851985) --- erpnext/translations/de.csv | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 2f714326ad9..0905c06896c 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1248,6 +1248,8 @@ Is Default,Ist Standard, Is Existing Asset,Vermögenswert existiert bereits., Is Frozen,Ist gesperrt, Is Group,Ist Gruppe, +Is Group Warehouse,Ist Lagergruppe, +Is Rejected Warehouse,Ist Lager für abgelehnte Ware, Issue,Anfrage, Issue Material,Material ausgeben, Issued,Ausgestellt, @@ -1311,6 +1313,7 @@ Items for Raw Material Request,Artikel für Rohstoffanforderung, Job Card,Jobkarte, Job card {0} created,Jobkarte {0} erstellt, Join,Beitreten, +Joining,Beitritt, Journal Entries {0} are un-linked,Buchungssätze {0} sind nicht verknüpft, Journal Entry,Buchungssatz, Journal Entry {0} does not have account {1} or already matched against other voucher,Buchungssatz {0} gehört nicht zu Konto {1} oder ist bereits mit einem anderen Beleg abgeglichen, @@ -2033,6 +2036,7 @@ Product Search,Produkt Suche, Production,Produktion, Production Item,Produktions-Artikel, Products,Produkte, +Profile,Profil, Profit and Loss,Gewinn und Verlust, Profit for the year,Jahresüberschuss, Program,Programm, @@ -2094,6 +2098,9 @@ Qty To Manufacture,Herzustellende Menge, Qty Total,Gesamtmenge, Qty for {0},Menge für {0}, Qualification,Qualifikation, +Qualification Status,Qualifikationsstatus, +Qualified By,Qualifiziert von, +Qualified on,Qualifiziert am, Quality,Qualität, Quality Action,Qualitätsmaßnahme, Quality Goal.,Qualitätsziel., @@ -3295,6 +3302,7 @@ Warehouse Type,Lagertyp, 'Date' is required,'Datum' ist erforderlich, Budgets,Budgets, Bundle Qty,Bundle Menge, +Company Details,Unternehmensdetails, Company GSTIN,Unternehmen GSTIN, Company field is required,Firmenfeld ist erforderlich, Creating Dimensions...,Dimensionen erstellen ..., @@ -3659,6 +3667,7 @@ Performance,Performance, Period based On,Zeitraum basierend auf, Perpetual inventory required for the company {0} to view this report.,"Permanente Bestandsaufnahme erforderlich, damit das Unternehmen {0} diesen Bericht anzeigen kann.", Phone,Telefon, +Phone Ext.,Telefon Ext., Pick List,Auswahlliste, Plaid authentication error,Plaid-Authentifizierungsfehler, Plaid public token error,Plaid public token error, @@ -5096,7 +5105,7 @@ Number of Depreciations Booked,Anzahl der gebuchten Abschreibungen, Finance Books,Finanzbücher, Straight Line,Gerade Linie, Double Declining Balance,Doppelte degressive, -Manual,Handbuch, +Manual,Manuell, Value After Depreciation,Wert nach Abschreibung, Total Number of Depreciations,Gesamtzahl der Abschreibungen, Frequency of Depreciation (Months),Die Häufigkeit der Abschreibungen (Monate), @@ -5156,6 +5165,7 @@ Maintenance Team Name,Name des Wartungsteams, Maintenance Team Members,Mitglieder des Wartungsteams, Purpose,Zweck, Stock Manager,Lagerleiter, +Stock Movement,Lagerbewegung, Asset Movement Item,Vermögensbewegungsgegenstand, Source Location,Quellspeicherort, From Employee,Von Mitarbeiter, @@ -5252,6 +5262,7 @@ Default Bank Account,Standardbankkonto, Is Transporter,Ist Transporter, Represents Company,Repräsentiert das Unternehmen, Supplier Type,Lieferantentyp, +Allow Purchase,Einkauf zulassen, Allow Purchase Invoice Creation Without Purchase Order,Erstellen von Eingangsrechnung ohne Bestellung zulassen, Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Kaufbeleg ohne Kaufbeleg zulassen, Warn RFQs,Warnung Ausschreibungen, @@ -6028,6 +6039,7 @@ Occupational Hazards and Environmental Factors,Berufsrisiken und Umweltfaktoren, Other Risk Factors,Andere Risikofaktoren, Patient Details,Patientendetails, Additional information regarding the patient,Zusätzliche Informationen zum Patienten, +Additional Info,Zusätzliche Informationen, HLC-APP-.YYYY.-,HLC-APP-.YYYY.-, Patient Age,Patient Alter, Get Prescribed Clinical Procedures,Holen Sie sich vorgeschriebene klinische Verfahren, @@ -6194,6 +6206,7 @@ Date Of Retirement,Zeitpunkt der Pensionierung, Department and Grade,Abteilung und Klasse, Reports to,Vorgesetzter, Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, +Attendance & Leaves,Anwesenheit & Urlaub, Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID), Applicable Holiday List,Geltende Feiertagsliste, Default Shift,Standardverschiebung, @@ -6746,7 +6759,7 @@ Default Costing Rate,Standardkosten, Default Billing Rate,Standard-Rechnungspreis, Dependent Task,Abhängiger Vorgang, Project Type,Projekttyp, -% Complete Method,% abgeschlossene Methode, +% Complete Method,Fertigstellung bemessen nach, Task Completion,Aufgabenerledigung, Task Progress,Vorgangsentwicklung, % Completed,% abgeschlossen, @@ -6909,6 +6922,7 @@ Restaurant Reservation,Restaurant Reservierung, Waitlisted,Auf der Warteliste, No Show,Nicht angetreten, No of People,Anzahl von Personen, +No of Employees,Anzahl der Mitarbeiter, Reservation Time,Reservierungszeit, Reservation End Time,Reservierungsendzeit, No of Seats,Anzahl der Sitze, @@ -6920,6 +6934,7 @@ Default Company Bank Account,Standard-Bankkonto des Unternehmens, From Lead,Aus Lead, Account Manager,Kundenberater, Accounts Manager,Buchhalter, +Allow Sales,Verkauf zulassen, Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Auftrag, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein, Default Price List,Standardpreisliste, @@ -6979,7 +6994,8 @@ Not Delivered,Nicht geliefert, Fully Delivered,Komplett geliefert, Partly Delivered,Teilweise geliefert, Not Applicable,Nicht andwendbar, -% Delivered,% geliefert, +% Delivered,% Geliefert, +% Picked,% Kommissioniert, % of materials delivered against this Sales Order,% der für diesen Auftrag gelieferten Materialien, % of materials billed against this Sales Order,% der Materialien welche zu diesem Auftrag gebucht wurden, Not Billed,Nicht abgerechnet, @@ -7113,6 +7129,7 @@ New Income,Neuer Verdienst, New Expenses,Neue Ausgaben, Annual Income,Jährliches Einkommen, Annual Expenses,Jährliche Kosten, +Annual Revenue,Jährlicher Umsatz, Bank Balance,Kontostand, Bank Credit Balance,Bankguthaben, Receivables,Forderungen, @@ -7595,6 +7612,7 @@ Actual Qty After Transaction,Tatsächliche Anzahl nach Transaktionen, Stock Value Difference,Lagerwert-Differenz, Stock Queue (FIFO),Lagerverfahren (FIFO), Is Cancelled,Ist storniert, +Is Cash or Non Trade Discount,Ist Bar- oder Nicht-Handelsrabatt, Stock Reconciliation,Bestandsabgleich, This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Dieses Werkzeug hilft Ihnen dabei, die Menge und die Bewertung von Bestand im System zu aktualisieren oder zu ändern. Es wird in der Regel verwendet, um die Systemwerte und den aktuellen Bestand Ihrer Lager zu synchronisieren.", Reconciliation JSON,Abgleich JSON (JavaScript Object Notation), @@ -7696,6 +7714,7 @@ Warranty / AMC Status,Status der Garantie / des jährlichen Wartungsvertrags, Resolved By,Entschieden von, Service Address,Serviceadresse, If different than customer address,Falls abweichend von Kundenadresse, +"If yes, then this warehouse will be used to store rejected materials","Falls aktiviert, wird dieses Lager verwendet, um abgelehnte Ware zu lagern", Raised By,Gemeldet durch, From Company,Von Unternehmen, Rename Tool,Werkzeug zum Umbenennen, @@ -8940,6 +8959,7 @@ Print Receipt,Druckeingang, Edit Receipt,Beleg bearbeiten, Focus on search input,Konzentrieren Sie sich auf die Sucheingabe, Focus on Item Group filter,Fokus auf Artikelgruppenfilter, +Footer will display correctly only in PDF,Die Fußzeile wird nur im PDF korrekt angezeigt, Checkout Order / Submit Order / New Order,Kaufabwicklung / Bestellung abschicken / Neue Bestellung, Add Order Discount,Bestellrabatt hinzufügen, Item Code: {0} is not available under warehouse {1}.,Artikelcode: {0} ist unter Lager {1} nicht verfügbar., From fcf65001447b4f10459e69ca6f5d78b8a07ef34d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:44:08 +0530 Subject: [PATCH 14/20] fix(Holiday List): sort holidays on save to avoid disorienting the user (backport #42236) (#42251) * fix(Holiday List): sort holidays on save to avoid disorienting the user (#42236) fix: sort holidays on save to avoid disorienting the user (cherry picked from commit ad137250fc8c9421fd085afd83ed439dcbbd0568) # Conflicts: # erpnext/setup/doctype/holiday_list/holiday_list.py * chore: fix conflicts --------- Co-authored-by: Rucha Mahabal --- erpnext/setup/doctype/holiday_list/holiday_list.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 94ec0b95675..4b51011c87d 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -19,6 +19,7 @@ class HolidayList(Document): def validate(self): self.validate_days() self.total_holidays = len(self.holidays) + self.sort_holidays() @frappe.whitelist() def get_weekly_off_dates(self): @@ -33,8 +34,6 @@ class HolidayList(Document): self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1}) - self.sort_holidays() - @frappe.whitelist() def get_supported_countries(self): from holidays.utils import list_supported_countries @@ -76,8 +75,6 @@ class HolidayList(Document): "holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0} ) - self.sort_holidays() - def sort_holidays(self): self.holidays.sort(key=lambda x: getdate(x.holiday_date)) for i in range(len(self.holidays)): From 49e50662b6d5c315fb24366eaeb7505cd4b35947 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 2 Jul 2024 19:07:31 +0530 Subject: [PATCH 15/20] fix: updated logic for calculating tax_withholding_net_total in payment entry (cherry picked from commit c8a34cde7f70cc22fde9743314439f0091077e6b) --- .../doctype/payment_entry/payment_entry.py | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index fa5aabe1268..9384ee180d2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -74,7 +74,6 @@ class PaymentEntry(AccountsController): self.set_exchange_rate() self.validate_mandatory() self.validate_reference_documents() - self.set_tax_withholding() self.set_amounts() self.validate_amounts() self.apply_taxes() @@ -89,6 +88,7 @@ class PaymentEntry(AccountsController): self.validate_allocated_amount() self.validate_paid_invoices() self.ensure_supplier_is_not_blocked() + self.set_tax_withholding() self.set_status() def on_submit(self): @@ -674,9 +674,7 @@ class PaymentEntry(AccountsController): if not self.apply_tax_withholding_amount: return - order_amount = self.get_order_net_total() - - net_total = flt(order_amount) + flt(self.unallocated_amount) + net_total = self.calculate_tax_withholding_net_total() # Adding args as purchase invoice to get TDS amount args = frappe._dict( @@ -720,7 +718,26 @@ class PaymentEntry(AccountsController): for d in to_remove: self.remove(d) - def get_order_net_total(self): + def calculate_tax_withholding_net_total(self): + net_total = 0 + order_details = self.get_order_wise_tax_withholding_net_total() + + for d in self.references: + tax_withholding_net_total = order_details.get(d.reference_name) + if not tax_withholding_net_total: + continue + + net_taxable_outstanding = max( + 0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total) + ) + + net_total += min(net_taxable_outstanding, d.allocated_amount) + + net_total += self.unallocated_amount + + return net_total + + def get_order_wise_tax_withholding_net_total(self): if self.party_type == "Supplier": doctype = "Purchase Order" else: @@ -728,12 +745,15 @@ class PaymentEntry(AccountsController): docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype] - tax_withholding_net_total = frappe.db.get_value( - doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"] + return frappe._dict( + frappe.db.get_all( + doctype, + filters={"name": ["in", docnames]}, + fields=["name", "base_tax_withholding_net_total"], + as_list=True, + ) ) - return tax_withholding_net_total - def apply_taxes(self): self.initialize_taxes() self.determine_exclusive_rate() From 51cbbee4cad4e131c88c0ab70efcbc7b0294977f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 10 Jul 2024 11:55:35 +0530 Subject: [PATCH 16/20] fix(tds): use doctype reference when mapping keys across multiple doctypes --- .../tds_payable_monthly.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 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 fc5e8452e1d..465c300a8ef 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -12,7 +12,7 @@ def execute(filters=None): else: party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name") - filters.update({"naming_series": party_naming_by}) + filters["naming_series"] = party_naming_by validate_filters(filters) ( @@ -63,21 +63,23 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ tax_withholding_category = tds_accounts.get(entry.account) # or else the consolidated value from the voucher document if not tax_withholding_category: - tax_withholding_category = tax_category_map.get(name) + tax_withholding_category = tax_category_map.get((voucher_type, name)) # or else from the party default if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") rate = tax_rate_map.get(tax_withholding_category) - if net_total_map.get(name): + if net_total_map.get((voucher_type, name)): if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount if rate: total_amount = grand_total = base_total = tax_amount / (rate / 100) elif voucher_type == "Purchase Invoice": - total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name) + total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get( + (voucher_type, name) + ) else: - total_amount, grand_total, base_total = net_total_map.get(name) + total_amount, grand_total, base_total = net_total_map.get((voucher_type, name)) else: total_amount += entry.credit @@ -97,7 +99,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ } if filters.naming_series == "Naming Series": - row.update({"party_name": party_map.get(party, {}).get(party_name)}) + row["party_name"] = party_map.get(party, {}).get(party_name) row.update( { @@ -279,7 +281,6 @@ def get_tds_docs(filters): journal_entries = [] tax_category_map = frappe._dict() net_total_map = frappe._dict() - frappe._dict() journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") @@ -412,7 +413,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): ) for entry in entries: - tax_category_map.update({entry.name: entry.tax_withholding_category}) + tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category if doctype == "Purchase Invoice": value = [ entry.base_tax_withholding_net_total, @@ -427,7 +428,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount] else: value = [entry.total_amount] * 3 - net_total_map.update({entry.name: value}) + + net_total_map[(doctype, entry.name)] = value def get_tax_rate_map(filters): From 4195c50f02bbc5bf5e541fc140be47fc6ad17a29 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 1 Jul 2024 16:07:38 +0530 Subject: [PATCH 17/20] fix: removed max discount validation for sales return (cherry picked from commit db807d433be8646de8507a91850f24befa2e3c86) --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5c38d7bef3a..d794245192d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -31,7 +31,7 @@ class SellingController(StockController): def validate(self): super().validate() self.validate_items() - if not self.get("is_debit_note"): + if not (self.get("is_debit_note") or self.get("is_return")): self.validate_max_discount() self.validate_selling_price() self.set_qty_as_per_stock_uom() From 106c154a16efce956357524309215cd62cc3c3ec Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 9 Jul 2024 13:18:09 +0530 Subject: [PATCH 18/20] fix: tax on stock_rbnb on repost of Purchase Receipt (cherry picked from commit 8633080dffcbb8e12c5c93946fe9e36837827f75) --- .../purchase_receipt/purchase_receipt.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 9afc9bbe9f3..1909874601d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -662,7 +662,6 @@ class PurchaseReceipt(BuyingController): def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False): negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) - is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): @@ -687,26 +686,10 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked - stock_rbnb = ( - self.get("asset_received_but_not_billed") - if is_asset_pr - else self.get_company_default("stock_received_but_not_billed") - ) i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): - if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item(): - account = tax.account_head - else: - negative_expense_booked_in_pi = frappe.db.sql( - """select name from `tabPurchase Invoice Item` pi - where docstatus = 1 and purchase_receipt=%s - and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", - (self.name, tax.account_head), - ) - account = stock_rbnb if negative_expense_booked_in_pi else tax.account_head - + account = tax.account_head if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: From fdf1dfe46ee1bc296989981cf17162e647394cf0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 9 Jul 2024 13:30:47 +0530 Subject: [PATCH 19/20] test: tax account heads on PR report without LCV (cherry picked from commit 9562628ed67c958cf2a9d41257b89dfd5c5e5fd7) # Conflicts: # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py --- .../purchase_receipt/test_purchase_receipt.py | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index bb218f4fe7b..b8cd03bf0ee 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2499,6 +2499,172 @@ class TestPurchaseReceipt(FrappeTestCase): lcv.save().submit() return lcv +<<<<<<< HEAD +======= + def test_tax_account_heads_on_item_repost_without_lcv(self): + """ + PO -> PR -> PI + Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb if Purchase Receipt was created first + This scenario is without LCV + """ + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_purchase_order, + make_pr_against_po, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice + + stock_rbnb = "Stock Received But Not Billed - _TC" + stock_in_hand = "Stock In Hand - _TC" + test_cc = "_Test Cost Center - _TC" + test_company = "_Test Company" + creditors = "Creditors - _TC" + + company_doc = frappe.get_doc("Company", test_company) + company_doc.enable_perpetual_inventory = True + company_doc.stock_received_but_not_billed = stock_rbnb + company_doc.default_inventory_account = stock_in_hand + company_doc.save() + + packaging_charges_account = create_account( + account_name="Packaging Charges", + parent_account="Indirect Expenses - _TC", + company=test_company, + account_type="Tax", + ) + + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + po.taxes = [] + po.append( + "taxes", + { + "category": "Valuation and Total", + "account_head": packaging_charges_account, + "cost_center": test_cc, + "description": "Test", + "add_deduct_tax": "Add", + "charge_type": "Actual", + "tax_amount": 250, + }, + ) + po.save().submit() + + pr = make_pr_against_po(po.name, received_qty=10) + pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + ] + self.assertEqual(expected_pr_gles, pr_gl_entries) + + # Make PI against Purchase Receipt + pi = make_purchase_invoice(pr.name).save().submit() + pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True) + expected_pi_gles = [ + {"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None}, + ] + self.assertEqual(expected_pi_gles, pi_gl_entries) + + # Trigger Repost Item Valudation on a older date + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Item and Warehouse", + "item_code": pr.items[0].item_code, + "warehouse": pr.items[0].warehouse, + "posting_date": add_days(pr.posting_date, -1), + "posting_time": "00:00:00", + "company": pr.company, + "allow_negative_stock": 1, + "via_landed_cost_voucher": 0, + "allow_zero_rate": 0, + } + ) + repost_doc.save().submit() + + pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles_after_repost = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + ] + self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost)) + self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost) + + # teardown + pi.reload() + pi.cancel() + pr.reload() + pr.cancel() + + company_doc.enable_perpetual_inventory = False + company_doc.stock_received_but_not_billed = None + company_doc.default_inventory_account = None + company_doc.save() + + def test_do_not_use_batchwise_valuation_rate(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = "Test Item for Do Not Use Batchwise Valuation" + make_item( + item_code, + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TIDNBV-.#####", + "valuation_method": "Moving Average", + }, + ) + + # 1st pr for 100 rate + pr = make_purchase_receipt( + item_code=item_code, + qty=1, + rate=100, + posting_date=add_days(today(), -2), + ) + + make_purchase_receipt( + item_code=item_code, + qty=1, + rate=200, + posting_date=add_days(today(), -1), + ) + + dn = create_delivery_note( + item_code=item_code, + qty=1, + rate=300, + posting_date=today(), + use_serial_batch_fields=1, + batch_no=get_batch_from_bundle(pr.items[0].serial_and_batch_bundle), + ) + dn.reload() + bundle = dn.items[0].serial_and_batch_bundle + + valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + self.assertEqual(valuation_rate, 100) + + doc = frappe.get_doc("Stock Settings") + doc.do_not_use_batchwise_valuation = 1 + doc.flags.ignore_validate = True + doc.save() + + pr.repost_future_sle_and_gle(force=True) + + valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + self.assertEqual(valuation_rate, 150) + + doc = frappe.get_doc("Stock Settings") + doc.do_not_use_batchwise_valuation = 0 + doc.flags.ignore_validate = True + doc.save() + +>>>>>>> 9562628ed6 (test: tax account heads on PR report without LCV) def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 115a0123ed624d78f3354b40600b048a5c8896b3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 10 Jul 2024 15:30:24 +0530 Subject: [PATCH 20/20] chore: resolve conflict --- .../purchase_receipt/test_purchase_receipt.py | 62 ------------------- 1 file changed, 62 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b8cd03bf0ee..c0cf79c63da 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2499,8 +2499,6 @@ class TestPurchaseReceipt(FrappeTestCase): lcv.save().submit() return lcv -<<<<<<< HEAD -======= def test_tax_account_heads_on_item_repost_without_lcv(self): """ PO -> PR -> PI @@ -2605,66 +2603,6 @@ class TestPurchaseReceipt(FrappeTestCase): company_doc.default_inventory_account = None company_doc.save() - def test_do_not_use_batchwise_valuation_rate(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - - item_code = "Test Item for Do Not Use Batchwise Valuation" - make_item( - item_code, - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "TIDNBV-.#####", - "valuation_method": "Moving Average", - }, - ) - - # 1st pr for 100 rate - pr = make_purchase_receipt( - item_code=item_code, - qty=1, - rate=100, - posting_date=add_days(today(), -2), - ) - - make_purchase_receipt( - item_code=item_code, - qty=1, - rate=200, - posting_date=add_days(today(), -1), - ) - - dn = create_delivery_note( - item_code=item_code, - qty=1, - rate=300, - posting_date=today(), - use_serial_batch_fields=1, - batch_no=get_batch_from_bundle(pr.items[0].serial_and_batch_bundle), - ) - dn.reload() - bundle = dn.items[0].serial_and_batch_bundle - - valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") - self.assertEqual(valuation_rate, 100) - - doc = frappe.get_doc("Stock Settings") - doc.do_not_use_batchwise_valuation = 1 - doc.flags.ignore_validate = True - doc.save() - - pr.repost_future_sle_and_gle(force=True) - - valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") - self.assertEqual(valuation_rate, 150) - - doc = frappe.get_doc("Stock Settings") - doc.do_not_use_batchwise_valuation = 0 - doc.flags.ignore_validate = True - doc.save() - ->>>>>>> 9562628ed6 (test: tax account heads on PR report without LCV) def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier