From 77e92b38ebf5efc57710fde8eaa3187962c3ca2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ei=C3=9Fler?= <77415730+PatrickDEissler@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:17:32 +0100 Subject: [PATCH 01/16] fix(Project): re-phrase welcome email (#45175) (cherry picked from commit 8d661428651fa1ce47f5f3b217cfadad47e060a6) # Conflicts: # erpnext/projects/doctype/project/project.py --- erpnext/projects/doctype/project/project.py | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 29a288ef671..c88dc3780d0 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -7,8 +7,15 @@ from email_reply_parser import EmailReplyParser from frappe import _, qb from frappe.desk.reportview import get_match_cond from frappe.model.document import Document +<<<<<<< HEAD from frappe.query_builder.functions import Sum from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today +======= +from frappe.query_builder import Interval +from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp +from frappe.utils import add_days, flt, get_datetime, get_link_to_form, get_time, get_url, nowtime, today +from frappe.utils.user import is_website_user +>>>>>>> 8d66142865 (fix(Project): re-phrase welcome email (#45175)) from erpnext import get_default_company from erpnext.controllers.queries import get_filters_cond @@ -275,24 +282,19 @@ class Project(Document): frappe.db.set_value("Project", new_name, "copied_from", new_name) def send_welcome_email(self): - url = get_url(f"/project/?name={self.name}") - messages = ( - _("You have been invited to collaborate on the project: {0}").format(self.name), - url, - _("Join"), - ) + label = f"{self.project_name} ({self.name})" + url = get_link_to_form(self.doctype, self.name, label) - content = """ -

{0}.

-

{2}

- """ + content = "

{}

".format( + _("You have been invited to collaborate on the project {0}.").format(url) + ) for user in self.users: if user.welcome_email_sent == 0: frappe.sendmail( user.user, subject=_("Project Collaboration Invitation"), - content=content.format(*messages), + content=content, ) user.welcome_email_sent = 1 From 667e659e3f660bb0a94ea198c9e61671e58bce08 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:50:15 +0530 Subject: [PATCH 02/16] fix: round off tax withholding amount (#45271) (cherry picked from commit ada272a29bf5b0c1ab2115a3d72b3d8a80bec11a) --- .../tax_withholding_category/tax_withholding_category.py | 3 +++ erpnext/controllers/taxes_and_totals.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index dc8a34647e2..2115d44322d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -124,6 +124,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): cost_center = get_cost_center(inv) tax_row.update({"cost_center": cost_center}) + if cint(tax_details.round_off_tax_amount): + inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head + if inv.doctype == "Purchase Invoice": return tax_row, tax_deducted_on_advances, voucher_wise_amount else: diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5b6a7b1506b..f98fdac3a6a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -27,6 +27,11 @@ class calculate_taxes_and_totals: self.doc = doc frappe.flags.round_off_applicable_accounts = [] + if doc.get("round_off_applicable_accounts_for_tax_withholding"): + frappe.flags.round_off_applicable_accounts.append( + doc.round_off_applicable_accounts_for_tax_withholding + ) + self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) From 524a8d77f75dade436043fbf344631262ca25eb4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:02:34 +0530 Subject: [PATCH 03/16] fix: pos search by term items price (backport #45006) (#45102) * fix: load price list rate for pos search term (cherry picked from commit 4b6cae156e2ece905158eaa649a9812d92bc87dd) # Conflicts: # erpnext/selling/page/point_of_sale/point_of_sale.py * fix: load search term price with customer default price list (cherry picked from commit 2beb485d7753e8b0cf399cf9025ea349a5857a3e) * chore: resolve conflict --------- Co-authored-by: diptanilsaha Co-authored-by: ruthra kumar --- .../selling/page/point_of_sale/point_of_sale.py | 15 ++++++++++----- .../page/point_of_sale/pos_item_selector.js | 9 ++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) 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 bc94d3b706e..7db9faacca2 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -51,13 +51,18 @@ def search_by_term(search_term, warehouse, price_list): item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) item.update({"actual_qty": item_stock_qty}) + price_filters = { + "price_list": price_list, + "item_code": item_code, + } + + if batch_no: + price_filters["batch_no"] = batch_no + price = frappe.get_list( doctype="Item Price", - filters={ - "price_list": price_list, - "item_code": item_code, - }, - fields=["uom", "currency", "price_list_rate"], + filters=price_filters, + fields=["uom", "currency", "price_list_rate", "batch_no"], ) def __sort(p): 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 dba36bd2429..7021777d158 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -325,13 +325,16 @@ erpnext.PointOfSale.ItemSelector = class { } filter_items({ search_term = "" } = {}) { + const selling_price_list = this.events.get_frm().doc.selling_price_list; + if (search_term) { search_term = search_term.toLowerCase(); // memoize this.search_index = this.search_index || {}; - if (this.search_index[search_term]) { - const items = this.search_index[search_term]; + this.search_index[selling_price_list] = this.search_index[selling_price_list] || {}; + if (this.search_index[selling_price_list][search_term]) { + const items = this.search_index[selling_price_list][search_term]; this.items = items; this.render_item_list(items); this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); @@ -343,7 +346,7 @@ erpnext.PointOfSale.ItemSelector = class { // eslint-disable-next-line no-unused-vars const { items, serial_no, batch_no, barcode } = message; if (search_term && !barcode) { - this.search_index[search_term] = items; + this.search_index[selling_price_list][search_term] = items; } this.items = items; this.render_item_list(items); From 1ccf30d97b2c8b92346bb3d73d4adbad6ab4b621 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:38:03 +0100 Subject: [PATCH 04/16] chore: resolve confilcts --- erpnext/projects/doctype/project/project.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index c88dc3780d0..6ea98c1baf5 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -7,15 +7,8 @@ from email_reply_parser import EmailReplyParser from frappe import _, qb from frappe.desk.reportview import get_match_cond from frappe.model.document import Document -<<<<<<< HEAD from frappe.query_builder.functions import Sum -from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today -======= -from frappe.query_builder import Interval -from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp -from frappe.utils import add_days, flt, get_datetime, get_link_to_form, get_time, get_url, nowtime, today -from frappe.utils.user import is_website_user ->>>>>>> 8d66142865 (fix(Project): re-phrase welcome email (#45175)) +from frappe.utils import add_days, flt, get_datetime, get_link_to_form, get_time, nowtime, today from erpnext import get_default_company from erpnext.controllers.queries import get_filters_cond From 20bb15167dde06950da04679cf38ac8b58b4ed1f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:34:44 +0100 Subject: [PATCH 05/16] revert: avoid change to translatable string --- erpnext/projects/doctype/project/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 6ea98c1baf5..13fad07cb4a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -279,7 +279,7 @@ class Project(Document): url = get_link_to_form(self.doctype, self.name, label) content = "

{}

".format( - _("You have been invited to collaborate on the project {0}.").format(url) + _("You have been invited to collaborate on the project: {0}").format(url) ) for user in self.users: From fe5de302565fa00f75838af535e08d2245e1b371 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Dec 2024 20:53:56 +0530 Subject: [PATCH 06/16] fix: do not reset picked items (cherry picked from commit 34a80bfcd30b4f03be5886a286b2c8056a9931e1) # Conflicts: # erpnext/stock/doctype/pick_list/test_pick_list.py --- erpnext/stock/doctype/pick_list/pick_list.py | 39 ++- .../stock/doctype/pick_list/test_pick_list.py | 262 ++++++++++++++++++ 2 files changed, 291 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 2176d75ee97..a2a07e6d11e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -26,9 +26,8 @@ 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() + self.validate_stock_qty() + self.check_serial_no_status() def before_save(self): self.update_status() @@ -42,14 +41,24 @@ class PickList(Document): from erpnext.stock.doctype.batch.batch import get_batch_qty for row in self.get("locations"): - if row.batch_no and not row.qty: + if not row.picked_qty: + continue + + if row.batch_no and row.picked_qty: batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code) - if row.qty > batch_qty: + if row.picked_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)), + "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}. Please restock the item." + ).format( + row.idx, + row.picked_qty, + row.item_code, + batch_qty, + row.batch_no, + bold(row.warehouse), + ), title=_("Insufficient Stock"), ) @@ -61,11 +70,11 @@ class PickList(Document): "actual_qty", ) - if row.qty > flt(bin_qty): + if row.picked_qty > flt(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)), + ).format(row.idx, row.picked_qty, bold(row.item_code), bin_qty, bold(row.warehouse)), title=_("Insufficient Stock"), ) @@ -253,7 +262,14 @@ class PickList(Document): locations_replica = self.get("locations") # reset - self.delete_key("locations") + reset_rows = [] + for row in self.get("locations"): + if not row.picked_qty: + reset_rows.append(row) + + for row in reset_rows: + self.remove(row) + updated_locations = frappe._dict() for item_doc in items: item_code = item_doc.item_code @@ -323,6 +339,9 @@ class PickList(Document): # aggregate qty for same item item_map = OrderedDict() for item in locations: + if item.picked_qty: + continue + if not item.item_code: frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") if not cint( diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 162fcadde7a..2fe80a3245c 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -725,8 +725,14 @@ class TestPickList(FrappeTestCase): for item in items ] +<<<<<<< HEAD def get_picked_items_details(pick_list_doc): items_data = {} +======= + so = make_sales_order(item_code=item, qty=4, rate=100) + pl = create_pick_list(so.name) + self.assertFalse(pl.locations) +>>>>>>> 34a80bfcd3 (fix: do not reset picked items) for location in pick_list_doc.locations: key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse @@ -741,6 +747,7 @@ class TestPickList(FrappeTestCase): return items_data +<<<<<<< HEAD # Step - 1: Setup - Create Items and Stock Entries items_properties = [ { @@ -748,9 +755,40 @@ class TestPickList(FrappeTestCase): }, { "valuation_rate": 200, +======= + so = make_sales_order(item_code=item, qty=5, rate=100) + + pl = create_pick_list(so.name) + pl.locations[0].qty = 5 + pl.save() + pl.submit() + self.assertTrue(pl.locations[0].serial_no) + self.assertEqual(pl.locations[0].qty, 5.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=5, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertTrue(pl.locations[0].serial_no) + self.assertEqual(pl.locations[0].qty, 5.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=4, rate=100) + pl = create_pick_list(so.name) + self.assertFalse(pl.locations) + + def test_pick_list_validation_for_batch_no(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Batch Pick List Item", + properties={ + "is_stock_item": 1, +>>>>>>> 34a80bfcd3 (fix: do not reset picked items) "has_batch_no": 1, "create_new_batch": 1, }, +<<<<<<< HEAD { "valuation_rate": 300, "has_serial_no": 1, @@ -758,6 +796,40 @@ class TestPickList(FrappeTestCase): }, { "valuation_rate": 400, +======= + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + + so = make_sales_order(item_code=item, qty=5, rate=100) + + pl = create_pick_list(so.name) + pl.locations[0].qty = 5 + pl.save() + pl.submit() + self.assertTrue(pl.locations[0].batch_no) + self.assertEqual(pl.locations[0].qty, 5.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=5, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertTrue(pl.locations[0].batch_no) + self.assertEqual(pl.locations[0].qty, 5.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=4, rate=100) + pl = create_pick_list(so.name) + self.assertFalse(pl.locations) + + def test_pick_list_validation_for_batch_no_and_serial_item(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Serialized Batch Pick List Item", + properties={ + "is_stock_item": 1, +>>>>>>> 34a80bfcd3 (fix: do not reset picked items) "has_batch_no": 1, "create_new_batch": 1, "has_serial_no": 1, @@ -768,8 +840,63 @@ class TestPickList(FrappeTestCase): items = create_items(items_properties) create_stock_entries(items) +<<<<<<< HEAD # Step - 2: Create Sales Order [1] so1 = make_sales_order(item_list=get_item_list(items, qty=6)) +======= + so = make_sales_order(item_code=item, qty=5, rate=100) + + pl = create_pick_list(so.name) + pl.locations[0].qty = 5 + pl.save() + pl.submit() + self.assertTrue(pl.locations[0].batch_no) + self.assertTrue(pl.locations[0].serial_no) + self.assertEqual(pl.locations[0].qty, 5.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=5, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertTrue(pl.locations[0].batch_no) + self.assertTrue(pl.locations[0].serial_no) + self.assertEqual(pl.locations[0].qty, 5.0) + self.assertTrue(hasattr(pl, "locations")) + + so = make_sales_order(item_code=item, qty=4, rate=100) + pl = create_pick_list(so.name) + self.assertFalse(pl.locations) + + def test_pick_list_validation_for_multiple_batches_and_sales_order(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Batch Pick List Item For Multiple Batches", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####", + "create_new_batch": 1, + }, + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=5) + make_stock_entry(item=item, to_warehouse=warehouse, qty=5) + + so = make_sales_order(item_code=item, qty=6, rate=100) + + pl1 = create_pick_list(so.name) + pl1.save() + self.assertEqual(pl1.locations[0].qty, 5.0) + self.assertEqual(pl1.locations[1].qty, 1.0) + + so = make_sales_order(item_code=item, qty=4, rate=100) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(pl.locations[0].qty, 4.0) + self.assertTrue(hasattr(pl, "locations")) +>>>>>>> 34a80bfcd3 (fix: do not reset picked items) # Step - 3: Create and Submit Pick List [1] for Sales Order [1] pl1 = create_pick_list(so1.name) @@ -842,5 +969,140 @@ class TestPickList(FrappeTestCase): for row in pl.locations: row.qty = row.qty + 10 + row.picked_qty = row.qty self.assertRaises(frappe.ValidationError, pl.save) +<<<<<<< HEAD +======= + + def test_over_allowance_picking(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Over Allowance Picking Item", + properties={ + "is_stock_item": 1, + }, + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl_doc = create_pick_list(so.name) + pl_doc.save() + self.assertEqual(pl_doc.locations[0].qty, 10) + + pl_doc.locations[0].qty = 15 + pl_doc.locations[0].stock_qty = 15 + pl_doc.save() + + self.assertEqual(pl_doc.locations[0].qty, 15) + self.assertRaises(frappe.ValidationError, pl_doc.submit) + + frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 50) + + pl_doc.reload() + pl_doc.submit() + + frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0) + + def test_ignore_pricing_rule_in_pick_list(self): + frappe.flags.print_stmt = False + warehouse = "_Test Warehouse - _TC" + item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "IPR-PICKLT-.######", + "create_new_batch": 1, + } + ).name + + make_stock_entry( + item=item, + to_warehouse=warehouse, + qty=2, + basic_rate=100, + ) + + pricing_rule = frappe.get_doc( + { + "doctype": "Pricing Rule", + "title": "Same Free Item", + "price_or_product_discount": "Product", + "selling": 1, + "apply_on": "Item Code", + "items": [ + { + "item_code": item, + } + ], + "same_item": 1, + "is_recursive": 1, + "recurse_for": 2, + "free_qty": 1, + "company": "_Test Company", + "customer": "_Test Customer", + } + ) + + pricing_rule.save() + frappe.flags.print_stmt = True + + so = make_sales_order(item_code=item, qty=2, rate=100, do_not_save=True) + so.set_warehouse = warehouse + so.submit() + + self.assertEqual(len(so.items), 2) + self.assertTrue(so.items[1].is_free_item) + + pl = create_pick_list(so.name) + pl.ignore_pricing_rule = 1 + pl.save() + pl.submit() + + self.assertEqual(len(pl.locations), 1) + + delivery_note = create_delivery_note(pl.name) + + self.assertEqual(len(delivery_note.items), 1) + + def test_pick_list_not_reset_batch(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Do Not Reset Picked Item", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BTH-PICKLT-.######", + }, + ).name + + se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + batch1 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + batch2 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl = create_pick_list(so.name) + pl.save() + + for loc in pl.locations: + self.assertEqual(loc.batch_no, batch1) + loc.batch_no = batch2 + loc.picked_qty = 0.0 + + pl.save() + + for loc in pl.locations: + self.assertEqual(loc.batch_no, batch1) + loc.batch_no = batch2 + loc.picked_qty = 10.0 + + pl.save() + + for loc in pl.locations: + self.assertEqual(loc.batch_no, batch2) +>>>>>>> 34a80bfcd3 (fix: do not reset picked items) From 0b8cf3a369f8e98b04d14edf5345ac1d843b3365 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 19 Jan 2025 13:52:49 +0530 Subject: [PATCH 07/16] chore: fix conflicts --- .../stock/doctype/pick_list/test_pick_list.py | 222 ------------------ 1 file changed, 222 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 2fe80a3245c..259290e0654 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -725,14 +725,8 @@ class TestPickList(FrappeTestCase): for item in items ] -<<<<<<< HEAD def get_picked_items_details(pick_list_doc): items_data = {} -======= - so = make_sales_order(item_code=item, qty=4, rate=100) - pl = create_pick_list(so.name) - self.assertFalse(pl.locations) ->>>>>>> 34a80bfcd3 (fix: do not reset picked items) for location in pick_list_doc.locations: key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse @@ -747,7 +741,6 @@ class TestPickList(FrappeTestCase): return items_data -<<<<<<< HEAD # Step - 1: Setup - Create Items and Stock Entries items_properties = [ { @@ -755,40 +748,9 @@ class TestPickList(FrappeTestCase): }, { "valuation_rate": 200, -======= - so = make_sales_order(item_code=item, qty=5, rate=100) - - pl = create_pick_list(so.name) - pl.locations[0].qty = 5 - pl.save() - pl.submit() - self.assertTrue(pl.locations[0].serial_no) - self.assertEqual(pl.locations[0].qty, 5.0) - self.assertTrue(hasattr(pl, "locations")) - - so = make_sales_order(item_code=item, qty=5, rate=100) - - pl = create_pick_list(so.name) - pl.save() - self.assertTrue(pl.locations[0].serial_no) - self.assertEqual(pl.locations[0].qty, 5.0) - self.assertTrue(hasattr(pl, "locations")) - - so = make_sales_order(item_code=item, qty=4, rate=100) - pl = create_pick_list(so.name) - self.assertFalse(pl.locations) - - def test_pick_list_validation_for_batch_no(self): - warehouse = "_Test Warehouse - _TC" - item = make_item( - "Test Batch Pick List Item", - properties={ - "is_stock_item": 1, ->>>>>>> 34a80bfcd3 (fix: do not reset picked items) "has_batch_no": 1, "create_new_batch": 1, }, -<<<<<<< HEAD { "valuation_rate": 300, "has_serial_no": 1, @@ -796,40 +758,6 @@ class TestPickList(FrappeTestCase): }, { "valuation_rate": 400, -======= - ).name - - make_stock_entry(item=item, to_warehouse=warehouse, qty=10) - - so = make_sales_order(item_code=item, qty=5, rate=100) - - pl = create_pick_list(so.name) - pl.locations[0].qty = 5 - pl.save() - pl.submit() - self.assertTrue(pl.locations[0].batch_no) - self.assertEqual(pl.locations[0].qty, 5.0) - self.assertTrue(hasattr(pl, "locations")) - - so = make_sales_order(item_code=item, qty=5, rate=100) - - pl = create_pick_list(so.name) - pl.save() - self.assertTrue(pl.locations[0].batch_no) - self.assertEqual(pl.locations[0].qty, 5.0) - self.assertTrue(hasattr(pl, "locations")) - - so = make_sales_order(item_code=item, qty=4, rate=100) - pl = create_pick_list(so.name) - self.assertFalse(pl.locations) - - def test_pick_list_validation_for_batch_no_and_serial_item(self): - warehouse = "_Test Warehouse - _TC" - item = make_item( - "Test Serialized Batch Pick List Item", - properties={ - "is_stock_item": 1, ->>>>>>> 34a80bfcd3 (fix: do not reset picked items) "has_batch_no": 1, "create_new_batch": 1, "has_serial_no": 1, @@ -840,63 +768,8 @@ class TestPickList(FrappeTestCase): items = create_items(items_properties) create_stock_entries(items) -<<<<<<< HEAD # Step - 2: Create Sales Order [1] so1 = make_sales_order(item_list=get_item_list(items, qty=6)) -======= - so = make_sales_order(item_code=item, qty=5, rate=100) - - pl = create_pick_list(so.name) - pl.locations[0].qty = 5 - pl.save() - pl.submit() - self.assertTrue(pl.locations[0].batch_no) - self.assertTrue(pl.locations[0].serial_no) - self.assertEqual(pl.locations[0].qty, 5.0) - self.assertTrue(hasattr(pl, "locations")) - - so = make_sales_order(item_code=item, qty=5, rate=100) - - pl = create_pick_list(so.name) - pl.save() - self.assertTrue(pl.locations[0].batch_no) - self.assertTrue(pl.locations[0].serial_no) - self.assertEqual(pl.locations[0].qty, 5.0) - self.assertTrue(hasattr(pl, "locations")) - - so = make_sales_order(item_code=item, qty=4, rate=100) - pl = create_pick_list(so.name) - self.assertFalse(pl.locations) - - def test_pick_list_validation_for_multiple_batches_and_sales_order(self): - warehouse = "_Test Warehouse - _TC" - item = make_item( - "Test Batch Pick List Item For Multiple Batches", - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####", - "create_new_batch": 1, - }, - ).name - - make_stock_entry(item=item, to_warehouse=warehouse, qty=5) - make_stock_entry(item=item, to_warehouse=warehouse, qty=5) - - so = make_sales_order(item_code=item, qty=6, rate=100) - - pl1 = create_pick_list(so.name) - pl1.save() - self.assertEqual(pl1.locations[0].qty, 5.0) - self.assertEqual(pl1.locations[1].qty, 1.0) - - so = make_sales_order(item_code=item, qty=4, rate=100) - - pl = create_pick_list(so.name) - pl.save() - self.assertEqual(pl.locations[0].qty, 4.0) - self.assertTrue(hasattr(pl, "locations")) ->>>>>>> 34a80bfcd3 (fix: do not reset picked items) # Step - 3: Create and Submit Pick List [1] for Sales Order [1] pl1 = create_pick_list(so1.name) @@ -972,100 +845,6 @@ class TestPickList(FrappeTestCase): row.picked_qty = row.qty self.assertRaises(frappe.ValidationError, pl.save) -<<<<<<< HEAD -======= - - def test_over_allowance_picking(self): - warehouse = "_Test Warehouse - _TC" - item = make_item( - "Test Over Allowance Picking Item", - properties={ - "is_stock_item": 1, - }, - ).name - - make_stock_entry(item=item, to_warehouse=warehouse, qty=100) - - so = make_sales_order(item_code=item, qty=10, rate=100) - - pl_doc = create_pick_list(so.name) - pl_doc.save() - self.assertEqual(pl_doc.locations[0].qty, 10) - - pl_doc.locations[0].qty = 15 - pl_doc.locations[0].stock_qty = 15 - pl_doc.save() - - self.assertEqual(pl_doc.locations[0].qty, 15) - self.assertRaises(frappe.ValidationError, pl_doc.submit) - - frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 50) - - pl_doc.reload() - pl_doc.submit() - - frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0) - - def test_ignore_pricing_rule_in_pick_list(self): - frappe.flags.print_stmt = False - warehouse = "_Test Warehouse - _TC" - item = make_item( - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "batch_number_series": "IPR-PICKLT-.######", - "create_new_batch": 1, - } - ).name - - make_stock_entry( - item=item, - to_warehouse=warehouse, - qty=2, - basic_rate=100, - ) - - pricing_rule = frappe.get_doc( - { - "doctype": "Pricing Rule", - "title": "Same Free Item", - "price_or_product_discount": "Product", - "selling": 1, - "apply_on": "Item Code", - "items": [ - { - "item_code": item, - } - ], - "same_item": 1, - "is_recursive": 1, - "recurse_for": 2, - "free_qty": 1, - "company": "_Test Company", - "customer": "_Test Customer", - } - ) - - pricing_rule.save() - frappe.flags.print_stmt = True - - so = make_sales_order(item_code=item, qty=2, rate=100, do_not_save=True) - so.set_warehouse = warehouse - so.submit() - - self.assertEqual(len(so.items), 2) - self.assertTrue(so.items[1].is_free_item) - - pl = create_pick_list(so.name) - pl.ignore_pricing_rule = 1 - pl.save() - pl.submit() - - self.assertEqual(len(pl.locations), 1) - - delivery_note = create_delivery_note(pl.name) - - self.assertEqual(len(delivery_note.items), 1) def test_pick_list_not_reset_batch(self): warehouse = "_Test Warehouse - _TC" @@ -1105,4 +884,3 @@ class TestPickList(FrappeTestCase): for loc in pl.locations: self.assertEqual(loc.batch_no, batch2) ->>>>>>> 34a80bfcd3 (fix: do not reset picked items) From 7046a019216a0b721ee771c34a7b75ed15276fa3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 19 Jan 2025 14:22:52 +0530 Subject: [PATCH 08/16] chore: fix test case --- erpnext/stock/doctype/pick_list/test_pick_list.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 259290e0654..f679d28c2b2 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -859,9 +859,11 @@ class TestPickList(FrappeTestCase): ).name se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10) - batch1 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + se.reload() + batch1 = se.items[0].batch_no se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10) - batch2 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + se.reload() + batch2 = se.items[0].batch_no so = make_sales_order(item_code=item, qty=10, rate=100) From f9420db3ca607e76dac4221c0ddf8b2b3e916269 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Wed, 8 Jan 2025 13:51:33 +0530 Subject: [PATCH 09/16] fix: validate linked sales person (cherry picked from commit e614f0779553005d4b7947b8a40aaffad0159816) --- .../doctype/sales_person/sales_person.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index 91a7008fabd..8af5e4aeb09 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt +from frappe.utils.data import get_url_to_list from frappe.utils.nestedset import NestedSet, get_root_of from erpnext import get_default_currency @@ -14,6 +15,9 @@ class SalesPerson(NestedSet): nsm_parent_field = "parent_sales_person" def validate(self): + if not self.enabled: + self.validate_sales_person() + if not self.parent_sales_person: self.parent_sales_person = get_root_of("Sales Person") @@ -55,6 +59,25 @@ class SalesPerson(NestedSet): super().on_update() self.validate_one_root() + def validate_sales_person(self): + sales_team = frappe.qb.DocType("Sales Team") + + query = ( + frappe.qb.from_(sales_team) + .select(sales_team.sales_person) + .where((sales_team.sales_person == self.name) & (sales_team.parenttype == "Customer")) + .groupby(sales_team.sales_person) + ).run(as_dict=True) + + if query: + frappe.throw( + _("The Sales Person is linked with {0}").format( + frappe.bold( + f"""{"Customers"}""" + ) + ) + ) + def get_email_id(self): if self.employee: user = frappe.db.get_value("Employee", self.employee, "user_id") From 17535095e23f0b4859c6dd813be4e317b4683093 Mon Sep 17 00:00:00 2001 From: rs-rethik Date: Fri, 20 Dec 2024 12:07:20 +0530 Subject: [PATCH 10/16] feat: add difference_posting_date field (cherry picked from commit 225e56cbcae5c30172b52aa2e6cbc04a5ec01d43) # Conflicts: # erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json # erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py # erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json # erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py --- .../purchase_invoice_advance.json | 18 ++++++++++--- .../purchase_invoice_advance.py | 25 +++++++++++++++++++ .../sales_invoice_advance.json | 18 ++++++++++--- .../sales_invoice_advance.py | 25 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 9fcbf5c6339..2f6d30c0922 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -14,7 +14,8 @@ "advance_amount", "allocated_amount", "exchange_gain_loss", - "ref_exchange_rate" + "ref_exchange_rate", + "difference_posting_date" ], "fields": [ { @@ -30,7 +31,7 @@ "width": "180px" }, { - "columns": 3, + "columns": 2, "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -40,7 +41,7 @@ "read_only": 1 }, { - "columns": 3, + "columns": 2, "fieldname": "remarks", "fieldtype": "Text", "in_list_view": 1, @@ -111,13 +112,24 @@ "label": "Reference Exchange Rate", "non_negative": 1, "read_only": 1 + }, + { + "columns": 2, + "fieldname": "difference_posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Difference Posting Date" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-09-26 15:47:28.167371", +======= + "modified": "2024-12-20 12:04:46.729972", +>>>>>>> 225e56cbca (feat: add difference_posting_date field) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Advance", diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py index 44e1f6d5685..6f8971c12f4 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py @@ -6,4 +6,29 @@ from frappe.model.document import Document class PurchaseInvoiceAdvance(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + advance_amount: DF.Currency + allocated_amount: DF.Currency + difference_posting_date: DF.Date | None + exchange_gain_loss: DF.Currency + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + ref_exchange_rate: DF.Float + reference_name: DF.DynamicLink | None + reference_row: DF.Data | None + reference_type: DF.Link | None + remarks: DF.Text | None + # end: auto-generated types + +>>>>>>> 225e56cbca (feat: add difference_posting_date field) pass diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index f92b57a45e1..19336f84342 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -14,7 +14,8 @@ "advance_amount", "allocated_amount", "exchange_gain_loss", - "ref_exchange_rate" + "ref_exchange_rate", + "difference_posting_date" ], "fields": [ { @@ -30,7 +31,7 @@ "width": "250px" }, { - "columns": 3, + "columns": 2, "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -41,7 +42,7 @@ "read_only": 1 }, { - "columns": 3, + "columns": 2, "fieldname": "remarks", "fieldtype": "Text", "in_list_view": 1, @@ -112,13 +113,24 @@ "label": "Reference Exchange Rate", "non_negative": 1, "read_only": 1 + }, + { + "columns": 2, + "fieldname": "difference_posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Difference Posting Date" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-09-26 15:47:46.911595", +======= + "modified": "2024-12-20 11:58:28.962370", +>>>>>>> 225e56cbca (feat: add difference_posting_date field) "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Advance", diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py index 6d4bd4633c3..edfa42e5eb7 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py @@ -6,4 +6,29 @@ from frappe.model.document import Document class SalesInvoiceAdvance(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + advance_amount: DF.Currency + allocated_amount: DF.Currency + difference_posting_date: DF.Date | None + exchange_gain_loss: DF.Currency + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + ref_exchange_rate: DF.Float + reference_name: DF.DynamicLink | None + reference_row: DF.Data | None + reference_type: DF.Link | None + remarks: DF.Text | None + # end: auto-generated types + +>>>>>>> 225e56cbca (feat: add difference_posting_date field) pass From 0fdd6817a6e1c966244bfcac0cc348421ca2a156 Mon Sep 17 00:00:00 2001 From: rs-rethik Date: Fri, 20 Dec 2024 12:10:08 +0530 Subject: [PATCH 11/16] feat: use difference_posting_date for journal entry posting_date (cherry picked from commit ff1d040a6e1aa80c3b93835395db31a222dd5568) --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fa26fea3bf0..f2dab6beffc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1082,6 +1082,7 @@ class AccountsController(TransactionBase): "advance_amount": flt(d.amount), "allocated_amount": allocated_amount, "ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry + "difference_posting_date": self.posting_date, } self.append("advances", advance_row) @@ -1332,7 +1333,6 @@ class AccountsController(TransactionBase): gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ) - je = create_gain_loss_journal( self.company, args.get("difference_posting_date") if args else self.posting_date, @@ -1445,6 +1445,7 @@ class AccountsController(TransactionBase): "Company", self.company, "exchange_gain_loss_account" ), "exchange_gain_loss": flt(d.get("exchange_gain_loss")), + "difference_posting_date": d.get("difference_posting_date"), } ) lst.append(args) From 52309fe0b639b2beeb40a7b5a37dbfeefa2a3af9 Mon Sep 17 00:00:00 2001 From: rs-rethik Date: Fri, 20 Dec 2024 12:13:35 +0530 Subject: [PATCH 12/16] test: add unit test to validate journal entry posting date (cherry picked from commit c14a2d73bf10fd910e319bc62c2d8e117cce73f1) # Conflicts: # erpnext/controllers/tests/test_accounts_controller.py --- .../tests/test_accounts_controller.py | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 4ada8e60d9b..029a5cb9659 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2,6 +2,8 @@ # For license information, please see license.txt +from datetime import datetime + import frappe from frappe import qb from frappe.query_builder.functions import Sum @@ -1737,3 +1739,217 @@ class TestAccountsController(FrappeTestCase): # Exchange Gain/Loss Journal should've been cancelled exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) self.assertEqual(exc_je_for_je1, []) +<<<<<<< HEAD +======= + + def test_70_advance_payment_against_sales_invoice_in_foreign_currency(self): + """ + Customer advance booked under Liability + """ + self.setup_advance_accounts_in_party_master() + + adv = self.create_payment_entry(amount=1, source_exc_rate=83) + adv.save() # explicit 'save' is needed to trigger set_liability_account() + self.assertEqual(adv.paid_from, self.advance_received_usd) + adv.submit() + + si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) + si.debit_to = self.debtors_usd + si.save().submit() + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + pr = self.create_payment_reconciliation() + pr.receivable_payable_account = self.debtors_usd + pr.default_advance_account = self.advance_received_usd + pr.get_unreconciled_entries() + self.assertEqual(pr.invoices[0].invoice_number, si.name) + self.assertEqual(pr.payments[0].reference_name, adv.name) + + # Allocate and Reconcile + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exc Gain/Loss journal should've been creatad + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + adv.reload() + adv.cancel() + si.reload() + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + # Exc Gain/Loss journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_si), 0) + self.assertEqual(len(exc_je_for_adv), 0) + + self.remove_advance_accounts_from_party_master() + + def test_71_advance_payment_against_purchase_invoice_in_foreign_currency(self): + """ + Supplier advance booked under Asset + """ + self.setup_advance_accounts_in_party_master() + + usd_amount = 1 + inr_amount = 85 + exc_rate = 85 + adv = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.advance_paid_usd, + paid_amount=inr_amount, + ) + adv.source_exchange_rate = 1 + adv.target_exchange_rate = exc_rate + adv.received_amount = usd_amount + adv.paid_amount = exc_rate * usd_amount + adv.posting_date = nowdate() + adv.save() + # Make sure that advance account is still set + self.assertEqual(adv.paid_to, self.advance_paid_usd) + adv.submit() + + pi = self.create_purchase_invoice(qty=1, conversion_rate=83, rate=1) + self.assertEqual(pi.credit_to, self.creditors_usd) + self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) + + pr = self.create_payment_reconciliation() + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.default_advance_account = self.advance_paid_usd + pr.get_unreconciled_entries() + self.assertEqual(pr.invoices[0].invoice_number, pi.name) + self.assertEqual(pr.payments[0].reference_name, adv.name) + + # Allocate and Reconcile + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + self.assert_ledger_outstanding(pi.doctype, pi.name, 0.0, 0.0) + + # Exc Gain/Loss journal should've been creatad + exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_pi), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_pi, exc_je_for_adv) + + adv.reload() + adv.cancel() + pi.reload() + self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) + # Exc Gain/Loss journal should've been cancelled + exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_pi), 0) + self.assertEqual(len(exc_je_for_adv), 0) + + self.remove_advance_accounts_from_party_master() + + def test_difference_posting_date_in_pi_and_si(self): + self.setup_advance_accounts_in_party_master() + + # create payment entry for customer + adv = self.create_payment_entry(amount=1, source_exc_rate=83) + adv.save() + self.assertEqual(adv.paid_from, self.advance_received_usd) + adv.submit() + + # create sales invoice with advance received + si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) + si.debit_to = self.debtors_usd + si.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": "ACC-PAY-2024-00001", + "remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19", + "advance_amount": 1.0, + "allocated_amount": 1.0, + "exchange_gain_loss": 3.0, + "ref_exchange_rate": 83.0, + "difference_posting_date": add_days(nowdate(), -2), + }, + ) + si.save().submit() + + # exc Gain/Loss journal should've been creatad + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # check jv created with difference_posting_date in sales invoice + jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent) + sales_invoice = frappe.get_doc("Sales Invoice", si.name) + self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date) + + # create payment entry for supplier + usd_amount = 1 + inr_amount = 85 + exc_rate = 85 + adv = create_payment_entry( + company=self.company, + payment_type="Pay", + party_type="Supplier", + party=self.supplier, + paid_from=self.cash, + paid_to=self.advance_paid_usd, + paid_amount=inr_amount, + ) + adv.source_exchange_rate = 1 + adv.target_exchange_rate = exc_rate + adv.received_amount = usd_amount + adv.paid_amount = exc_rate * usd_amount + adv.posting_date = nowdate() + adv.save() + self.assertEqual(adv.paid_to, self.advance_paid_usd) + adv.submit() + + # create purchase invoice with advance paid + pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) + pi.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": "ACC-PAY-2024-00002", + "remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20", + "advance_amount": 1.0, + "allocated_amount": 1.0, + "exchange_gain_loss": 5.0, + "ref_exchange_rate": 85.0, + "difference_posting_date": add_days(nowdate(), -2), + }, + ) + pi.save().submit() + self.assertEqual(pi.credit_to, self.creditors_usd) + + # exc Gain/Loss journal should've been creatad + exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(len(exc_je_for_pi), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_pi, exc_je_for_adv) + + # check jv created with difference_posting_date in purchase invoice + journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent) + purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name) + self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date) +>>>>>>> c14a2d73bf (test: add unit test to validate journal entry posting date) From 33a1da8194ca6d471373c81bcec9510bd4a5780b Mon Sep 17 00:00:00 2001 From: rs-rethik Date: Fri, 20 Dec 2024 12:42:47 +0530 Subject: [PATCH 13/16] refactor: convert sql query to query builder (cherry picked from commit 2d58e845e633943bae2d481f66d200b16ab91bdf) --- erpnext/controllers/accounts_controller.py | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f2dab6beffc..e3d5ebf7fa1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -346,13 +346,14 @@ class AccountsController(TransactionBase): == 1 ) ).run() - frappe.db.sql( - "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) - ) - frappe.db.sql( - "delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", - (self.doctype, self.name), - ) + gle = frappe.qb.DocType("GL Entry") + frappe.qb.from_(gle).delete().where( + (gle.voucher_type == self.doctype) & (gle.voucher_no == self.name) + ).run() + sle = frappe.qb.DocType("Stock Ledger Entry") + frappe.qb.from_(gle).delete().where( + (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) + ).run() def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: @@ -1027,11 +1028,12 @@ class AccountsController(TransactionBase): def clear_unallocated_advances(self, childtype, parentfield): self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]})) - frappe.db.sql( - """delete from `tab{}` where parentfield={} and parent = {} - and allocated_amount = 0""".format(childtype, "%s", "%s"), - (parentfield, self.name), - ) + doctype = frappe.qb.DocType(childtype) + frappe.qb.from_(doctype).delete().where( + (doctype.parentfield == parentfield) + & (doctype.parent == self.name) + & (doctype.allocated_amount == 0) + ).run() @frappe.whitelist() def apply_shipping_rule(self): @@ -1972,11 +1974,9 @@ class AccountsController(TransactionBase): for adv in self.advances: consider_for_total_advance = True if adv.reference_name == linked_doc_name: - frappe.db.sql( - f"""delete from `tab{self.doctype} Advance` - where name = %s""", - adv.name, - ) + doctype = frappe.qb.DocType(self.doctype + " Advance") + frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run() + consider_for_total_advance = False if consider_for_total_advance: From 49e3865265477a718564c6c92ff6be5c8b472fed Mon Sep 17 00:00:00 2001 From: rs-rethik Date: Fri, 20 Dec 2024 14:00:54 +0530 Subject: [PATCH 14/16] fix: update query (cherry picked from commit 854e37c05c454ad2d93ec0f4a70600f2ca7d5eec) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e3d5ebf7fa1..ecbf1177ee4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -351,7 +351,7 @@ class AccountsController(TransactionBase): (gle.voucher_type == self.doctype) & (gle.voucher_no == self.name) ).run() sle = frappe.qb.DocType("Stock Ledger Entry") - frappe.qb.from_(gle).delete().where( + frappe.qb.from_(sle).delete().where( (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) ).run() From 8764a321c73c124019dbfde72aa43efc32424fe6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 21 Jan 2025 12:57:10 +0530 Subject: [PATCH 15/16] chore: resolve conflicts --- .../purchase_invoice_advance.json | 4 - .../purchase_invoice_advance.py | 25 -- .../sales_invoice_advance.json | 4 - .../sales_invoice_advance.py | 25 -- .../tests/test_accounts_controller.py | 214 ------------------ 5 files changed, 272 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 2f6d30c0922..ebd309bfb67 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -125,11 +125,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-09-26 15:47:28.167371", -======= "modified": "2024-12-20 12:04:46.729972", ->>>>>>> 225e56cbca (feat: add difference_posting_date field) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Advance", diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py index 6f8971c12f4..44e1f6d5685 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.py @@ -6,29 +6,4 @@ from frappe.model.document import Document class PurchaseInvoiceAdvance(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - advance_amount: DF.Currency - allocated_amount: DF.Currency - difference_posting_date: DF.Date | None - exchange_gain_loss: DF.Currency - parent: DF.Data - parentfield: DF.Data - parenttype: DF.Data - ref_exchange_rate: DF.Float - reference_name: DF.DynamicLink | None - reference_row: DF.Data | None - reference_type: DF.Link | None - remarks: DF.Text | None - # end: auto-generated types - ->>>>>>> 225e56cbca (feat: add difference_posting_date field) pass diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index 19336f84342..d4e3b9d896c 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -126,11 +126,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-09-26 15:47:46.911595", -======= "modified": "2024-12-20 11:58:28.962370", ->>>>>>> 225e56cbca (feat: add difference_posting_date field) "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Advance", diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py index edfa42e5eb7..6d4bd4633c3 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.py @@ -6,29 +6,4 @@ from frappe.model.document import Document class SalesInvoiceAdvance(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - advance_amount: DF.Currency - allocated_amount: DF.Currency - difference_posting_date: DF.Date | None - exchange_gain_loss: DF.Currency - parent: DF.Data - parentfield: DF.Data - parenttype: DF.Data - ref_exchange_rate: DF.Float - reference_name: DF.DynamicLink | None - reference_row: DF.Data | None - reference_type: DF.Link | None - remarks: DF.Text | None - # end: auto-generated types - ->>>>>>> 225e56cbca (feat: add difference_posting_date field) pass diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 029a5cb9659..a184009b1e1 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1739,217 +1739,3 @@ class TestAccountsController(FrappeTestCase): # Exchange Gain/Loss Journal should've been cancelled exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name) self.assertEqual(exc_je_for_je1, []) -<<<<<<< HEAD -======= - - def test_70_advance_payment_against_sales_invoice_in_foreign_currency(self): - """ - Customer advance booked under Liability - """ - self.setup_advance_accounts_in_party_master() - - adv = self.create_payment_entry(amount=1, source_exc_rate=83) - adv.save() # explicit 'save' is needed to trigger set_liability_account() - self.assertEqual(adv.paid_from, self.advance_received_usd) - adv.submit() - - si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) - si.debit_to = self.debtors_usd - si.save().submit() - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) - - pr = self.create_payment_reconciliation() - pr.receivable_payable_account = self.debtors_usd - pr.default_advance_account = self.advance_received_usd - pr.get_unreconciled_entries() - self.assertEqual(pr.invoices[0].invoice_number, si.name) - self.assertEqual(pr.payments[0].reference_name, adv.name) - - # Allocate and Reconcile - invoices = [x.as_dict() for x in pr.invoices] - payments = [x.as_dict() for x in pr.payments] - pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() - self.assertEqual(len(pr.invoices), 0) - self.assertEqual(len(pr.payments), 0) - self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) - - # Exc Gain/Loss journal should've been creatad - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_si, exc_je_for_adv) - - adv.reload() - adv.cancel() - si.reload() - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) - # Exc Gain/Loss journal should've been cancelled - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_si), 0) - self.assertEqual(len(exc_je_for_adv), 0) - - self.remove_advance_accounts_from_party_master() - - def test_71_advance_payment_against_purchase_invoice_in_foreign_currency(self): - """ - Supplier advance booked under Asset - """ - self.setup_advance_accounts_in_party_master() - - usd_amount = 1 - inr_amount = 85 - exc_rate = 85 - adv = create_payment_entry( - company=self.company, - payment_type="Pay", - party_type="Supplier", - party=self.supplier, - paid_from=self.cash, - paid_to=self.advance_paid_usd, - paid_amount=inr_amount, - ) - adv.source_exchange_rate = 1 - adv.target_exchange_rate = exc_rate - adv.received_amount = usd_amount - adv.paid_amount = exc_rate * usd_amount - adv.posting_date = nowdate() - adv.save() - # Make sure that advance account is still set - self.assertEqual(adv.paid_to, self.advance_paid_usd) - adv.submit() - - pi = self.create_purchase_invoice(qty=1, conversion_rate=83, rate=1) - self.assertEqual(pi.credit_to, self.creditors_usd) - self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) - - pr = self.create_payment_reconciliation() - pr.party_type = "Supplier" - pr.party = self.supplier - pr.receivable_payable_account = self.creditors_usd - pr.default_advance_account = self.advance_paid_usd - pr.get_unreconciled_entries() - self.assertEqual(pr.invoices[0].invoice_number, pi.name) - self.assertEqual(pr.payments[0].reference_name, adv.name) - - # Allocate and Reconcile - invoices = [x.as_dict() for x in pr.invoices] - payments = [x.as_dict() for x in pr.payments] - pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() - self.assertEqual(len(pr.invoices), 0) - self.assertEqual(len(pr.payments), 0) - self.assert_ledger_outstanding(pi.doctype, pi.name, 0.0, 0.0) - - # Exc Gain/Loss journal should've been creatad - exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_pi), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_pi, exc_je_for_adv) - - adv.reload() - adv.cancel() - pi.reload() - self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0) - # Exc Gain/Loss journal should've been cancelled - exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_pi), 0) - self.assertEqual(len(exc_je_for_adv), 0) - - self.remove_advance_accounts_from_party_master() - - def test_difference_posting_date_in_pi_and_si(self): - self.setup_advance_accounts_in_party_master() - - # create payment entry for customer - adv = self.create_payment_entry(amount=1, source_exc_rate=83) - adv.save() - self.assertEqual(adv.paid_from, self.advance_received_usd) - adv.submit() - - # create sales invoice with advance received - si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) - si.debit_to = self.debtors_usd - si.append( - "advances", - { - "reference_type": "Payment Entry", - "reference_name": "ACC-PAY-2024-00001", - "remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19", - "advance_amount": 1.0, - "allocated_amount": 1.0, - "exchange_gain_loss": 3.0, - "ref_exchange_rate": 83.0, - "difference_posting_date": add_days(nowdate(), -2), - }, - ) - si.save().submit() - - # exc Gain/Loss journal should've been creatad - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_si, exc_je_for_adv) - - # check jv created with difference_posting_date in sales invoice - jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent) - sales_invoice = frappe.get_doc("Sales Invoice", si.name) - self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date) - - # create payment entry for supplier - usd_amount = 1 - inr_amount = 85 - exc_rate = 85 - adv = create_payment_entry( - company=self.company, - payment_type="Pay", - party_type="Supplier", - party=self.supplier, - paid_from=self.cash, - paid_to=self.advance_paid_usd, - paid_amount=inr_amount, - ) - adv.source_exchange_rate = 1 - adv.target_exchange_rate = exc_rate - adv.received_amount = usd_amount - adv.paid_amount = exc_rate * usd_amount - adv.posting_date = nowdate() - adv.save() - self.assertEqual(adv.paid_to, self.advance_paid_usd) - adv.submit() - - # create purchase invoice with advance paid - pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True) - pi.append( - "advances", - { - "reference_type": "Payment Entry", - "reference_name": "ACC-PAY-2024-00002", - "remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20", - "advance_amount": 1.0, - "allocated_amount": 1.0, - "exchange_gain_loss": 5.0, - "ref_exchange_rate": 85.0, - "difference_posting_date": add_days(nowdate(), -2), - }, - ) - pi.save().submit() - self.assertEqual(pi.credit_to, self.creditors_usd) - - # exc Gain/Loss journal should've been creatad - exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(len(exc_je_for_pi), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_pi, exc_je_for_adv) - - # check jv created with difference_posting_date in purchase invoice - journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent) - purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name) - self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date) ->>>>>>> c14a2d73bf (test: add unit test to validate journal entry posting date) From 431fa225e3bc13e99ea7cd06e05dd3fbd16a01bf Mon Sep 17 00:00:00 2001 From: venkat102 Date: Mon, 13 Jan 2025 16:51:25 +0530 Subject: [PATCH 16/16] fix: include pos invoice in modifing key for returned item validation (cherry picked from commit 2936139c797e1e9a36e6b743fe616718bfacdc9b) --- erpnext/controllers/sales_and_purchase_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index f661cc33c63..b2a4a4e0f7e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -105,7 +105,7 @@ def validate_returned_items(doc): for d in doc.get("items"): key = d.item_code raise_exception = False - if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice"]: + if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]: field = frappe.scrub(doc.doctype) + "_item" if d.get(field): key = (d.item_code, d.get(field))