From eeff0a1252b81738ae7cfb5bdc751c3563611b28 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Oct 2024 17:53:47 +0530 Subject: [PATCH 01/48] refactor: cancel old PR and invalidate tokens (cherry picked from commit cda7800777371a34a99d9b8d55c809e03f8abfcf) --- .../payment_request/payment_request.py | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 2d4a64c317b..107ed8db8e1 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -1,7 +1,7 @@ import json import frappe -from frappe import _ +from frappe import _, qb from frappe.model.document import Document from frappe.query_builder.functions import Sum from frappe.utils import flt, nowdate @@ -564,11 +564,24 @@ def make_payment_request(**args): # fetches existing payment request `grand_total` amount existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name) - if existing_payment_request_amount: + def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount): grand_total -= existing_payment_request_amount - if not grand_total: frappe.throw(_("Payment Request is already created")) + return grand_total + + if existing_payment_request_amount: + if args.order_type == "Shopping Cart": + # If Payment Request is in an advanced stage, then create for remaining amount. + if get_existing_payment_request_amount( + ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"] + ): + grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) + else: + # If PR's are processed, cancel all of them. + cancel_old_payment_requests(ref_doc.doctype, ref_doc.name) + else: + grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) if draft_payment_request: frappe.db.set_value( @@ -678,21 +691,47 @@ def get_amount(ref_doc, payment_account=None): frappe.throw(_("Payment Entry is already created")) -def get_existing_payment_request_amount(ref_dt, ref_dn): +def cancel_old_payment_requests(ref_dt, ref_dn): + PR = frappe.qb.DocType("Payment Request") + + if res := ( + frappe.qb.from_(PR) + .select(PR.name) + .where(PR.reference_doctype == ref_dt) + .where(PR.reference_name == ref_dn) + .where(PR.docstatus == 1) + .where(PR.status.isin(["Draft", "Requested"])) + .run(as_dict=True) + ): + for x in res: + doc = frappe.get_doc("Payment Request", x.name) + doc.flags.ignore_permissions = True + doc.cancel() + + if ireqs := get_irequests_of_payment_request(doc.name): + for ireq in ireqs: + frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled") + + +def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list: """ Return the total amount of Payment Requests against a reference document. """ PR = frappe.qb.DocType("Payment Request") - response = ( + query = ( frappe.qb.from_(PR) .select(Sum(PR.grand_total)) .where(PR.reference_doctype == ref_dt) .where(PR.reference_name == ref_dn) .where(PR.docstatus == 1) - .run() ) + if statuses: + query = query.where(PR.status.isin(statuses)) + + response = query.run() + return response[0][0] if response[0] else 0 @@ -915,3 +954,17 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, ) for pr in open_payment_requests ] + + +def get_irequests_of_payment_request(doc: str | None = None) -> list: + res = [] + if doc: + res = frappe.db.get_all( + "Integration Request", + { + "reference_doctype": "Payment Request", + "reference_docname": doc, + "status": "Queued", + }, + ) + return res From ff4751c9e8e58e4cf3561a123fdb2d5735514d37 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 6 Nov 2024 17:58:22 +0530 Subject: [PATCH 02/48] refactor: handle PR's in advance stage (cherry picked from commit 18c13a2cff8594d177777afb5519a9a20b968e09) --- .../payment_request/payment_request.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 107ed8db8e1..577a1ea2426 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -691,6 +691,21 @@ def get_amount(ref_doc, payment_account=None): frappe.throw(_("Payment Entry is already created")) +def get_irequest_status(payment_requests: None | list = None) -> list: + IR = frappe.qb.DocType("Integration Request") + res = [] + if payment_requests: + res = ( + frappe.qb.from_(IR) + .select(IR.name) + .where(IR.reference_doctype.eq("Payment Request")) + .where(IR.reference_docname.isin(payment_requests)) + .where(IR.status.isin(["Authorized", "Completed"])) + .run(as_dict=True) + ) + return res + + def cancel_old_payment_requests(ref_dt, ref_dn): PR = frappe.qb.DocType("Payment Request") @@ -703,14 +718,17 @@ def cancel_old_payment_requests(ref_dt, ref_dn): .where(PR.status.isin(["Draft", "Requested"])) .run(as_dict=True) ): - for x in res: - doc = frappe.get_doc("Payment Request", x.name) - doc.flags.ignore_permissions = True - doc.cancel() + if get_irequest_status([x.name for x in res]): + frappe.throw(_("Another Payment Request is already processed")) + else: + for x in res: + doc = frappe.get_doc("Payment Request", x.name) + doc.flags.ignore_permissions = True + doc.cancel() - if ireqs := get_irequests_of_payment_request(doc.name): - for ireq in ireqs: - frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled") + if ireqs := get_irequests_of_payment_request(doc.name): + for ireq in ireqs: + frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled") def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list: From 58ca4a2b99a694ea891e1b3cfd949f799edacafb Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 18 Sep 2024 19:38:14 +0530 Subject: [PATCH 03/48] fix: improved the conditions for determining voucher subtypes (cherry picked from commit 00eee161904b5deb01ba80b6a3629bd7e2dd613f) --- erpnext/controllers/accounts_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e47e9917149..b14cf428c53 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1096,9 +1096,11 @@ class AccountsController(TransactionBase): return "Purchase Return" elif self.doctype == "Delivery Note" and self.is_return: return "Sales Return" - elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice": + elif self.doctype == "Sales Invoice" and self.is_return: return "Credit Note" - elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice": + elif self.doctype == "Sales Invoice" and self.is_debit_note: + return "Debit Note" + elif self.doctype == "Purchase Invoice" and self.is_return: return "Debit Note" return self.doctype From 107d53b3582ccdac3f237745efba4eca5a14a328 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Sep 2024 20:10:36 +0530 Subject: [PATCH 04/48] fix: patch (cherry picked from commit d76cc210860651377262371359b65e4f7ea9abb7) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 5 ++ .../update_sub_voucher_type_in_gl_entries.py | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 64c74f9d645..01dc68d1d3a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -379,5 +379,10 @@ erpnext.patches.v15_0.drop_index_posting_datetime_from_sle erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 erpnext.patches.v15_0.set_standard_stock_entry_type erpnext.patches.v15_0.link_purchase_item_to_asset_doc +<<<<<<< HEAD erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log +======= +erpnext.patches.v15_0.migrate_to_utm_analytics +erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries +>>>>>>> d76cc21086 (fix: patch) diff --git a/erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py b/erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py new file mode 100644 index 00000000000..7160a6ba87d --- /dev/null +++ b/erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py @@ -0,0 +1,57 @@ +import frappe + + +def execute(): + update_purchase_invoices() + update_sales_invoices() + update_sales_debit_notes() + + +def update_purchase_invoices(): + invoices = frappe.get_all( + "Purchase Invoice", + filters={"docstatus": 1, "is_return": 0}, + pluck="name", + ) + + if not invoices: + return + + update_gl_entry(doctype="Purchase Invoice", invoices=invoices, value="Purchase Invoice") + + +def update_sales_invoices(): + invoices = frappe.get_all( + "Sales Invoice", + filters={"docstatus": 1, "is_return": 0, "is_debit_note": 0}, + pluck="name", + ) + if not invoices: + return + + update_gl_entry(doctype="Sales Invoice", invoices=invoices, value="Sales Invoice") + + +def update_sales_debit_notes(): + invoices = frappe.get_all( + "Sales Invoice", + filters={"docstatus": 1, "is_debit_note": 1}, + pluck="name", + ) + + if not invoices: + return + + update_gl_entry(doctype="Sales Invoice", invoices=invoices, value="Debit Note") + + +def update_gl_entry(doctype, invoices, value): + gl_entry = frappe.qb.DocType("GL Entry") + ( + frappe.qb.update(gl_entry) + .set("voucher_subtype", value) + .where(gl_entry.voucher_subtype.isnotnull()) + .where(gl_entry.voucher_no.isin(invoices)) + .where(gl_entry.voucher_type == doctype) + .run() + ) From d7f91824c0e7b5a36ac5aa28f9d6d23580ed192e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 7 Nov 2024 13:35:54 +0530 Subject: [PATCH 05/48] test: test voucher subtype for sales invoice (cherry picked from commit ad6cc352f189449484e8560dc4a4c87ff8c0f894) --- .../sales_invoice/test_sales_invoice.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d05da0dbf19..db6fd41e439 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4005,6 +4005,25 @@ class TestSalesInvoice(FrappeTestCase): si.submit() self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}") + def test_gl_voucher_subtype(self): + si = create_sales_invoice() + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, + pluck="voucher_subtype", + ) + + self.assertTrue(all([x == "Sales Invoice" for x in gl_entries])) + + si = create_sales_invoice(is_return=1, qty=-1) + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, + pluck="voucher_subtype", + ) + + self.assertTrue(all([x == "Credit Note" for x in gl_entries])) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 6649d17b068fcacf7c8b24b1b1cdc5891c4735dd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Nov 2024 10:33:56 +0530 Subject: [PATCH 06/48] chore: resolve conflict --- erpnext/patches.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 01dc68d1d3a..1f1c2edc917 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -379,10 +379,6 @@ erpnext.patches.v15_0.drop_index_posting_datetime_from_sle erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 erpnext.patches.v15_0.set_standard_stock_entry_type erpnext.patches.v15_0.link_purchase_item_to_asset_doc -<<<<<<< HEAD erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log -======= -erpnext.patches.v15_0.migrate_to_utm_analytics erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries ->>>>>>> d76cc21086 (fix: patch) From 978a0078d8f2e93878b883643c94d327deb5fd06 Mon Sep 17 00:00:00 2001 From: "bhaveshkumar.j" Date: Mon, 21 Oct 2024 18:47:20 +0000 Subject: [PATCH 07/48] fix: NoneType while updating ordered_qty in SO for removed items (cherry picked from commit 442cdd7ce49ccbaff855bfac71f3222bc83b7d49) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c03c896c29f..f4e1984332f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -625,9 +625,11 @@ class PurchaseOrder(BuyingController): if not self.is_against_so(): return for item in removed_items: - prev_ordered_qty = frappe.get_cached_value( - "Sales Order Item", item.get("sales_order_item"), "ordered_qty" + prev_ordered_qty = ( + frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty") + or 0.0 ) + frappe.db.set_value( "Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty ) From 487b5776e62afc0f8b256e5028bbe5cac288ab4b Mon Sep 17 00:00:00 2001 From: Ravindu Nethmina <117300601+NethminaHiker360@users.noreply.github.com> Date: Mon, 28 Oct 2024 08:08:32 +0000 Subject: [PATCH 08/48] refactor: add "margin_type" and "margin_rate_or_amount" to no copy (cherry picked from commit 70f090c1ec3be1b904b19801b47dac16ed4a8775) --- erpnext/selling/doctype/sales_order/sales_order.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a87c1352471..a15e8adb57c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1347,6 +1347,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "discount_percentage", "discount_amount", "pricing_rules", + "margin_type", + "margin_rate_or_amount", ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty From 6dcd015a39be3d5985482f8899da7dc670c52576 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:23:50 +0530 Subject: [PATCH 09/48] fix: item not set in the batch quick entry form (backport #44028) (#44031) fix: item not set in the batch quick entry form (#44028) (cherry picked from commit 0399ccc51ea9d0f48f1221dda0761603d1cea22d) Co-authored-by: rohitwaghchaure --- erpnext/public/js/controllers/transaction.js | 51 +++++++++++--------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index adb72818fe8..1773586ac48 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -289,28 +289,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } ]); } - - if(this.frm.fields_dict['items'].grid.get_field('serial_and_batch_bundle')) { - let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle'); - if (sbb_field) { - sbb_field.get_route_options_for_new_doc = (row) => { - return { - 'item_code': row.doc.item_code, - } - }; - } - } - - if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { - let batch_no_field = this.frm.get_docfield('items', 'batch_no'); - if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { - return { - 'item': row.doc.item_code - } - }; - } - } } is_return() { @@ -409,6 +387,35 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.setup_quality_inspection(); this.validate_has_items(); erpnext.utils.view_serial_batch_nos(this.frm); + this.set_route_options_for_new_doc(); + } + + set_route_options_for_new_doc() { + // While creating the batch from the link field, copy item from line item to batch form + + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + let batch_no_field = this.frm.get_docfield('items', 'batch_no'); + if (batch_no_field) { + batch_no_field.get_route_options_for_new_doc = function(row) { + return { + 'item': row.doc.item_code + } + }; + } + } + + // While creating the SABB from the link field, copy item, doctype from line item to SABB form + if(this.frm.fields_dict['items'].grid.get_field('serial_and_batch_bundle')) { + let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle'); + if (sbb_field) { + sbb_field.get_route_options_for_new_doc = (row) => { + return { + "item_code": row.doc.item_code, + "voucher_type": this.frm.doc.doctype, + } + }; + } + } } scan_barcode() { From 5958d0c2570aa7ae3e0e86e1afdbef89758318f1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:24:21 +0530 Subject: [PATCH 10/48] fix: calculate percentage received and delivered considering over-receipt and over-delivery (backport #43870) (#44030) fix: calculate percentage received and delivered considering over-receipt and over-delivery (#43870) (cherry picked from commit adba1168c1bf28911b9664934d2e84bbae906dec) Co-authored-by: Nabin Hait --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index f4e1984332f..6ba9cb69f9f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -581,7 +581,7 @@ class PurchaseOrder(BuyingController): def update_receiving_percentage(self): total_qty, received_qty = 0.0, 0.0 for item in self.items: - received_qty += item.received_qty + received_qty += min(item.received_qty, item.qty) total_qty += item.qty if total_qty: self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a15e8adb57c..62262b41021 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -584,7 +584,7 @@ class SalesOrder(SellingController): item_delivered_qty = item_delivered_qty[0][0] if item_delivered_qty else 0 item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False) - delivered_qty += item.delivered_qty + delivered_qty += min(item.delivered_qty, item.qty) tot_qty += item.qty if tot_qty != 0: From 56f25ae0657e941f865fb3a224dea3767c8c3ecb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:34:47 +0530 Subject: [PATCH 11/48] fix: sort by ascending to get the first period closing voucher (backport #44029) (#44035) fix: sort by ascending to get the first period closing voucher (#44029) (cherry picked from commit 42dcdcde1a7a982f376f3ddd6969f825b42203e0) Co-authored-by: Venkatesh <47534423+venkat102@users.noreply.github.com> --- .../doctype/period_closing_voucher/period_closing_voucher.py | 2 +- erpnext/patches.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 2a9b624a47a..1d4ee25241e 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -417,7 +417,7 @@ class PeriodClosingVoucher(AccountsController): "Period Closing Voucher", {"company": self.company, "docstatus": 1}, "name", - order_by="period_end_date", + order_by="period_end_date asc", ) if not first_pcv or first_pcv == self.name: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1f1c2edc917..ab416d9d6c3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,7 +315,7 @@ erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.set_period_start_end_date_in_pcv -erpnext.patches.v14_0.update_closing_balances #29-10-2024 +erpnext.patches.v14_0.update_closing_balances #08-11-2024 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts erpnext.patches.v14_0.update_subscription_details From 4cde77d8d8f4a6f833e4f28d7cd6f08ac0005474 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:05:48 +0530 Subject: [PATCH 12/48] fix: duplicate items and outdated item price in POS (backport #42978) (#44038) fix: duplicate items and outdated item price in POS (#42978) * fix: duplicate items and outdated item price in POS * fix: duplicate items and outdated item price in POS --formatter (cherry picked from commit 4ea2071265d524e1e09280ec8fe70d8861d1bda6) Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- erpnext/selling/page/point_of_sale/point_of_sale.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 2bb61a6439c..fad184d152c 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -159,6 +159,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if not items_data: return result + current_date = frappe.utils.today() + for item in items_data: uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) @@ -167,12 +169,16 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te item_price = frappe.get_all( "Item Price", - fields=["price_list_rate", "currency", "uom", "batch_no"], + fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"], filters={ "price_list": price_list, "item_code": item.item_code, "selling": True, + "valid_from": ["<=", current_date], + "valid_upto": ["in", [None, "", current_date]], }, + order_by="valid_from desc", + limit=1, ) if not item_price: From 50fa77276e72e5c32afc66a14871f284e78dccdc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Nov 2024 16:13:52 +0530 Subject: [PATCH 13/48] refactor: depracate old method and handle inclusive tax --- erpnext/controllers/taxes_and_totals.py | 24 ++++++++++++----- erpnext/public/js/controllers/accounts.js | 9 ++++--- .../public/js/controllers/taxes_and_totals.js | 27 +++++++++++++------ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index fc0ba56d1f4..bf5beab1a82 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -8,6 +8,7 @@ import frappe from frappe import _, scrub from frappe.model.document import Document from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction +from frappe.utils.deprecations import deprecated import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate @@ -74,7 +75,7 @@ class calculate_taxes_and_totals: self.calculate_net_total() self.calculate_tax_withholding_net_total() self.calculate_taxes() - self.manipulate_grand_total_for_inclusive_tax() + self.adjust_grand_total_for_inclusive_tax() self.calculate_totals() self._cleanup() self.calculate_total_net_weight() @@ -286,7 +287,7 @@ class calculate_taxes_and_totals: ): amount = flt(item.amount) - total_inclusive_tax_amount_per_qty - item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) + item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount")) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) item.discount_percentage = flt( item.discount_percentage, item.precision("discount_percentage") @@ -531,7 +532,12 @@ class calculate_taxes_and_totals: tax.base_tax_amount = round(tax.base_tax_amount, 0) tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0) + @deprecated def manipulate_grand_total_for_inclusive_tax(self): + # for backward compatablility - if in case used by an external application + return self.adjust_grand_total_for_inclusive_tax() + + def adjust_grand_total_for_inclusive_tax(self): # if fully inclusive taxes and diff if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")): last_tax = self.doc.get("taxes")[-1] @@ -553,17 +559,21 @@ class calculate_taxes_and_totals: diff = flt(diff, self.doc.precision("rounding_adjustment")) if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): - self.doc.rounding_adjustment = diff + self.doc.grand_total_diff = diff + else: + self.doc.grand_total_diff = 0 def calculate_totals(self): if self.doc.get("taxes"): - self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment) + self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt( + self.doc.get("grand_total_diff") + ) else: self.doc.grand_total = flt(self.doc.net_total) if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( - self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), + self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")), self.doc.precision("total_taxes_and_charges"), ) else: @@ -626,8 +636,8 @@ class calculate_taxes_and_totals: self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total") ) - # if print_in_rate is set, we would have already calculated rounding adjustment - self.doc.rounding_adjustment += flt( + # rounding adjustment should always be the difference vetween grand and rounded total + self.doc.rounding_adjustment = flt( self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment") ) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index c39fb524264..c7b08f1dc15 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -160,7 +160,7 @@ erpnext.accounts.taxes = { let tax = frappe.get_doc(cdt, cdn); try { me.validate_taxes_and_charges(cdt, cdn); - me.validate_inclusive_tax(tax); + me.validate_inclusive_tax(tax, frm); } catch(e) { tax.included_in_print_rate = 0; refresh_field("included_in_print_rate", tax.name, tax.parentfield); @@ -170,7 +170,8 @@ erpnext.accounts.taxes = { }); }, - validate_inclusive_tax: function(tax) { + validate_inclusive_tax: function(tax, frm) { + this.frm = this.frm || frm; let actual_type_error = function() { var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx]) frappe.throw(msg); @@ -186,12 +187,12 @@ erpnext.accounts.taxes = { if(tax.charge_type == "Actual") { // inclusive tax cannot be of type Actual actual_type_error(); - } else if(tax.charge_type == "On Previous Row Amount" && + } else if(tax.charge_type == "On Previous Row Amount" && this.frm && !cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate) ) { // referred row should also be an inclusive tax on_previous_row_error(tax.row_id); - } else if(tax.charge_type == "On Previous Row Total") { + } else if(tax.charge_type == "On Previous Row Total" && this.frm) { var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id), function(t) { return cint(t.included_in_print_rate) ? null : t; }); if(taxes_not_included.length > 0) { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index dc7a5a8eef1..8b2d7943772 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -103,7 +103,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.determine_exclusive_rate(); this.calculate_net_total(); this.calculate_taxes(); - this.manipulate_grand_total_for_inclusive_tax(); + this.adjust_grand_total_for_inclusive_tax(); this.calculate_totals(); this._cleanup(); } @@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (!this.discount_amount_applied) { erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name); - erpnext.accounts.taxes.validate_inclusive_tax(tax); + erpnext.accounts.taxes.validate_inclusive_tax(tax, this.frm); } frappe.model.round_floats_in(tax); }); @@ -250,7 +250,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) { var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty; - item.net_amount = flt(amount / (1 + cumulated_tax_fraction)); + item.net_amount = flt(amount / (1 + cumulated_tax_fraction), precision("net_amount", item)); item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0; me.set_in_company_currency(item, ["net_rate", "net_amount"]); @@ -305,6 +305,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { me.frm.doc.net_total += item.net_amount; me.frm.doc.base_net_total += item.base_net_amount; }); + + frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); } calculate_shipping_charges() { @@ -523,7 +525,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } + /** + * @deprecated Use adjust_grand_total_for_inclusive_tax instead. + */ manipulate_grand_total_for_inclusive_tax() { + // for backward compatablility - if in case used by an external application + this.adjust_grand_total_for_inclusive_tax() + } + + adjust_grand_total_for_inclusive_tax() { var me = this; // if fully inclusive taxes and diff if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) { @@ -550,7 +560,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { diff = flt(diff, precision("rounding_adjustment")); if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) { - me.frm.doc.rounding_adjustment = diff; + me.frm.doc.grand_total_diff = diff; + } else { + me.frm.doc.grand_total_diff = 0; } } } @@ -561,7 +573,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var me = this; var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; this.frm.doc.grand_total = flt(tax_count - ? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment) + ? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff) : this.frm.doc.net_total); if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { @@ -621,7 +633,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) { this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total, this.frm.doc.currency, precision("rounded_total")); - this.frm.doc.rounding_adjustment += flt(this.frm.doc.rounded_total - this.frm.doc.grand_total, + this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total, precision("rounding_adjustment")); this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]); @@ -689,8 +701,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (total_for_discount_amount) { $.each(this.frm._items || [], function(i, item) { distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; - item.net_amount = flt(item.net_amount - distributed_amount, - precision("base_amount", item)); + item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item)); net_total += item.net_amount; // discount amount rounding loss adjustment if no taxes From 705a26a2fa5d1dc20c07dd4150771529c707e820 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 9 Nov 2024 15:14:21 +0530 Subject: [PATCH 14/48] fix: better gls for purchases with tax witholding (#42743) * fix: better gls for purchases with tax witholding * test: test case for purchase invoice gl entries with tax witholding * fix: use flag `_skip_merge` instead of skipping merge based on against account * test: fix test `test_single_threshold_tds` for newer implementation (cherry picked from commit e3cd6539c34891fae6aab3012b8c13145ccc84ec) --- .../purchase_invoice/purchase_invoice.py | 81 +++++++++++++------ .../purchase_invoice/test_purchase_invoice.py | 55 +++++++++++++ .../test_tax_withholding_category.py | 10 ++- erpnext/accounts/general_ledger.py | 4 + 4 files changed, 123 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b46fb9e9b30..b47e90eb77d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -863,6 +863,7 @@ class PurchaseInvoice(BuyingController): self.make_tax_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) + self.make_gl_entries_for_tax_withholding(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) @@ -896,32 +897,37 @@ class PurchaseInvoice(BuyingController): ) if grand_total and not self.is_internal_transfer(): - against_voucher = self.name - if self.is_return and self.return_against and not self.update_outstanding_for_self: - against_voucher = self.return_against + self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total) - # Did not use base_grand_total to book rounding loss gle - gl_entries.append( - self.get_gl_dict( - { - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "due_date": self.due_date, - "against": self.against_expense_account, - "credit": base_grand_total, - "credit_in_account_currency": base_grand_total - if self.party_account_currency == self.company_currency - else grand_total, - "against_voucher": against_voucher, - "against_voucher_type": self.doctype, - "project": self.project, - "cost_center": self.cost_center, - }, - self.party_account_currency, - item=self, - ) - ) + def add_supplier_gl_entry( + self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False + ): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + + # Did not use base_grand_total to book rounding loss gle + gl = { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "due_date": self.due_date, + "against": against_account or self.against_expense_account, + "credit": base_grand_total, + "credit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": against_voucher, + "against_voucher_type": self.doctype, + "project": self.project, + "cost_center": self.cost_center, + "_skip_merge": skip_merge, + } + + if remarks: + gl["remarks"] = remarks + + gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self)) def make_item_gl_entries(self, gl_entries): # item gl entries @@ -1413,6 +1419,31 @@ class PurchaseInvoice(BuyingController): ) ) + def make_gl_entries_for_tax_withholding(self, gl_entries): + """ + Tax withholding amount is not part of supplier invoice. + Separate supplier GL Entry for correct reporting. + """ + if not self.apply_tds: + return + + for row in self.get("taxes"): + if not row.is_tax_withholding_account or not row.tax_amount: + continue + + base_tds_amount = row.base_tax_amount_after_discount_amount + tds_amount = row.tax_amount_after_discount_amount + + self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount) + self.add_supplier_gl_entry( + gl_entries, + -base_tds_amount, + -tds_amount, + against_account=row.account_head, + remarks=_("TDS Deducted"), + skip_merge=True, + ) + def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries if cint(self.is_paid) and self.cash_bank_account and self.paid_amount: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index cf5bfedaebd..f5835deb0d0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1544,6 +1544,61 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): payment_entry.load_from_db() self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def test_purchase_gl_with_tax_withholding_tax(self): + company = "_Test Company" + + tds_account_args = { + "doctype": "Account", + "account_name": "TDS Payable", + "account_type": "Tax", + "parent_account": frappe.db.get_value( + "Account", {"account_name": "Duties and Taxes", "company": company} + ), + "company": company, + } + + tds_account = create_account(**tds_account_args) + tax_withholding_category = "Test TDS - 194 - Dividends - Individual" + + # Update tax withholding category with current fiscal year and rate details + create_tax_witholding_category(tax_withholding_category, company, tds_account) + + # create a new supplier to test + supplier = create_supplier( + supplier_name="_Test TDS Advance Supplier", + tax_withholding_category=tax_withholding_category, + ) + + pi = make_purchase_invoice( + supplier=supplier.name, + rate=3000, + qty=1, + item="_Test Non Stock Item", + do_not_submit=1, + ) + pi.apply_tds = 1 + pi.tax_withholding_category = tax_withholding_category + pi.save() + pi.submit() + + self.assertEqual(pi.taxes[0].tax_amount, 300) + self.assertEqual(pi.taxes[0].account_head, tds_account) + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"}, + fields=["account", "against", "debit", "credit"], + ) + + for gle in gl_entries: + if gle.debit: + # GL Entry with TDS Amount + self.assertEqual(gle.against, tds_account) + self.assertEqual(gle.debit, 300) + else: + # GL Entry with Purchase Invoice Amount + self.assertEqual(gle.credit, 3000) + def test_provisional_accounting_entry(self): setup_provisional_accounting() diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index c8893a13e86..2b7ae5fd689 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -74,11 +74,17 @@ class TestTaxWithholdingCategory(FrappeTestCase): self.assertEqual(pi.grand_total, 18000) # check gl entry for the purchase invoice - gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"]) + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pi.name}, + fields=["account", "sum(debit) as debit", "sum(credit) as credit"], + group_by="account", + ) self.assertEqual(len(gl_entries), 3) for d in gl_entries: if d.account == pi.credit_to: - self.assertEqual(d.credit, 18000) + self.assertEqual(d.credit, 20000) + self.assertEqual(d.debit, 2000) elif d.account == pi.items[0].get("expense_account"): self.assertEqual(d.debit, 20000) elif d.account == pi.taxes[0].get("account_head"): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 856e2b96af0..f9b503675aa 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -234,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None): merge_properties = get_merge_properties(accounting_dimensions) for entry in gl_map: + if entry._skip_merge: + merged_gl_map.append(entry) + continue + entry.merge_key = get_merge_key(entry, merge_properties) # if there is already an entry in this account then just add it # to that entry From e6894b949c9058ac0141d57dd4b6a5527c656f62 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Mon, 11 Nov 2024 05:46:17 +0100 Subject: [PATCH 15/48] fix: when company is created with other company template Chart of Account the Create Taxe Template failed (#42755) fix: when company if create with other company template Created Template Taxe failed (cherry picked from commit 8383883977783d33f509cf3d34b40f388a2b32a7) --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 384673448b0..0faebb6ab4c 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -86,7 +86,10 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" - coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts") + charts_company_name = company_name + if frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on"): + charts_company_name = frappe.db.get_value("Company", company_name, "existing_company") + coa_name = frappe.db.get_value("Company", charts_company_name, "chart_of_accounts") coa_data = data.get("chart_of_accounts", {}) tax_templates = coa_data.get(coa_name) or coa_data.get("*", {}) tax_categories = data.get("tax_categories") From 865786e0b67fb35d2423b4cf281dd76cdca3bd51 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Mon, 11 Nov 2024 05:47:51 +0100 Subject: [PATCH 16/48] feat: add template taxe for charts of account France - Plan Comptable General avec code (#42757) * feat: add template taxe for charts of account France - Plan Comptable General avec code * feat: add template taxe for charts of account France - Plan Comptable General avec code * feat: add template taxe for charts of account France - Plan Comptable General avec code * feat: add template taxe for charts of account France - Plan Comptable General avec code * feat: add template taxe for charts of account France - Plan Comptable General avec code (cherry picked from commit 1fe6efdeb9ef7a2d100a536a099b6de8b003e416) --- .../setup_wizard/data/country_wise_tax.json | 361 +++++++++++++++++- 1 file changed, 349 insertions(+), 12 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index a746ebee7ed..efe1d705c51 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -449,18 +449,355 @@ }, "France": { - "France VAT 20%": { - "account_name": "VAT 20%", - "tax_rate": 20, - "default": 1 - }, - "France VAT 10%": { - "account_name": "VAT 10%", - "tax_rate": 10 - }, - "France VAT 5.5%": { - "account_name": "VAT 5.5%", - "tax_rate": 5.5 + "chart_of_accounts": { + "France - Plan Comptable General avec code": { + "sales_tax_templates": [ + { + "title": "TVA 20% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 20% Collectée", + "account_number": "445720", + "root_type": "Liability", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "rate": 20 + } + ] + }, + { + "title": "TVA 10% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 10% Collectée", + "account_number": "445710", + "root_type": "Liability", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 5.5% Collectée", + "account_number": "445755", + "root_type": "Liability", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 2.1% Collectée", + "account_number": "445721", + "root_type": "Liability", + "tax_rate": 2.10 + }, + "description": "TVA 2.1%", + "rate": 2.1 + } + ] + } + ], + "purchase_tax_templates": [ + { + "title": "TVA 20% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 20% Déductible", + "account_number": "445620", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "rate": 20 + } + ] + }, + { + "title": "TVA 10% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 10% Déductible", + "account_number": "445610", + "root_type": "Asset", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 5.5% Déductible", + "account_number": "445655", + "root_type": "Asset", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 2.1% Déductible", + "account_number": "445621", + "root_type": "Asset", + "tax_rate": 2.1 + }, + "description": "TVA 2.1%", + "rate": 2.1 + } + ] + }, + { + "title": "TVA 20% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 20% Déductible", + "account_number": "445620", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "included_in_print_rate": 1, + "description": "TVA 20%", + "rate": 20 + } + ] + }, + { + "title": "TVA 10% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 10% Déductible", + "account_number": "445610", + "root_type": "Asset", + "tax_rate": 10.0 + }, + "included_in_print_rate": 1, + "description": "TVA 10%", + "rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 5.5% Déductible", + "account_number": "445655", + "root_type": "Asset", + "tax_rate": 5.5 + }, + "included_in_print_rate": 1, + "description": "TVA 5.5%", + "rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 2.1% Déductible", + "account_number": "445621", + "root_type": "Asset", + "tax_rate": 2.1 + }, + "included_in_print_rate": 1, + "description": "TVA 2.1%", + "rate": 2.1 + } + ] + }, + { + "title": "TVA Intracommunautaire", + "tax_category": "Achat - EU", + "taxes": [ + { + "account_head": { + "account_name": "TVA déductible sur acquisition intracommunautaires", + "account_number": "445662", + "root_type": "Asset", + "tax_rate": 20.0, + "add_deduct_tax": "Add" + }, + "description": "TVA déductible sur acquisition intracommunautaires", + "rate": 20 + }, + { + "account_head": { + "account_name": "TVA due intracommunautaire", + "account_number": "445200", + "root_type": "Asset", + "tax_rate": 20.0, + "add_deduct_tax": "Deduct" + }, + "description": "TVA due intracommunautaire", + "rate": 20 + } + ] + } + ], + "item_tax_templates": [ + { + "title": "TVA 20% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 20% Déductible", + "account_number": "445620", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "tax_rate": 20 + } + ] + }, + { + "title": "TVA 10% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 10% Déductible", + "account_number": "445610", + "root_type": "Asset", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "tax_rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 5.5% Déductible", + "account_number": "445655", + "root_type": "Asset", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "tax_rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 2.1% Déductible", + "account_number": "445621", + "root_type": "Asset", + "tax_rate": 2.1 + }, + "description": "TVA 2.1%", + "tax_rate": 2.1 + } + ] + }, + { + "title": "TVA 20% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 20% Collecté", + "account_number": "445720", + "root_type": "Liability", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "tax_rate": 20 + } + ] + }, + { + "title": "TVA 10% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 10% Collecté", + "account_number": "445710", + "root_type": "Liability", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "tax_rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 5.5% Collecté", + "account_number": "445755", + "root_type": "Liability", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "tax_rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 2.1% Collecté", + "account_number": "445721", + "root_type": "Liability", + "tax_rate": 2.1 + }, + "description": "TVA 2.1%", + "tax_rate": 2.1 + } + ] + } + ] + } } }, From 3e29ae85345fc8c3ec0851987804f3886852097f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 11 Nov 2024 11:12:33 +0530 Subject: [PATCH 17/48] fix: slow reposting due to SABB update (cherry picked from commit 2447b3f424fb26d1a72676ac439c1b2c28cfeb11) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5993580032f..f362f9d3da9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -795,9 +795,6 @@ class StockEntry(StockController): self.set_total_incoming_outgoing_value() self.set_total_amount() - if not reset_outgoing_rate: - self.set_serial_and_batch_bundle() - def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): """ Set rate for outgoing, scrapped and finished items From 7d098328d0bc6e74aca03e505f3c31d845c38d9b Mon Sep 17 00:00:00 2001 From: vishnu Date: Sun, 10 Nov 2024 06:55:16 +0000 Subject: [PATCH 18/48] fix: tyeerror while saving pick list (cherry picked from commit 22de0ecbdcb34ffde91b0788c66e6bb8beb18a17) --- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 27a37ef1bab..ba1cc228bfa 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -109,7 +109,7 @@ class PickList(Document): "actual_qty", ) - if row.qty > bin_qty: + if row.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}." From 65ec7c56041612c17dce79def87ce31ee503f391 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:27:26 +0530 Subject: [PATCH 19/48] fix: add default height to POS item card selector (backport #44071) (#44075) fix: add default height to POS item card selector (cherry picked from commit 5f5a514d6f7845be1a729427561e040b16ba4b5e) Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com> --- erpnext/selling/page/point_of_sale/pos_item_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b5fa8849d60..862dfd54e5c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -99,7 +99,7 @@ erpnext.PointOfSale.ItemSelector = class { return `
${qty_to_display}
-
+
Date: Mon, 11 Nov 2024 12:51:48 +0530 Subject: [PATCH 20/48] chore: update `CODEOWNERS` (backport #44074) (#44081) chore: update `CODEOWNERS` (#44074) (cherry picked from commit 9a758ea826c091dcd298b19d50d8684e0147beae) Co-authored-by: s-aga-r --- CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9077c6783c7..4a19fc871b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,21 +4,21 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @anandbaburajan @deepeshgarg007 +erpnext/assets/ @khushi8112 @deepeshgarg007 erpnext/regional @deepeshgarg007 @ruthra-kumar erpnext/selling @deepeshgarg007 @ruthra-kumar erpnext/support/ @deepeshgarg007 pos* -erpnext/buying/ @rohitwaghchaure @s-aga-r -erpnext/maintenance/ @rohitwaghchaure @s-aga-r -erpnext/manufacturing/ @rohitwaghchaure @s-aga-r -erpnext/quality_management/ @rohitwaghchaure @s-aga-r -erpnext/stock/ @rohitwaghchaure @s-aga-r -erpnext/subcontracting @rohitwaghchaure @s-aga-r +erpnext/buying/ @rohitwaghchaure +erpnext/maintenance/ @rohitwaghchaure +erpnext/manufacturing/ @rohitwaghchaure +erpnext/quality_management/ @rohitwaghchaure +erpnext/stock/ @rohitwaghchaure +erpnext/subcontracting @rohitwaghchaure erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure erpnext/patches/ @deepeshgarg007 .github/ @deepeshgarg007 -pyproject.toml @phot0n +pyproject.toml @akhilnarang From 34b5639d1c3a260f527ae02e2dc5ce05efd73e1c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:00:27 +0530 Subject: [PATCH 21/48] fix: task path (backport #44073) (#44078) fix: task path (#44073) (cherry picked from commit 8c99acb1b905d57515c42db3e24d480f96048aa1) Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- erpnext/projects/doctype/task/task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index c03c99b25d5..5eae55d71c5 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -153,14 +153,14 @@ class Task(NestedSet): def validate_parent_template_task(self): if self.parent_task: if not frappe.db.get_value("Task", self.parent_task, "is_template"): - parent_task_format = f"""{self.parent_task}""" + parent_task_format = f"""{self.parent_task}""" frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) def validate_depends_on_tasks(self): if self.depends_on: for task in self.depends_on: if not frappe.db.get_value("Task", task.task, "is_template"): - dependent_task_format = f"""{task.task}""" + dependent_task_format = f"""{task.task}""" frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) def validate_completed_on(self): From 363f15124eaa857d112b04ae7429b11360015215 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:12:30 +0530 Subject: [PATCH 22/48] fix: populate payment schedule from payment terms (backport #44082) (#44083) fix: populate payment schedule from payment terms (#44082) (cherry picked from commit c81eb6c824b04d8f174ece6900a5669b9a7c9461) Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1773586ac48..07ba90cd6d1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2446,7 +2446,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe payment_terms_template() { var me = this; const doc = this.frm.doc; - if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && doc.is_return == 0) { + if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && !doc.is_return) { var posting_date = doc.posting_date || doc.transaction_date; frappe.call({ method: "erpnext.controllers.accounts_controller.get_payment_terms", From be07421ab77b43681dffc7e038630b01ecc61984 Mon Sep 17 00:00:00 2001 From: Vishv-silveroak <108357657+Vishv-024@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:03:40 +0530 Subject: [PATCH 23/48] fix: exception on register reports when filtered on cost center 1 (cherry picked from commit f01e1a8e20d6fa32e48ff22d850e737be5e32e2b) --- erpnext/accounts/report/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index d6c1b95cf7c..2a72b10e4eb 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -255,7 +255,9 @@ def get_journal_entries(filters, args): ) .orderby(je.posting_date, je.name, order=Order.desc) ) - query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True) + query = apply_common_conditions( + filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True + ) journal_entries = query.run(as_dict=True) return journal_entries @@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment query = query.where(parent_doc.posting_date <= filters.to_date) if payments: - if filters.get("cost_center"): + if doctype == "Journal Entry" and filters.get("cost_center"): + query = query.where(child_doc.cost_center == filters.cost_center) + elif filters.get("cost_center"): query = query.where(parent_doc.cost_center == filters.cost_center) else: if filters.get("cost_center"): From 2183b993300124df7652c2add8ffaa1504b66825 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Nov 2024 12:08:05 +0530 Subject: [PATCH 24/48] test: basic report output (cherry picked from commit 657201b32485cc3471fc91fdb6b20c7d73abdf60) --- .../sales_register/test_sales_register.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 erpnext/accounts/report/sales_register/test_sales_register.py diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py new file mode 100644 index 00000000000..6ed754fa837 --- /dev/null +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -0,0 +1,64 @@ +import frappe +from frappe.tests import IntegrationTestCase +from frappe.utils import getdate, today + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + + def tearDown(self): + frappe.db.rollback() + + def create_sales_invoice(self, rate=100, do_not_submit=False): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=rate, + price_list_rate=rate, + do_not_save=1, + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def test_basic_report_output(self): + si = self.create_sales_invoice(rate=98) + + filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company}) + report = execute(filters) + + self.assertEqual(len(report[1]), 1) + + expected_result = { + "item_code": si.items[0].item_code, + "invoice": si.name, + "posting_date": getdate(), + "customer": si.customer, + "debit_to": si.debit_to, + "company": self.company, + "income_account": si.items[0].income_account, + "stock_qty": 1.0, + "stock_uom": si.items[0].stock_uom, + "rate": 98.0, + "amount": 98.0, + "total_tax": 0, + "total_other_charges": 0, + "total": 98.0, + "currency": "INR", + } + + report_output = {k: v for k, v in report[1][0].items() if k in expected_result} + self.assertDictEqual(report_output, expected_result) From 2affa60ea9e606cc63c8328be836cc1302b7541a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Nov 2024 12:18:25 +0530 Subject: [PATCH 25/48] test: journals with cost center (cherry picked from commit c255f34eead1cba9210df609b22eb21e643f3d40) --- .../sales_register/test_sales_register.py | 121 +++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 6ed754fa837..0bd67f39907 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -3,7 +3,7 @@ from frappe.tests import IntegrationTestCase from frappe.utils import getdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute +from erpnext.accounts.report.sales_register.sales_register import execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin @@ -12,10 +12,27 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): self.create_company() self.create_customer() self.create_item() + self.create_child_cost_center() def tearDown(self): frappe.db.rollback() + def create_child_cost_center(self): + cc_name = "South Wing" + if frappe.db.exists("Cost Center", cc_name): + cc = frappe.get_doc("Cost Center", cc_name) + else: + cc = frappe.get_doc( + { + "doctype": "Cost Center", + "parent_cost_center": self.cost_center, + "company": self.company, + "is_group": False, + } + ) + cc = cc.save() + self.south_cc = cc.name + def create_sales_invoice(self, rate=100, do_not_submit=False): si = create_sales_invoice( item=self.item, @@ -62,3 +79,105 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): report_output = {k: v for k, v in report[1][0].items() if k in expected_result} self.assertDictEqual(report_output, expected_result) + + def test_journal_with_cost_center_filter(self): + je1 = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Journal Entry", + "company": self.company, + "posting_date": getdate(), + "accounts": [ + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "credit_in_account_currency": 77, + "credit": 77, + "is_advance": "Yes", + "cost_center": self.south_cc, + }, + { + "account": self.cash, + "debit_in_account_currency": 77, + "debit": 77, + }, + ], + } + ) + je1.submit() + + je2 = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Journal Entry", + "company": self.company, + "posting_date": getdate(), + "accounts": [ + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "credit_in_account_currency": 98, + "credit": 98, + "is_advance": "Yes", + "cost_center": self.cost_center, + }, + { + "account": self.cash, + "debit_in_account_currency": 98, + "debit": 98, + }, + ], + } + ) + je2.submit() + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + "cost_center": self.cost_center, + } + ) + result = [x for x in execute(filters)[1] if x.voucher_no == je1.name] + expected_result = { + "voucher_type": je1.doctype, + "voucher_no": je1.name, + "posting_date": je1.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 77, + "cost_center": self.south_cc, + "credit": 77, + } + result_output = {k: v for k, v in result.items() if k in expected_result} + self.assertDictEqual(result_output, expected_result) + + # Without cost center filter + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + "cost_center": self.south_cc, + } + ) + result = [x for x in execute(filters)[1] if x.voucher_no == je2.name] + expected_result = { + "voucher_type": je2.doctype, + "voucher_no": je2.name, + "posting_date": je2.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 98, + "cost_center": self.south_cc, + "credit": 98, + } + result_output = {k: v for k, v in result.items() if k in expected_result} + self.assertDictEqual(result_output, expected_result) From 9724cefce8ee677865e2152ba6c4d867e0594692 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Nov 2024 12:34:54 +0530 Subject: [PATCH 26/48] refactor(test): fix incorrect assertion (cherry picked from commit d6030e71121f5c9d7d25c4d11310f4e56cc46833) --- .../sales_register/test_sales_register.py | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 0bd67f39907..dbb4cd89851 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -60,21 +60,14 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): self.assertEqual(len(report[1]), 1) expected_result = { - "item_code": si.items[0].item_code, - "invoice": si.name, - "posting_date": getdate(), - "customer": si.customer, - "debit_to": si.debit_to, - "company": self.company, - "income_account": si.items[0].income_account, - "stock_qty": 1.0, - "stock_uom": si.items[0].stock_uom, - "rate": 98.0, - "amount": 98.0, - "total_tax": 0, - "total_other_charges": 0, - "total": 98.0, - "currency": "INR", + "voucher_type": si.doctype, + "voucher_no": si.name, + "posting_date": si.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 98, + "grand_total": 98, + "credit": 98, } report_output = {k: v for k, v in report[1][0].items() if k in expected_result} @@ -151,13 +144,12 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): "customer": self.customer, "receivable_account": self.debit_to, "net_total": 77, - "cost_center": self.south_cc, + "cost_center": self.cost_center, "credit": 77, } result_output = {k: v for k, v in result.items() if k in expected_result} self.assertDictEqual(result_output, expected_result) - # Without cost center filter filters = frappe._dict( { "from_date": today(), From d0e5568010969c01eae67ca9434f528c05185984 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Nov 2024 13:06:09 +0530 Subject: [PATCH 27/48] refactor(test): pass all mandatory fields (cherry picked from commit c53e9637dd1ee39ebfeb713f39e0eaafb414e5c7) --- erpnext/accounts/report/sales_register/test_sales_register.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index dbb4cd89851..cb8f37a4f0f 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -28,6 +28,7 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): "parent_cost_center": self.cost_center, "company": self.company, "is_group": False, + "cost_center_name": cc_name, } ) cc = cc.save() From 2de9292ac0d93b1a904fdd46377631dead44c972 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Nov 2024 14:16:49 +0530 Subject: [PATCH 28/48] refactor(test): assertion refactoring and exact decimals (cherry picked from commit 1d11131afeef9040ad98647c834daf2faafcb782) --- .../sales_register/test_sales_register.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index cb8f37a4f0f..50f12e52e33 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -22,12 +22,13 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): if frappe.db.exists("Cost Center", cc_name): cc = frappe.get_doc("Cost Center", cc_name) else: + parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center") cc = frappe.get_doc( { "doctype": "Cost Center", - "parent_cost_center": self.cost_center, "company": self.company, "is_group": False, + "parent_cost_center": parent, "cost_center_name": cc_name, } ) @@ -63,12 +64,12 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): expected_result = { "voucher_type": si.doctype, "voucher_no": si.name, - "posting_date": si.posting_date, + "posting_date": getdate(), "customer": self.customer, "receivable_account": self.debit_to, - "net_total": 98, - "grand_total": 98, - "credit": 98, + "net_total": 98.0, + "grand_total": 98.0, + "debit": 98.0, } report_output = {k: v for k, v in report[1][0].items() if k in expected_result} @@ -89,7 +90,7 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): "credit_in_account_currency": 77, "credit": 77, "is_advance": "Yes", - "cost_center": self.south_cc, + "cost_center": self.cost_center, }, { "account": self.cash, @@ -115,7 +116,7 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): "credit_in_account_currency": 98, "credit": 98, "is_advance": "Yes", - "cost_center": self.cost_center, + "cost_center": self.south_cc, }, { "account": self.cash, @@ -137,19 +138,20 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): "cost_center": self.cost_center, } ) - result = [x for x in execute(filters)[1] if x.voucher_no == je1.name] + report_output = execute(filters)[1] + filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name] + self.assertEqual(len(filtered_output), 1) expected_result = { "voucher_type": je1.doctype, "voucher_no": je1.name, "posting_date": je1.posting_date, "customer": self.customer, "receivable_account": self.debit_to, - "net_total": 77, - "cost_center": self.cost_center, - "credit": 77, + "net_total": 77.0, + "credit": 77.0, } - result_output = {k: v for k, v in result.items() if k in expected_result} - self.assertDictEqual(result_output, expected_result) + result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result} + self.assertDictEqual(result_fields, expected_result) filters = frappe._dict( { @@ -161,16 +163,17 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): "cost_center": self.south_cc, } ) - result = [x for x in execute(filters)[1] if x.voucher_no == je2.name] + report_output = execute(filters)[1] + filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name] + self.assertEqual(len(filtered_output), 1) expected_result = { "voucher_type": je2.doctype, "voucher_no": je2.name, "posting_date": je2.posting_date, "customer": self.customer, "receivable_account": self.debit_to, - "net_total": 98, - "cost_center": self.south_cc, - "credit": 98, + "net_total": 98.0, + "credit": 98.0, } - result_output = {k: v for k, v in result.items() if k in expected_result} + result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result} self.assertDictEqual(result_output, expected_result) From 2bce7353008573832c110050534ec9f5f6799a32 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Nov 2024 15:06:55 +0530 Subject: [PATCH 29/48] chore: use FrappeTestCase --- erpnext/accounts/report/sales_register/test_sales_register.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 50f12e52e33..693c33c0ca6 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -1,5 +1,5 @@ import frappe -from frappe.tests import IntegrationTestCase +from frappe.tests.utils import FrappeTestCase from frappe.utils import getdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -7,7 +7,7 @@ from erpnext.accounts.report.sales_register.sales_register import execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase): +class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() From 9ac54f694c27323baca073f6d790497d1c056507 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 11 Nov 2024 16:07:16 +0530 Subject: [PATCH 30/48] fix: item mapping from modal to batch form (#44090) (cherry picked from commit 9223ef2f3744fbd4f6962040b403b26a5925e67d) --- erpnext/public/js/utils/serial_no_batch_selector.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 8329e5c9d55..61b40d6fef0 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -437,6 +437,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fieldname: "batch_no", label: __("Batch No"), in_list_view: 1, + get_route_options_for_new_doc: () => { + return { + item: this.item.item_code, + }; + }, change() { let doc = this.doc; if (!doc.qty && me.item.type_of_transaction === "Outward") { From f387a8fcebbd5958e50a010b05c0c53d9d98328d Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Mon, 11 Nov 2024 07:38:30 -0300 Subject: [PATCH 31/48] fix: add field conversion_factor when include_uom is settled (#43701) (cherry picked from commit cfe6af1f6815bc420d7b74e2122c3cb3d2fe6463) --- erpnext/stock/report/stock_projected_qty/stock_projected_qty.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 743656c6472..9b4520064d6 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -297,6 +297,7 @@ def get_item_map(item_code, include_uom): if include_uom: ucd = frappe.qb.DocType("UOM Conversion Detail") + query = query.select(ucd.conversion_factor) query = query.left_join(ucd).on((ucd.parent == item.name) & (ucd.uom == include_uom)) items = query.run(as_dict=True) From b314f3839bfd19f81eac6d712a343d837fd4cb0f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:12:01 +0530 Subject: [PATCH 32/48] fix: Negative stock validation against inventory dimension (backport #43834) (#43846) fix: Negative stock validation against inventory dimension (#43834) (cherry picked from commit c330a292d2c473e5f0213ce5d5b15ed9928e65aa) Co-authored-by: Nabin Hait --- .../test_inventory_dimension.py | 21 ++++-- .../stock_ledger_entry/stock_ledger_entry.py | 70 +++++++++---------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 8ce954d55e6..918399a7f66 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import InventoryDimensionNegativeStockError from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -426,39 +427,49 @@ class TestInventoryDimension(FrappeTestCase): warehouse = create_warehouse("Negative Stock Warehouse") + # Try issuing 10 qty, more than available stock against inventory dimension doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True) doc.items[0].inv_site = "Site 1" - self.assertRaises(frappe.ValidationError, doc.submit) + self.assertRaises(InventoryDimensionNegativeStockError, doc.submit) + + # cancel the stock entry doc.reload() if doc.docstatus == 1: doc.cancel() + # Receive 10 qty against inventory dimension doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) - doc.items[0].to_inv_site = "Site 1" doc.submit() + # check inventory dimension value in stock ledger entry site_name = frappe.get_all( "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] )[0].inv_site self.assertEqual(site_name, "Site 1") + # Receive another 100 qty without inventory dimension + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=100) + + # Try issuing 100 qty, more than available stock against inventory dimension + # Note: total available qty for the item is 110, but against inventory dimension, only 10 qty is available doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) - doc.items[0].inv_site = "Site 1" - self.assertRaises(frappe.ValidationError, doc.submit) + self.assertRaises(InventoryDimensionNegativeStockError, doc.submit) + # disable validate_negative_stock for inventory dimension inv_dimension.reload() inv_dimension.db_set("validate_negative_stock", 0) frappe.local.inventory_dimensions = {} + # Try issuing 100 qty, more than available stock against inventory dimension doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) - doc.items[0].inv_site = "Site 1" doc.submit() self.assertEqual(doc.docstatus, 1) + # check inventory dimension value in stock ledger entry site_name = frappe.get_all( "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] )[0].inv_site diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 319303dbbb0..5aeabeeec56 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -8,6 +8,7 @@ import frappe from frappe import _, bold from frappe.core.doctype.role.role import get_users from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year @@ -25,6 +26,10 @@ class BackDatedStockTransaction(frappe.ValidationError): pass +class InventoryDimensionNegativeStockError(frappe.ValidationError): + pass + + exclude_from_linked_with = True @@ -104,61 +109,56 @@ class StockLedgerEntry(Document): self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time) def validate_inventory_dimension_negative_stock(self): - if self.is_cancelled: + if self.is_cancelled or self.actual_qty >= 0: return - extra_cond = "" - kwargs = {} - dimensions = self._get_inventory_dimensions() if not dimensions: return - for dimension, values in dimensions.items(): - kwargs[dimension] = values.get("value") - extra_cond += f" and {dimension} = %({dimension})s" - - kwargs.update( - { - "item_code": self.item_code, - "warehouse": self.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "company": self.company, - "sle": self.name, - } - ) - - sle = get_previous_sle(kwargs, extra_cond=extra_cond) - qty_after_transaction = 0.0 flt_precision = cint(frappe.db.get_default("float_precision")) or 2 - if sle: - qty_after_transaction = sle.qty_after_transaction + for dimension, values in dimensions.items(): + dimension_value = values.get("value") + available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value) - diff = qty_after_transaction + flt(self.actual_qty) - diff = flt(diff, flt_precision) - if diff < 0 and abs(diff) > 0.0001: - self.throw_validation_error(diff, dimensions) + diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimension, dimension_value) - def throw_validation_error(self, diff, dimensions): - dimension_msg = _(", with the inventory {0}: {1}").format( - "dimensions" if len(dimensions) > 1 else "dimension", - ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()), - ) + def get_available_qty_after_prev_transaction(self, dimension, dimension_value): + sle = frappe.qb.DocType("Stock Ledger Entry") + available_qty = ( + frappe.qb.from_(sle) + .select(Sum(sle.actual_qty)) + .where( + (sle.item_code == self.item_code) + & (sle.warehouse == self.warehouse) + & (sle.posting_datetime < self.posting_datetime) + & (sle.company == self.company) + & (sle.is_cancelled == 0) + & (sle[dimension] == dimension_value) + ) + ).run() + return available_qty[0][0] or 0 + + def throw_validation_error(self, diff, dimension, dimension_value): msg = _( - "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction." + "{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction." ).format( abs(diff), frappe.get_desk_link("Item", self.item_code), frappe.get_desk_link("Warehouse", self.warehouse), - dimension_msg, + frappe.bold(dimension), + frappe.bold(dimension_value), self.posting_date, self.posting_time, frappe.get_desk_link(self.voucher_type, self.voucher_no), ) - frappe.throw(msg, title=_("Inventory Dimension Negative Stock")) + frappe.throw( + msg, title=_("Inventory Dimension Negative Stock"), exc=InventoryDimensionNegativeStockError + ) def _get_inventory_dimensions(self): inv_dimensions = get_inventory_dimensions() From 762f3bac65f05d66a1f2a09d1805686128221d6f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 11 Nov 2024 15:59:40 +0530 Subject: [PATCH 33/48] chore: filter report output on document name --- erpnext/accounts/report/sales_register/test_sales_register.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 693c33c0ca6..95aa5add24c 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -59,7 +59,7 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company}) report = execute(filters) - self.assertEqual(len(report[1]), 1) + res = [x for x in report[1] if x.get("voucher_no") == si.name] expected_result = { "voucher_type": si.doctype, @@ -72,7 +72,7 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): "debit": 98.0, } - report_output = {k: v for k, v in report[1][0].items() if k in expected_result} + report_output = {k: v for k, v in res[0].items() if k in expected_result} self.assertDictEqual(report_output, expected_result) def test_journal_with_cost_center_filter(self): From ba6e068abc2a158de251525758032ba18f7ae0d9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Nov 2024 17:16:52 +0530 Subject: [PATCH 34/48] refactor(test): update tests for new rounding logic --- .../doctype/pos_invoice/test_pos_invoice.py | 2 +- .../test_pos_invoice_merge_log.py | 2 +- .../sales_invoice/test_sales_invoice.py | 65 +++++++++++-------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index f210a6434cf..1dbc630e62e 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase): inv.save() - self.assertEqual(inv.net_total, 4298.25) + self.assertEqual(inv.net_total, 4298.24) self.assertEqual(inv.grand_total, 4900.00) def test_tax_calculation_with_multiple_items(self): diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 20f6ddb5ba9..904d8e83b9c 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -343,7 +343,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv.load_from_db() consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) self.assertEqual(consolidated_invoice.status, "Return") - self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001) + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002) finally: frappe.set_user("Administrator") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index db6fd41e439..56f90ae8cd4 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -314,7 +314,8 @@ class TestSalesInvoice(FrappeTestCase): si.insert() # with inclusive tax - self.assertEqual(si.items[0].net_amount, 3947.368421052631) + self.assertEqual(si.items[0].net_amount, 3947.37) + self.assertEqual(si.net_total, si.base_net_total) self.assertEqual(si.net_total, 3947.37) self.assertEqual(si.grand_total, 5000) @@ -658,7 +659,7 @@ class TestSalesInvoice(FrappeTestCase): 62.5, 625.0, 50, - 499.97600115194473, + 499.98, ], "_Test Item Home Desktop 200": [ 190.66, @@ -669,7 +670,7 @@ class TestSalesInvoice(FrappeTestCase): 190.66, 953.3, 150, - 749.9968530500239, + 750, ], } @@ -682,20 +683,21 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(d.get(k), expected_values[d.item_code][i]) # check net total - self.assertEqual(si.net_total, 1249.97) + self.assertEqual(si.base_net_total, si.net_total) + self.assertEqual(si.net_total, 1249.98) self.assertEqual(si.total, 1578.3) # check tax calculation expected_values = { "keys": ["tax_amount", "total"], - "_Test Account Excise Duty - _TC": [140, 1389.97], - "_Test Account Education Cess - _TC": [2.8, 1392.77], - "_Test Account S&H Education Cess - _TC": [1.4, 1394.17], - "_Test Account CST - _TC": [27.88, 1422.05], - "_Test Account VAT - _TC": [156.25, 1578.30], - "_Test Account Customs Duty - _TC": [125, 1703.30], - "_Test Account Shipping Charges - _TC": [100, 1803.30], - "_Test Account Discount - _TC": [-180.33, 1622.97], + "_Test Account Excise Duty - _TC": [140, 1389.98], + "_Test Account Education Cess - _TC": [2.8, 1392.78], + "_Test Account S&H Education Cess - _TC": [1.4, 1394.18], + "_Test Account CST - _TC": [27.88, 1422.06], + "_Test Account VAT - _TC": [156.25, 1578.31], + "_Test Account Customs Duty - _TC": [125, 1703.31], + "_Test Account Shipping Charges - _TC": [100, 1803.31], + "_Test Account Discount - _TC": [-180.33, 1622.98], } for d in si.get("taxes"): @@ -731,7 +733,7 @@ class TestSalesInvoice(FrappeTestCase): "base_rate": 2500, "base_amount": 25000, "net_rate": 40, - "net_amount": 399.9808009215558, + "net_amount": 399.98, "base_net_rate": 2000, "base_net_amount": 19999, }, @@ -745,7 +747,7 @@ class TestSalesInvoice(FrappeTestCase): "base_rate": 7500, "base_amount": 37500, "net_rate": 118.01, - "net_amount": 590.0531205155963, + "net_amount": 590.05, "base_net_rate": 5900.5, "base_net_amount": 29502.5, }, @@ -783,8 +785,13 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(si.base_grand_total, 60795) self.assertEqual(si.grand_total, 1215.90) - self.assertEqual(si.rounding_adjustment, 0.01) - self.assertEqual(si.base_rounding_adjustment, 0.50) + # no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01 + if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01: + self.assertEqual(si.rounding_adjustment, 0.10) + self.assertEqual(si.base_rounding_adjustment, 5.0) + else: + self.assertEqual(si.rounding_adjustment, 0.0) + self.assertEqual(si.base_rounding_adjustment, 0.0) def test_outstanding(self): w = self.make() @@ -2172,7 +2179,7 @@ class TestSalesInvoice(FrappeTestCase): def test_rounding_adjustment_2(self): si = create_sales_invoice(rate=400, do_not_save=True) - for rate in [400, 600, 100]: + for rate in [400.25, 600.30, 100.65]: si.append( "items", { @@ -2198,18 +2205,19 @@ class TestSalesInvoice(FrappeTestCase): ) si.save() si.submit() - self.assertEqual(si.net_total, 1271.19) - self.assertEqual(si.grand_total, 1500) - self.assertEqual(si.total_taxes_and_charges, 228.82) - self.assertEqual(si.rounding_adjustment, -0.01) + self.assertEqual(si.net_total, si.base_net_total) + self.assertEqual(si.net_total, 1272.20) + self.assertEqual(si.grand_total, 1501.20) + self.assertEqual(si.total_taxes_and_charges, 229) + self.assertEqual(si.rounding_adjustment, -0.20) round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account") expected_values = { - "_Test Account Service Tax - _TC": [0.0, 114.41], - "_Test Account VAT - _TC": [0.0, 114.41], - si.debit_to: [1500, 0.0], - round_off_account: [0.01, 0.01], - "Sales - _TC": [0.0, 1271.18], + "_Test Account Service Tax - _TC": [0.0, 114.50], + "_Test Account VAT - _TC": [0.0, 114.50], + si.debit_to: [1501, 0.0], + round_off_account: [0.20, 0.0], + "Sales - _TC": [0.0, 1272.20], } gl_entries = frappe.db.sql( @@ -2267,7 +2275,8 @@ class TestSalesInvoice(FrappeTestCase): si.save() si.submit() - self.assertEqual(si.net_total, 4007.16) + self.assertEqual(si.net_total, si.base_net_total) + self.assertEqual(si.net_total, 4007.15) self.assertEqual(si.grand_total, 4488.02) self.assertEqual(si.total_taxes_and_charges, 480.86) self.assertEqual(si.rounding_adjustment, -0.02) @@ -2280,7 +2289,7 @@ class TestSalesInvoice(FrappeTestCase): ["_Test Account Service Tax - _TC", 0.0, 240.43], ["_Test Account VAT - _TC", 0.0, 240.43], ["Sales - _TC", 0.0, 4007.15], - [round_off_account, 0.02, 0.01], + [round_off_account, 0.01, 0.0], ] ) From 3423d3c13d3da909cdb1e165d04107b9bccee5d6 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:56:56 +0530 Subject: [PATCH 35/48] fix: bind this object explicitly on callback event function (cherry picked from commit 5e790a0fcea2e1e2f714aa8e312e9b9870b646e3) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index deb8bd7529d..6a537a2559a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -65,7 +65,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex super.refresh(); if (doc.docstatus == 1 && !doc.is_return) { - this.frm.add_custom_button(__("Return"), this.make_sales_return, __("Create")); + this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create")); this.frm.page.set_inner_btn_group_as_primary(__("Create")); } From 61559be8a46f410370c199140fbf261a559ce17e Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:21:48 +0530 Subject: [PATCH 36/48] fix: update payment amount for partial pos return (cherry picked from commit 53ef6336b62fab0dedc19b74dc1e5c293bf933c3) --- erpnext/public/js/controllers/taxes_and_totals.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8b2d7943772..ee78e493db6 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -844,13 +844,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ); } - if(!this.frm.doc.is_return){ - this.frm.doc.payments.find(payment => { - if (payment.default) { - payment.amount = total_amount_to_pay; - } - }); - } + this.frm.doc.payments.find(payment => { + if (payment.default) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0 + } + }); this.frm.refresh_fields(); } From 2754793ff9d5164b97607f4a977bda8a67313c61 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Sat, 9 Nov 2024 19:54:50 +0530 Subject: [PATCH 37/48] feat: Add item group filtering for search results (cherry picked from commit 5e7cf3899bb6d841f4e96f12e8aa08db120b8ab0) --- .../selling/page/point_of_sale/point_of_sale.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 fad184d152c..13430b0523d 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -92,6 +92,20 @@ def search_by_term(search_term, warehouse, price_list): return {"items": [item]} +def filter_result_items(result, pos_profile): + if result and result.get("items"): + pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group") + for i, item in enumerate(result.get("items")): + item_group = frappe.db.get_value( + "Item Group", frappe.db.get_value("Item", item.get("item_code"), "item_group"), "name" + ) + if item_group in pos_item_groups: + continue + else: + if result.get("items"): + result.get("items").pop(i) + + @frappe.whitelist() def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): warehouse, hide_unavailable_items = frappe.db.get_value( @@ -102,6 +116,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if search_term: result = search_by_term(search_term, warehouse, price_list) or [] + filter_result_items(result, pos_profile) if result: return result From aca157704058337ef5d09bccc0e4eafd23086c42 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Sat, 9 Nov 2024 19:55:50 +0530 Subject: [PATCH 38/48] refactor: Relocate doc variable for better scope management (cherry picked from commit 488b60fc278bd0093056df8ac0b699bd43b139db) --- erpnext/selling/page/point_of_sale/pos_item_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 862dfd54e5c..207a444218b 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -138,7 +138,6 @@ erpnext.PointOfSale.ItemSelector = class { make_search_bar() { const me = this; - const doc = me.events.get_frm().doc; this.$component.find(".search-field").html(""); this.$component.find(".item-group-field").html(""); @@ -163,6 +162,7 @@ erpnext.PointOfSale.ItemSelector = class { me.filter_items(); }, get_query: function () { + const doc = me.events.get_frm().doc; return { query: "erpnext.selling.page.point_of_sale.point_of_sale.item_group_query", filters: { From 55f7f63e6e12151d6ca383b6b2a5104282e32cfe Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 12 Nov 2024 11:39:30 +0530 Subject: [PATCH 39/48] refactor: simpler filtering (cherry picked from commit f072b1266e298b4717443a4b1ae6b17498bb7303) --- erpnext/selling/page/point_of_sale/point_of_sale.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 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 13430b0523d..28eadec9bc5 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -35,6 +35,7 @@ def search_by_term(search_term, warehouse, price_list): "description": item_doc.description, "is_stock_item": item_doc.is_stock_item, "item_code": item_doc.name, + "item_group": item_doc.item_group, "item_image": item_doc.image, "item_name": item_doc.item_name, "serial_no": serial_no, @@ -95,15 +96,7 @@ def search_by_term(search_term, warehouse, price_list): def filter_result_items(result, pos_profile): if result and result.get("items"): pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group") - for i, item in enumerate(result.get("items")): - item_group = frappe.db.get_value( - "Item Group", frappe.db.get_value("Item", item.get("item_code"), "item_group"), "name" - ) - if item_group in pos_item_groups: - continue - else: - if result.get("items"): - result.get("items").pop(i) + result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups] @frappe.whitelist() From e3d74684d53218d383f2338415f4493fa2a34263 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 12 Nov 2024 15:43:20 +0530 Subject: [PATCH 40/48] fix: purchase receipt creation from SCR (#44095) (cherry picked from commit 774845f886881592e80089161b658c70b352bd32) --- .../subcontracting_receipt/subcontracting_receipt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index a99f7ea1615..37cd43ac1f6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -766,7 +766,11 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False "postprocess": update_item, "condition": lambda doc: doc.name in po_sr_item_dict, }, - "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + "reset_value": True, + "condition": lambda doc: not doc.is_tax_withholding_account, + }, }, postprocess=post_process, ) From 4ba07a40eb45f6d75c1d6e0775104b998f6cf537 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 12 Nov 2024 15:48:28 +0530 Subject: [PATCH 41/48] fix: not able to reconcile expired batches (#44012) (cherry picked from commit 8805e74784ee423c7e279138c1f0486ce6e0766e) --- erpnext/controllers/queries.py | 10 ++++++++-- erpnext/public/js/utils/serial_no_batch_selector.js | 11 +++++++++++ .../serial_and_batch_bundle.py | 3 +++ erpnext/stock/doctype/stock_entry/stock_entry.js | 4 ++++ .../stock_reconciliation/stock_reconciliation.py | 12 +++++++++++- erpnext/stock/serial_batch_bundle.py | 4 ++-- erpnext/stock/stock_ledger.py | 1 + 7 files changed, 40 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 919bbb477f4..463cb859970 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -415,7 +415,6 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p stock_ledger_entry.batch_no, Sum(stock_ledger_entry.actual_qty).as_("qty"), ) - .where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) .where(stock_ledger_entry.is_cancelled == 0) .where( (stock_ledger_entry.item_code == filters.get("item_code")) @@ -428,6 +427,9 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p .limit(page_len) ) + if not filters.get("include_expired_batches"): + query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) + query = query.select( Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"), Concat("EXP-", batch_table.expiry_date).as_("expiry_date"), @@ -466,7 +468,6 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0 bundle.batch_no, Sum(bundle.qty).as_("qty"), ) - .where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) .where(stock_ledger_entry.is_cancelled == 0) .where( (stock_ledger_entry.item_code == filters.get("item_code")) @@ -479,6 +480,11 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0 .limit(page_len) ) + if not filters.get("include_expired_batches"): + bundle_query = bundle_query.where( + (batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()) + ) + bundle_query = bundle_query.select( Concat("MFG-", batch_table.manufacturing_date), Concat("EXP-", batch_table.expiry_date), diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 61b40d6fef0..de1faf36ef5 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -462,6 +462,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { is_inward = true; } + let include_expired_batches = me.include_expired_batches(); + return { query: "erpnext.controllers.queries.get_batch_no", filters: { @@ -469,6 +471,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { warehouse: this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse, is_inward: is_inward, + include_expired_batches: include_expired_batches, }, }; }, @@ -497,6 +500,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { return fields; } + include_expired_batches() { + return ( + this.frm.doc.doctype === "Stock Reconciliation" || + (this.frm.doc.doctype === "Stock Entry" && + ["Material Receipt", "Material Transfer", "Material Issue"].includes(this.frm.doc.purpose)) + ); + } + get_auto_data() { let { qty, based_on } = this.dialog.get_values(); diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ed6d5e155d7..08aa978aa99 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -689,6 +689,9 @@ class SerialandBatchBundle(Document): serial_batches = {} for row in self.entries: if not row.qty and row.batch_no and not row.serial_no: + if self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Inward": + continue + frappe.throw( _("At row {0}: Qty is mandatory for the batch {1}").format( bold(row.idx), bold(row.batch_no) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index ea47ffd79ea..0e39c2a9756 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -117,6 +117,10 @@ frappe.ui.form.on("Stock Entry", { filters["is_inward"] = 1; } + if (["Material Receipt", "Material Transfer", "Material Issue"].includes(doc.purpose)) { + filters["include_expired_batches"] = 1; + } + return { query: "erpnext.controllers.queries.get_batch_no", filters: filters, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 8ee3d9b3901..f671c11712a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -324,6 +324,7 @@ class StockReconciliation(StockController): row.item_code, posting_date=self.posting_date, posting_time=self.posting_time, + for_stock_levels=True, ) total_current_qty += current_qty @@ -1322,7 +1323,16 @@ def get_stock_balance_for( qty, rate = data if item_dict.get("has_batch_no"): - qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + qty = ( + get_batch_qty( + batch_no, + warehouse, + posting_date=posting_date, + posting_time=posting_time, + for_stock_levels=True, + ) + or 0 + ) return { "qty": qty, diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index c1002095b62..cb156aea119 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -124,7 +124,7 @@ class SerialBatchBundle: "Outward": self.sle.actual_qty < 0, }.get(sn_doc.type_of_transaction) - if not condition: + if not condition and self.sle.actual_qty: correct_type = "Inward" if sn_doc.type_of_transaction == "Inward": correct_type = "Outward" @@ -133,7 +133,7 @@ class SerialBatchBundle: frappe.throw(_(msg), title=_("Incorrect Type of Transaction")) precision = sn_doc.precision("total_qty") - if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision): + if self.sle.actual_qty and flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision): msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {link} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}" frappe.throw(_(msg)) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ccf7c7643c8..26e7af150b9 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1183,6 +1183,7 @@ class update_entries_after: stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: + # Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item. if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse): d.db_update() From 290bddea7777d73a3a31c18a255be8d8a39f3793 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:12:13 +0530 Subject: [PATCH 42/48] fix: not able to cancel DN (backport #44108) (#44109) fix: not able to cancel DN (#44108) (cherry picked from commit e8882718c9d99db01f3dbc16c477c4e295b9becb) Co-authored-by: rohitwaghchaure --- erpnext/stock/serial_batch_bundle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index cb156aea119..3fed0195d69 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -288,7 +288,7 @@ class SerialBatchBundle: "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus" ) - if docstatus != 1: + if docstatus == 0: self.submit_serial_and_batch_bundle() if self.item_details.has_serial_no == 1: @@ -311,7 +311,9 @@ class SerialBatchBundle: if self.is_pos_transaction(): return - frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel() + doc = frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle) + if doc.docstatus == 1: + doc.cancel() def is_pos_transaction(self): if ( From 6e83fec5ca5286bd36b150e1b5660d0e71c3d270 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:22:18 +0530 Subject: [PATCH 43/48] fix: consider service item cost in the RM cost of the BOM (backport #43962) (#44111) fix: consider service item cost in the RM cost of the BOM (#43962) (cherry picked from commit c0ffaa444cc870f1751468cdad8928515bbf26d6) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 843528de706..c3105b8e9ec 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -742,11 +742,8 @@ class BOM(WebsiteGenerator): base_total_rm_cost = 0 for d in self.get("items"): - if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate": - continue - old_rate = d.rate - if not self.bom_creator: + if not self.bom_creator and d.is_stock_item: d.rate = self.get_rm_rate( { "company": self.company, From a833dd67f35d12ea60ff5103eea89c5156975cea Mon Sep 17 00:00:00 2001 From: NaviN <118178330+Navin-S-R@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:23:16 +0530 Subject: [PATCH 44/48] fix: update per_billed value in Purchase Receipt while creating Debit Note (#43977) * fix: update per_billed value in Purchase Receipt while creating Debit Note * test: add unit test for validating per_billed value for partial Debit Note (cherry picked from commit 494fd7ceeab3edd56f7f4975101a611e9e8ad391) --- .../purchase_receipt/purchase_receipt.py | 11 ++++- .../purchase_receipt/test_purchase_receipt.py | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 228bc35693b..efe1628d8e0 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1059,6 +1059,8 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty + buying_settings = frappe.get_single("Buying Settings") + total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -1066,10 +1068,15 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount - total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt + if buying_settings.bill_for_rejected_quantity_in_purchase_invoice: + pending_amount = flt(item.amount) + + total_billable_amount = abs(flt(item.amount)) + if pending_amount > 0: + total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt total_amount += total_billable_amount - total_billed_amount += flt(item.billed_amt) + total_billed_amount += abs(flt(item.billed_amt)) if pr_doc.get("is_return") and not total_amount and total_billed_amount: total_amount = total_billed_amount diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 64c3d2c67b2..4b8d5101f43 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3900,6 +3900,54 @@ class TestPurchaseReceipt(FrappeTestCase): for incoming_rate in bundle_data: self.assertEqual(incoming_rate, 0) + def test_purchase_return_partial_debit_note(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + ) + + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-2, + do_not_submit=1, + ) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() + + # because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 2) + self.assertEqual(pr.per_returned, 40) + + # Create first partial debit_note + pi_1 = make_purchase_invoice(return_pr.name) + pi_1.items[0].qty = -1 + pi_1.submit() + + # Check if the first partial debit billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 50) + self.assertEqual(return_pr.status, "Partly Billed") + + # Create second partial debit_note to complete the debit note + pi_2 = make_purchase_invoice(return_pr.name) + pi_2.items[0].qty = -1 + pi_2.submit() + + # Check if the second partial debit note billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 100) + self.assertEqual(return_pr.status, "Completed") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From bce7acf9ccae317eafe2be77dc06440e3c44edd9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:31:31 +0530 Subject: [PATCH 45/48] fix: incorrect produced qty in Production Plan Summary (backport #44112) (#44113) fix: incorrect produced qty in Production Plan Summary (#44112) (cherry picked from commit 0828c74fe39fa4b77a48a7139fa9c07121cfa2d8) Co-authored-by: rohitwaghchaure --- .../production_plan/production_plan.js | 20 ++++---- .../production_plan_summary.py | 51 +++++++++++++------ 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index aba213ebca4..e7ac7f7cc83 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -87,17 +87,17 @@ frappe.ui.form.on("Production Plan", { if (frm.doc.docstatus === 1) { frm.trigger("show_progress"); - if (frm.doc.status !== "Completed") { - frm.add_custom_button( - __("Production Plan Summary"), - () => { - frappe.set_route("query-report", "Production Plan Summary", { - production_plan: frm.doc.name, - }); - }, - __("View") - ); + frm.add_custom_button( + __("Production Plan Summary"), + () => { + frappe.set_route("query-report", "Production Plan Summary", { + production_plan: frm.doc.name, + }); + }, + __("View") + ); + if (frm.doc.status !== "Completed") { if (frm.doc.status === "Closed") { frm.add_custom_button( __("Re-open"), diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 5bc9236c1d5..c62cab77d61 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -27,32 +27,51 @@ def get_data(filters): def get_production_plan_item_details(filters, data, order_details): - itemwise_indent = {} - production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan")) for row in production_plan_doc.po_items: - work_order = frappe.get_value( + work_orders = frappe.get_all( "Work Order", - {"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code}, - "name", + filters={ + "production_plan_item": row.name, + "bom_no": row.bom_no, + "production_item": row.item_code, + }, + pluck="name", ) - if row.item_code not in itemwise_indent: - itemwise_indent.setdefault(row.item_code, {}) + order_qty = row.planned_qty + total_produced_qty = 0.0 + pending_qty = 0.0 + for work_order in work_orders: + produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) + pending_qty = flt(order_qty) - produced_qty + + total_produced_qty += produced_qty + + data.append( + { + "indent": 0, + "item_code": row.item_code, + "sales_order": row.get("sales_order"), + "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "qty": order_qty, + "document_type": "Work Order", + "document_name": work_order or "", + "bom_level": 0, + "produced_qty": produced_qty, + "pending_qty": pending_qty, + } + ) + + order_qty = pending_qty data.append( { - "indent": 0, "item_code": row.item_code, - "sales_order": row.get("sales_order"), - "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "indent": 0, "qty": row.planned_qty, - "document_type": "Work Order", - "document_name": work_order or "", - "bom_level": 0, - "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), - "pending_qty": flt(row.planned_qty) - - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)), + "produced_qty": total_produced_qty, + "pending_qty": pending_qty, } ) From 8af005cef0c84a572fdb1e35928de5c228db610b Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Wed, 13 Nov 2024 14:55:45 +0530 Subject: [PATCH 46/48] fix: Drop Shipping address based on customer shopping address (cherry picked from commit c7499f35285182837570e3aaa1d6b274783d2204) --- erpnext/public/js/controllers/transaction.js | 11 ++++++++--- erpnext/selling/doctype/sales_order/sales_order.py | 14 +++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 07ba90cd6d1..0efec214c0d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -956,9 +956,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) { - erpnext.utils.get_shipping_address(this.frm, function() { - set_party_account(set_pricing); - }); + let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier); + + if (!is_drop_ship) { + console.log('get_shipping_address'); + erpnext.utils.get_shipping_address(this.frm, function() { + set_party_account(set_pricing); + }); + } } else { set_party_account(set_pricing); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 62262b41021..88528d7178f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1402,9 +1402,17 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.payment_schedule = [] if is_drop_ship_order(target): - target.customer = source.customer - target.customer_name = source.customer_name - target.shipping_address = source.shipping_address_name + if source.shipping_address_name: + target.shipping_address = source.shipping_address_name + target.shipping_address_display = source.shipping_address + else: + target.shipping_address = source.customer_address + target.shipping_address_display = source.address_display + + target.customer_contact_person = source.contact_person + target.customer_contact_display = source.contact_display + target.customer_contact_mobile = source.contact_mobile + target.customer_contact_email = source.contact_email else: target.customer = target.customer_name = target.shipping_address = None From c1983a48469dc3c961c4653e3e20191ee73dee2d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:24:12 +0530 Subject: [PATCH 47/48] fix: Cannot read properties of undefined (reading 'work_order_closed') (backport #44117) (#44122) fix: Cannot read properties of undefined (reading 'work_order_closed') (#44117) (cherry picked from commit 13834014b50e46e8d432215a1ddf7c8895c8dda5) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/job_card/job_card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 4cc60a3b4a6..ad514efa55f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -37,7 +37,7 @@ frappe.ui.form.on("Job Card", { frappe.flags.resume_job = 0; let has_items = frm.doc.items && frm.doc.items.length; - if (!frm.is_new() && frm.doc.__onload.work_order_closed) { + if (!frm.is_new() && frm.doc.__onload?.work_order_closed) { frm.disable_save(); return; } From b9ec43c354accd98bad6f08393664747ca49781a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Nov 2024 16:56:08 +0530 Subject: [PATCH 48/48] refactor: 'Partly Billed' status for Purchase Receipt (cherry picked from commit c58bbd25f2813ac4392877602ca89247127e3623) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 4 ++-- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 61a180caba4..643f9e7a82f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -889,7 +889,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", + "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1273,7 +1273,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2024-07-04 14:50:10.538472", + "modified": "2024-11-13 16:55:14.129055", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 228bc35693b..523acf51c5b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -112,7 +112,9 @@ class PurchaseReceipt(BuyingController): shipping_address: DF.Link | None shipping_address_display: DF.SmallText | None shipping_rule: DF.Link | None - status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"] + status: DF.Literal[ + "", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed" + ] subcontracting_receipt: DF.Link | None supplied_items: DF.Table[PurchaseReceiptItemSupplied] supplier: DF.Link