From 3a51a3f37ecd70c13cee558a0b882d06e812a21c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Nov 2023 17:34:21 +0530 Subject: [PATCH 01/44] refactor: convert payment reconciliation tool to virtual doctype --- .../payment_reconciliation/payment_reconciliation.json | 6 +++--- .../payment_reconciliation_allocation.json | 3 ++- .../payment_reconciliation_invoice.json | 3 ++- .../payment_reconciliation_payment.json | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index b88791d3f9..ccb9e648cb 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -212,9 +212,10 @@ ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", + "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-08-15 05:35:50.109290", + "modified": "2023-11-17 17:33:55.701726", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -239,6 +240,5 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [], - "track_changes": 1 + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 5b8556e7c8..491c67818d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -159,9 +159,10 @@ "label": "Difference Posting Date" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-10-23 10:44:56.066303", + "modified": "2023-11-17 17:33:38.612615", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index c4dbd7e844..7c9d49e773 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -71,9 +71,10 @@ "label": "Exchange Rate" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2022-11-08 18:18:02.502149", + "modified": "2023-11-17 17:33:45.455166", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index 17f3900880..d199236ae9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -107,9 +107,10 @@ "options": "Cost Center" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-09-03 07:43:29.965353", + "modified": "2023-11-17 17:33:34.818530", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", From 9c7b19e0b7732189c9db12e2c67b12a67fe562b3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 18 Nov 2023 07:48:15 +0530 Subject: [PATCH 02/44] refactor: virtual doctype methods --- .../payment_reconciliation.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 43167be15a..6673e8de28 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -29,6 +29,58 @@ class PaymentReconciliation(Document): self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + def load_from_db(self): + # 'modified' attribute is required for `run_doc_method` to work properly. + doc_dict = frappe._dict( + { + "modified": None, + "company": None, + "party": None, + "party_type": None, + "receivable_payable_account": None, + "default_advance_account": None, + "from_invoice_date": None, + "to_invoice_date": None, + "invoice_limit": 50, + "from_payment_date": None, + "to_payment_date": None, + "payment_limit": 50, + "minimum_invoice_amount": None, + "minimum_payment_amount": None, + "maximum_invoice_amount": None, + "maximum_payment_amount": None, + "bank_cash_account": None, + "cost_center": None, + "payment_name": None, + "invoice_name": None, + } + ) + super(Document, self).__init__(doc_dict) + + def save(self): + return + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self, *args, **kwargs): + pass + + def delete(self): + pass + @frappe.whitelist() def get_unreconciled_entries(self): self.get_nonreconciled_payment_entries() From b5dd0c8630c59cc38011039e7c7a21976b53ebd8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 18 Nov 2023 08:07:36 +0530 Subject: [PATCH 03/44] chore: remove reconciliation defaults from patch --- erpnext/patches.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0aeadce6c8..2935a16d8e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -344,8 +344,6 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v15_0.update_sre_from_voucher_details erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field -execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) -execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) erpnext.patches.v14_0.add_default_for_repost_settings erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based From f31002636bf21a3599ee5a7372db4d0cb3dbfad9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 18 Nov 2023 08:10:09 +0530 Subject: [PATCH 04/44] chore: clear singles table and reconciliation related tables --- erpnext/patches.txt | 1 + .../clear_reconciliation_values_from_singles.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2935a16d8e..1e988c5a00 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -345,6 +345,7 @@ erpnext.patches.v15_0.update_sre_from_voucher_details erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field erpnext.patches.v14_0.add_default_for_repost_settings +erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v15_0.set_reserved_stock_in_bin diff --git a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py new file mode 100644 index 0000000000..c1f5b60a40 --- /dev/null +++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py @@ -0,0 +1,17 @@ +from frappe import qb + + +def execute(): + """ + Clear `tabSingles` and Payment Reconciliation tables of values + """ + singles = qb.DocType("Singles") + qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run() + doctypes = [ + "Payment Reconciliation Invoice", + "Payment Reconciliation Payment", + "Payment Reconciliation Allocation", + ] + for x in doctypes: + dt = qb.DocType(x) + qb.from_(dt).delete().run() From 8b04c1d4f6356bc332c5dd8f6f8711d778e030cd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 22 Nov 2023 09:59:32 +0530 Subject: [PATCH 05/44] refactor: optimize outstanding vouchers query --- erpnext/accounts/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 7d91309fcc..6d2a47f73e 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1833,6 +1833,28 @@ class QueryPaymentLedger(object): Table("outstanding").amount_in_account_currency >= self.max_outstanding ) + if self.limit and self.get_invoices: + outstanding_vouchers = ( + qb.from_(ple) + .select( + ple.against_voucher_no.as_("voucher_no"), + Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"), + ) + .where(ple.delinked == 0) + .where(Criterion.all(filter_on_against_voucher_no)) + .where(Criterion.all(self.common_filter)) + .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party) + .orderby(ple.posting_date, ple.voucher_no) + .having(qb.Field("amount_in_account_currency") > 0) + .limit(self.limit) + .run() + ) + if outstanding_vouchers: + filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers])) + filter_on_against_voucher_no.append( + ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers]) + ) + # build query for voucher amount query_voucher_amount = ( qb.from_(ple) From 9c889b37fb3e6572043c7a28706e43d051e2ff46 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 20 Nov 2023 18:33:56 +0530 Subject: [PATCH 06/44] fix(ux): no need to select rows to reserve the stock --- erpnext/selling/doctype/sales_order/sales_order.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3ad18daf19..01cbbe743d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -214,7 +214,6 @@ frappe.ui.form.on("Sales Order", { label: __("Items to Reserve"), allow_bulk_edit: false, cannot_add_rows: true, - cannot_delete_rows: true, data: [], fields: [ { @@ -260,7 +259,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Reserve Stock"), primary_action: () => { - var data = {items: dialog.fields_dict.items.grid.get_selected_children()}; + var data = {items: dialog.fields_dict.items.grid.data}; if (data.items && data.items.length > 0) { frappe.call({ @@ -278,9 +277,6 @@ frappe.ui.form.on("Sales Order", { } }); } - else { - frappe.msgprint(__("Please select items to reserve.")); - } dialog.hide(); }, @@ -316,7 +312,6 @@ frappe.ui.form.on("Sales Order", { label: __("Reserved Stock"), allow_bulk_edit: false, cannot_add_rows: true, - cannot_delete_rows: true, in_place_edit: true, data: [], fields: [ @@ -360,7 +355,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Unreserve Stock"), primary_action: () => { - var data = {sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children()}; + var data = {sr_entries: dialog.fields_dict.sr_entries.grid.data}; if (data.sr_entries && data.sr_entries.length > 0) { frappe.call({ @@ -377,9 +372,6 @@ frappe.ui.form.on("Sales Order", { } }); } - else { - frappe.msgprint(__("Please select items to unreserve.")); - } dialog.hide(); }, From 73586fd9b2f9392d18f65a063b14ef2de2629615 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 22 Nov 2023 11:37:14 +0530 Subject: [PATCH 07/44] fix: use field `sales_order_item` instead `name` --- erpnext/selling/doctype/sales_order/sales_order.js | 6 +++--- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- .../stock_reservation_entry/stock_reservation_entry.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 01cbbe743d..81b0d016ae 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -217,9 +217,9 @@ frappe.ui.form.on("Sales Order", { data: [], fields: [ { - fieldname: "name", + fieldname: "sales_order_item", fieldtype: "Data", - label: __("Name"), + label: __("Sales Order Item"), reqd: 1, read_only: 1, }, @@ -288,7 +288,7 @@ frappe.ui.form.on("Sales Order", { if (unreserved_qty > 0) { dialog.fields_dict.items.df.data.push({ - 'name': item.name, + 'sales_order_item': item.name, 'item_code': item.item_code, 'warehouse': item.warehouse, 'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor)) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ed20209577..3350a688e8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -233,7 +233,7 @@ class PickList(Document): for location in self.locations: if location.warehouse and location.sales_order and location.sales_order_item: item_details = { - "name": location.sales_order_item, + "sales_order_item": location.sales_order_item, "item_code": location.item_code, "warehouse": location.warehouse, "qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)), diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d905fe5ea6..9bc7e58f80 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -775,7 +775,7 @@ class PurchaseReceipt(BuyingController): for item in self.items: if item.sales_order and item.sales_order_item: item_details = { - "name": item.sales_order_item, + "sales_order_item": item.sales_order_item, "item_code": item.item_code, "warehouse": item.warehouse, "qty_to_reserve": item.stock_qty, diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 09542826f3..df3e288c4c 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -869,7 +869,7 @@ def create_stock_reservation_entries_for_so_items( items = [] if items_details: for item in items_details: - so_item = frappe.get_doc("Sales Order Item", item.get("name")) + so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item")) so_item.warehouse = item.get("warehouse") so_item.qty_to_reserve = ( flt(item.get("qty_to_reserve")) From 2a41da94d443dee51e615b395d18ad161d4e87fc Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 21 Nov 2023 11:34:53 +0530 Subject: [PATCH 08/44] fix(ux): no need to select rows to unreserve the stock --- erpnext/selling/doctype/sales_order/sales_order.js | 10 +++++----- .../stock_reservation_entry/stock_reservation_entry.py | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 81b0d016ae..97b214e33e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -304,7 +304,7 @@ frappe.ui.form.on("Sales Order", { cancel_stock_reservation_entries(frm) { const dialog = new frappe.ui.Dialog({ title: __("Stock Unreservation"), - size: "large", + size: "extra-large", fields: [ { fieldname: "sr_entries", @@ -316,9 +316,9 @@ frappe.ui.form.on("Sales Order", { data: [], fields: [ { - fieldname: "name", + fieldname: "sre", fieldtype: "Link", - label: __("SRE"), + label: __("Stock Reservation Entry"), options: "Stock Reservation Entry", reqd: 1, read_only: 1, @@ -362,7 +362,7 @@ frappe.ui.form.on("Sales Order", { doc: frm.doc, method: "cancel_stock_reservation_entries", args: { - sre_list: data.sr_entries, + sre_list: data.sr_entries.map(item => item.sre), }, freeze: true, freeze_message: __('Unreserving Stock...'), @@ -388,7 +388,7 @@ frappe.ui.form.on("Sales Order", { r.message.forEach(sre => { if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) { dialog.fields_dict.sr_entries.df.data.push({ - 'name': sre.name, + 'sre': sre.name, 'item_code': sre.item_code, 'warehouse': sre.warehouse, 'qty': (flt(sre.reserved_qty) - flt(sre.delivered_qty)) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index df3e288c4c..cbfa4e0a43 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -1053,12 +1053,14 @@ def cancel_stock_reservation_entries( from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, from_voucher_no: str = None, from_voucher_detail_no: str = None, - sre_list: list[dict] = None, + sre_list: list = None, notify: bool = True, ) -> None: """Cancel Stock Reservation Entries.""" if not sre_list: + sre_list = {} + if voucher_type and voucher_no: sre_list = get_stock_reservation_entries_for_voucher( voucher_type, voucher_no, voucher_detail_no, fields=["name"] @@ -1082,9 +1084,11 @@ def cancel_stock_reservation_entries( sre_list = query.run(as_dict=True) + sre_list = [d.name for d in sre_list] + if sre_list: for sre in sre_list: - frappe.get_doc("Stock Reservation Entry", sre["name"]).cancel() + frappe.get_doc("Stock Reservation Entry", sre).cancel() if notify: msg = _("Stock Reservation Entries Cancelled") From f9713eeb5690f9e71e01c6c570012f561a633c73 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 15:22:42 +0530 Subject: [PATCH 09/44] fix: skip fixed assets in parent --- .../doctype/product_bundle/product_bundle.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index ac83c0f046..4b401e7e67 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -59,6 +59,8 @@ class ProductBundle(Document): """Validates, main Item is not a stock item""" if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code)) + if frappe.db.get_value("Item", self.new_item_code, "is_fixed_asset"): + frappe.throw(_("Parent Item {0} must not be a Fixed Asset").format(self.new_item_code)) def validate_child_items(self): for item in self.items: @@ -73,12 +75,17 @@ class ProductBundle(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): - from erpnext.controllers.queries import get_match_cond - - return frappe.db.sql( - """select name, item_name, description from tabItem - where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) - and %s like %s %s limit %s offset %s""" - % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), - ("%%%s%%" % txt, page_len, start), - ) + product_bundles = frappe.db.get_list("Product Bundle", pluck="name") + item = frappe.qb.DocType("Item") + return ( + frappe.qb.from_(item) + .select("*") + .where( + (item.is_stock_item == 0) + & (item.is_fixed_asset == 0) + & (item.name.notin(product_bundles)) + & (item[searchfield].like(f"%{txt}%")) + ) + .limit(page_len) + .offset(start) + ).run() From ee76af76812fe04d86653fd1955ad133c8cb5df8 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 15:23:17 +0530 Subject: [PATCH 10/44] feat: add disabled field in product bundle --- .../product_bundle/product_bundle.json | 398 +++++------------- 1 file changed, 101 insertions(+), 297 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index 56155fb750..c4f21b61b9 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -1,315 +1,119 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "beta": 0, - "creation": "2013-06-20 11:53:21", - "custom": 0, - "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "creation": "2013-06-20 11:53:21", + "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "basic_section", + "new_item_code", + "description", + "column_break_eonk", + "disabled", + "item_section", + "items", + "section_break_4", + "about" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "basic_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "basic_section", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "new_item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Parent Item", - "length": 0, - "no_copy": 1, - "oldfieldname": "new_item_code", - "oldfieldtype": "Data", - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "new_item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Parent Item", + "no_copy": 1, + "oldfieldname": "new_item_code", + "oldfieldtype": "Data", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Description" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "List items that form the package.", - "fieldname": "item_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "List items that form the package.", + "fieldname": "item_section", + "fieldtype": "Section Break", + "label": "Items" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Items", - "length": 0, - "no_copy": 0, - "oldfieldname": "sales_bom_items", - "oldfieldtype": "Table", - "options": "Product Bundle Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "sales_bom_items", + "oldfieldtype": "Table", + "options": "Product Bundle Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "about", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "options": "

About Product Bundle

\n\n

Aggregate group of Items into another Item. This is useful if you are bundling a certain Items into a package and you maintain stock of the packed Items and not the aggregate Item.

\n

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

\n

Example:

\n

If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.

", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "about", + "fieldtype": "HTML", + "options": "

About Product Bundle

\n\n

Aggregate group of Items into another Item. This is useful if you are bundling a certain Items into a package and you maintain stock of the packed Items and not the aggregate Item.

\n

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

\n

Example:

\n

If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.

" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_eonk", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-sitemap", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Selling", - "name": "Product Bundle", - "owner": "Administrator", + ], + "icon": "fa fa-sitemap", + "idx": 1, + "links": [], + "modified": "2023-11-22 15:20:46.805114", + "modified_by": "Administrator", + "module": "Selling", + "name": "Product Bundle", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC", + "states": [] } \ No newline at end of file From 874774fe6c81c854fb3ade03e93f003c0fc8dd8a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 15:24:56 +0530 Subject: [PATCH 11/44] fix: filter bundle items based on disabled check --- erpnext/stock/doctype/packed_item/packed_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index a9e9ad1a63..8b6f7158e0 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -111,7 +111,7 @@ def get_product_bundle_items(item_code): product_bundle_item.uom, product_bundle_item.description, ) - .where(product_bundle.new_item_code == item_code) + .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) .orderby(product_bundle_item.idx) ) return query.run(as_dict=True) From 627165dc7c6c1cd83d746b97c82210abeb9a2e29 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 22 Nov 2023 15:26:45 +0530 Subject: [PATCH 12/44] fix: Supplier `Primary Contact` --- erpnext/buying/doctype/supplier/supplier.py | 27 +++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 31bf439dbb..b052f564a4 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -165,16 +165,17 @@ class Supplier(TransactionBase): @frappe.validate_and_sanitize_search_inputs def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): supplier = filters.get("supplier") - return frappe.db.sql( - """ - SELECT - `tabContact`.name from `tabContact`, - `tabDynamic Link` - WHERE - `tabContact`.name = `tabDynamic Link`.parent - and `tabDynamic Link`.link_name = %(supplier)s - and `tabDynamic Link`.link_doctype = 'Supplier' - and `tabContact`.name like %(txt)s - """, - {"supplier": supplier, "txt": "%%%s%%" % txt}, - ) + contact = frappe.qb.DocType("Contact") + dynamic_link = frappe.qb.DocType("Dynamic Link") + + return ( + frappe.qb.from_(contact) + .join(dynamic_link) + .on(contact.name == dynamic_link.parent) + .select(contact.name, contact.email_id) + .where( + (dynamic_link.link_name == supplier) + & (dynamic_link.link_doctype == "Supplier") + & (contact.name.like("%{0}%".format(txt))) + ) + ).run(as_dict=False) From 3543f86c639cf7383c37a5ed9a61d5787c9ebad4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:04:05 +0530 Subject: [PATCH 13/44] fix: has_product_bundle util to only check for enabled bundles --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d34fbeb0da..c3211b1d1c 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -352,7 +352,7 @@ class SellingController(StockController): def has_product_bundle(self, item_code): return frappe.db.sql( """select name from `tabProduct Bundle` - where new_item_code=%s and docstatus != 2""", + where new_item_code=%s and disabled=0""", item_code, ) From 67f43d37df9e77c3e59562117fa44ea76273a536 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:10:49 +0530 Subject: [PATCH 14/44] fix: validation for existing bundles --- erpnext/selling/doctype/product_bundle/product_bundle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 4b401e7e67..2fd9cc1301 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -64,7 +64,7 @@ class ProductBundle(Document): def validate_child_items(self): for item in self.items: - if frappe.db.exists("Product Bundle", item.item_code): + if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}): frappe.throw( _( "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save" @@ -75,7 +75,7 @@ class ProductBundle(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): - product_bundles = frappe.db.get_list("Product Bundle", pluck="name") + product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") item = frappe.qb.DocType("Item") return ( frappe.qb.from_(item) From 8bdb61cb87802723a0db1aaa29c30c15c740ec6b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:17:23 +0530 Subject: [PATCH 15/44] fix: condition in other bundle utils --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- erpnext/selling/doctype/sales_order/sales_order.py | 8 +++++--- erpnext/stock/get_item_details.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e36e97bc4b..9091a77f99 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -556,7 +556,7 @@ def get_stock_availability(item_code, warehouse): return bin_qty - pos_sales_qty, is_stock_item else: is_stock_item = True - if frappe.db.exists("Product Bundle", item_code): + if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): return get_bundle_availability(item_code, warehouse), is_stock_item else: is_stock_item = False diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a97198aa78..a23599b180 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -688,7 +688,9 @@ def make_material_request(source_name, target_doc=None): "Sales Order Item": { "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, - "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code) + "condition": lambda item: not frappe.db.exists( + "Product Bundle", {"name": item.item_code, "disabled": 0} + ) and get_remaining_qty(item) > 0, "postprocess": update_item, }, @@ -1309,7 +1311,7 @@ def set_delivery_date(items, sales_order): def is_product_bundle(item_code): - return frappe.db.exists("Product Bundle", item_code) + return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}) @frappe.whitelist() @@ -1521,7 +1523,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0): product_bundle_parents = [ pb.new_item_code for pb in frappe.get_all( - "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + "Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"] ) ] diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c766cab0a1..d1a9cf26ac 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -149,7 +149,7 @@ def remove_standard_fields(details): def set_valuation_rate(out, args): - if frappe.db.exists("Product Bundle", args.item_code, cache=True): + if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True): valuation_rate = 0.0 bundled_items = frappe.get_doc("Product Bundle", args.item_code) From 362f377f6127e9262ab7829a427d0a2051a77b2f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:26:09 +0530 Subject: [PATCH 16/44] fix: skip disabled bundles for non-report utils --- erpnext/stock/doctype/delivery_note/delivery_note.py | 4 ++-- erpnext/stock/doctype/item/item.py | 8 ++++++-- erpnext/stock/doctype/packed_item/packed_item.py | 2 +- erpnext/stock/doctype/pick_list/pick_list.py | 8 ++++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 66dd33a400..f240136e9c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -615,7 +615,7 @@ class DeliveryNote(SellingController): items_list = [item.item_code for item in self.items] return frappe.db.get_all( "Product Bundle", - filters={"new_item_code": ["in", items_list]}, + filters={"new_item_code": ["in", items_list], "disabled": 0}, pluck="name", ) @@ -938,7 +938,7 @@ def make_packing_slip(source_name, target_doc=None): }, "postprocess": update_item, "condition": lambda item: ( - not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}) + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}) and flt(item.packed_qty) < flt(item.qty) ), }, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d8935fe203..cb34497f28 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -512,8 +512,12 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name}) - new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name}) + old_bundle = frappe.get_value( + "Product Bundle", filters={"new_item_code": old_name, "disabled": 0} + ) + new_bundle = frappe.get_value( + "Product Bundle", filters={"new_item_code": new_name, "disabled": 0} + ) if old_bundle and new_bundle: bundle_link = get_link_to_form("Product Bundle", old_bundle) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 8b6f7158e0..35701c90de 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -55,7 +55,7 @@ def make_packing_list(doc): def is_product_bundle(item_code: str) -> bool: - return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code})) + return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0})) def get_indexed_packed_items_table(doc): diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ed20209577..644df3d29a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -368,7 +368,9 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) if not cint( frappe.get_cached_value("Item", item.item_code, "is_stock_item") - ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + ) and not frappe.db.exists( + "Product Bundle", {"new_item_code": item.item_code, "disabled": 0} + ): continue item_code = item.item_code reference = item.sales_order_item or item.material_request_item @@ -507,7 +509,9 @@ class PickList(Document): # bundle_item_code: Dict[component, qty] product_bundle_qty_map = {} for bundle_item_code in bundles: - bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code}) + bundle = frappe.get_last_doc( + "Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0} + ) product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map From 16573378870bb9c6e80f39e80fa43e5b2cc0a69f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:45:14 +0530 Subject: [PATCH 17/44] chore: linting issues --- erpnext/controllers/selling_controller.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index c3211b1d1c..5575a24b35 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -350,11 +350,12 @@ class SellingController(StockController): return il def has_product_bundle(self, item_code): - return frappe.db.sql( - """select name from `tabProduct Bundle` - where new_item_code=%s and disabled=0""", - item_code, - ) + product_bundle = frappe.qb.DocType("Product Bundle") + return ( + frappe.qb.from_(product_bundle) + .select(product_bundle.name) + .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) + ).run() def get_already_delivered_qty(self, current_docname, so, so_detail): delivered_via_dn = frappe.db.sql( From 52305e3000decb84aad1a99557e13a0bb2b68ec4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 18:38:33 +0530 Subject: [PATCH 18/44] fix: annual income and expenses in digest --- erpnext/accounts/utils.py | 3 +++ erpnext/setup/doctype/email_digest/email_digest.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 7d91309fcc..dbad2bacb6 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -183,6 +183,7 @@ def get_balance_on( cost_center=None, ignore_account_permission=False, account_type=None, + start_date=None, ): if not account and frappe.form_dict.get("account"): account = frappe.form_dict.get("account") @@ -196,6 +197,8 @@ def get_balance_on( cost_center = frappe.form_dict.get("cost_center") cond = ["is_cancelled=0"] + if start_date: + cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date))) if date: cond.append("posting_date <= %s" % frappe.db.escape(cstr(date))) else: diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 4fc20e6103..b9e225728e 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -382,9 +382,10 @@ class EmailDigest(Document): """Get income to date""" balance = 0.0 count = 0 + fy_start_date = get_fiscal_year().get("year_start_date") for account in self.get_root_type_accounts(root_type): - balance += get_balance_on(account, date=self.future_to_date) + balance += get_balance_on(account, date=self.future_to_date, start_date=fy_start_date) count += get_count_on(account, fieldname, date=self.future_to_date) if fieldname == "income": From 728cc9f725264e057d9331362b256ea8d0f80b83 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 19:28:57 +0530 Subject: [PATCH 19/44] fix: fiscal year using future date --- erpnext/setup/doctype/email_digest/email_digest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index b9e225728e..6ed44fff68 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -382,7 +382,7 @@ class EmailDigest(Document): """Get income to date""" balance = 0.0 count = 0 - fy_start_date = get_fiscal_year().get("year_start_date") + fy_start_date = get_fiscal_year(self.future_to_date)[1] for account in self.get_root_type_accounts(root_type): balance += get_balance_on(account, date=self.future_to_date, start_date=fy_start_date) From f258ab5e98f323303276aeca98dcc644f6611a70 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 10:39:41 +0530 Subject: [PATCH 20/44] chore: move reconciliation cleanup patch to pre-model --- erpnext/patches.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a71d71b83a..8e091d4a5d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -259,6 +259,7 @@ erpnext.patches.v14_0.update_reference_due_date_in_journal_entry erpnext.patches.v15_0.saudi_depreciation_warning erpnext.patches.v15_0.delete_saudi_doctypes erpnext.patches.v14_0.show_loan_management_deprecation_warning +erpnext.patches.v14_0.clear_reconciliation_values_from_singles execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) [post_model_sync] @@ -345,7 +346,6 @@ erpnext.patches.v15_0.update_sre_from_voucher_details erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field erpnext.patches.v14_0.add_default_for_repost_settings -erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v15_0.set_reserved_stock_in_bin From 24fcd67f8bcf8cc8b387856983b97d8778eac084 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 11:44:34 +0530 Subject: [PATCH 21/44] chore: index to speed up queries on JE child table reference --- .../journal_entry_account/journal_entry_account.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 3ba8cea94b..3132fe9b12 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -203,7 +203,8 @@ "fieldtype": "Select", "label": "Reference Type", "no_copy": 1, - "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry" + "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry", + "search_index": 1 }, { "fieldname": "reference_name", @@ -211,7 +212,8 @@ "in_list_view": 1, "label": "Reference Name", "no_copy": 1, - "options": "reference_type" + "options": "reference_type", + "search_index": 1 }, { "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])", @@ -278,13 +280,14 @@ "fieldtype": "Data", "hidden": 1, "label": "Reference Detail No", - "no_copy": 1 + "no_copy": 1, + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-06-16 14:11:13.507807", + "modified": "2023-11-23 11:44:25.841187", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", From 816b1b6bd52b4f6adcb35d4039ad75892b0976ff Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 23 Nov 2023 13:48:46 +0530 Subject: [PATCH 22/44] fix: don't depreciate assets with no schedule on scrapping (#38276) fix: don't depreciate non-depreciable assets on scrapping --- erpnext/assets/doctype/asset/depreciation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 84a428ca54..66930c0e7c 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -509,6 +509,9 @@ def restore_asset(asset_name): def depreciate_asset(asset_doc, date, notes): + if not asset_doc.calculate_depreciation: + return + asset_doc.flags.ignore_validate_update_after_submit = True make_new_active_asset_depr_schedules_and_cancel_current_ones( @@ -521,6 +524,9 @@ def depreciate_asset(asset_doc, date, notes): def reset_depreciation_schedule(asset_doc, date, notes): + if not asset_doc.calculate_depreciation: + return + asset_doc.flags.ignore_validate_update_after_submit = True make_new_active_asset_depr_schedules_and_cancel_current_ones( From b206b0583b2998382c0ce1a63c867f09a6a0b03a Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 23 Nov 2023 15:10:47 +0530 Subject: [PATCH 23/44] fix: fetch item_tax_template values if fields with fetch_from exisit --- erpnext/controllers/accounts_controller.py | 1 + erpnext/public/js/controllers/transaction.js | 1 + erpnext/stock/get_item_details.py | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d12d50d2e7..e68894fa77 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -625,6 +625,7 @@ class AccountsController(TransactionBase): args["doctype"] = self.doctype args["name"] = self.name + args["child_doctype"] = item.doctype args["child_docname"] = item.name args["ignore_pricing_rule"] = ( self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2c40f4964b..6dc24faa96 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -512,6 +512,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe cost_center: item.cost_center, tax_category: me.frm.doc.tax_category, item_tax_template: item.item_tax_template, + child_doctype: item.doctype, child_docname: item.name, is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c766cab0a1..5143067f74 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -8,6 +8,7 @@ import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields from frappe.model.meta import get_field_precision +from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, getdate @@ -571,6 +572,9 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group + if args.child_doctype and item_tax_template: + out.update(get_fetch_values(args.child_doctype, "item_tax_template", item_tax_template)) + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: From 1efff268b036dd27f7472bc7ec494aafac9edbc8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 16:56:35 +0530 Subject: [PATCH 24/44] chore: speed up Purchase Invoice cancellation --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index d1677832ed..f2094874e0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1615,7 +1615,8 @@ "hide_seconds": 1, "label": "Inter Company Invoice Reference", "options": "Purchase Invoice", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "customer_group", @@ -2173,7 +2174,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-11-20 11:51:43.555197", + "modified": "2023-11-23 16:56:29.679499", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 3be345e6056f36baa6eef75818a7cdc63826b5f8 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:02:27 +0100 Subject: [PATCH 25/44] feat: add Bank Transaction to connections in Journal and Payment Entry (#38297) * feat(Payment Entry): Bank Transaction connection * feat(Journal Entry): Bank Transaction connection --- .../doctype/journal_entry/journal_entry.json | 12 ++++++++++-- .../doctype/payment_entry/payment_entry.json | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 2eb54a54d5..906760ec31 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -548,8 +548,16 @@ "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, - "links": [], - "modified": "2023-08-10 14:32:22.366895", + "links": [ + { + "is_child_table": 1, + "link_doctype": "Bank Transaction Payments", + "link_fieldname": "payment_entry", + "parent_doctype": "Bank Transaction", + "table_fieldname": "payment_entries" + } + ], + "modified": "2023-11-23 12:11:04.128015", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 4d50a35ed4..aa181564b0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -750,8 +750,16 @@ ], "index_web_pages_for_search": 1, "is_submittable": 1, - "links": [], - "modified": "2023-11-08 21:51:03.482709", + "links": [ + { + "is_child_table": 1, + "link_doctype": "Bank Transaction Payments", + "link_fieldname": "payment_entry", + "parent_doctype": "Bank Transaction", + "table_fieldname": "payment_entries" + } + ], + "modified": "2023-11-23 12:07:20.887885", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 64b44a360af8c2d3a6a9cb99e03358b97cdf9fa5 Mon Sep 17 00:00:00 2001 From: NandhiniDevi <95607404+Nandhinidevi123@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:40:17 +0530 Subject: [PATCH 26/44] add flt() for None type error (#38299) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 85ef6f76d2..1cff4c7f2d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -868,7 +868,7 @@ class JournalEntry(AccountsController): party_account_currency = d.account_currency elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]: - bank_amount += d.debit_in_account_currency or d.credit_in_account_currency + bank_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency) bank_account_currency = d.account_currency if party_type and pay_to_recd_from: From 3f6d80503335a33bf2bff53a19870a6cdba00044 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 23 Nov 2023 12:12:12 -0500 Subject: [PATCH 27/44] fix: display all item rate stop messages (#38289) --- erpnext/utilities/transaction_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 7eba35dedd..b083614a5f 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater): "Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] ) + stop_actions = [] for ref_dt, ref_dn_field, ref_link_field in ref_details: reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)] reference_details = self.get_reference_details(reference_names, ref_dt + " Item") @@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater): if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": if role_allowed_to_override not in frappe.get_roles(): - frappe.throw( + stop_actions.append( _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate ) @@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater): title=_("Warning"), indicator="orange", ) + if stop_actions: + frappe.throw(stop_actions, as_list=True) def get_reference_details(self, reference_names, reference_doctype): return frappe._dict( From aa17110bde44603574d4532afaa6cf1181423ac5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 17:39:48 +0530 Subject: [PATCH 28/44] perf: optimize total_purchase_cost update --- .../purchase_invoice/purchase_invoice.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d7c2361b4d..e83525dbac 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1281,13 +1281,21 @@ class PurchaseInvoice(BuyingController): self.update_advance_tax_references(cancel=1) def update_project(self): - project_list = [] + projects = frappe._dict() for d in self.items: - if d.project and d.project not in project_list: - project = frappe.get_doc("Project", d.project) - project.update_purchase_costing() - project.db_update() - project_list.append(d.project) + if d.project: + if self.docstatus == 1: + projects[d.project] = projects.get(d.project, 0) + d.base_net_amount + elif self.docstatus == 2: + projects[d.project] = projects.get(d.project, 0) - d.base_net_amount + + pj = frappe.qb.DocType("Project") + for proj, value in projects.items(): + res = ( + frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run() + ) + current_purchase_cost = res and res[0][0] or 0 + frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value) def validate_supplier_invoice(self): if self.bill_date: From bcbe6c4a531986836a0a2497e05bdbe1a327407a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 17:53:42 +0530 Subject: [PATCH 29/44] refactor: provide UI button to recalculate when needed --- erpnext/projects/doctype/project/project.js | 20 +++++++++++ erpnext/projects/doctype/project/project.py | 37 ++++++++++++++++----- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index f366f77556..2dac399d88 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -68,6 +68,10 @@ frappe.ui.form.on("Project", { frm.events.create_duplicate(frm); }, __("Actions")); + frm.add_custom_button(__('Update Total Purchase Cost'), () => { + frm.events.update_total_purchase_cost(frm); + }, __("Actions")); + frm.trigger("set_project_status_button"); @@ -92,6 +96,22 @@ frappe.ui.form.on("Project", { }, + update_total_purchase_cost: function(frm) { + frappe.call({ + method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost", + args: {project: frm.doc.name}, + freeze: true, + freeze_message: __('Recalculating Purchase Cost against this Project...'), + callback: function(r) { + if (r && !r.exc) { + frappe.msgprint(__('Total Purchase Cost has been updated')); + frm.refresh(); + } + } + + }); + }, + set_project_status_button: function(frm) { frm.add_custom_button(__('Set Project Status'), () => { let d = new frappe.ui.Dialog({ diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index e9aed1afb4..4f2e39539d 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -4,11 +4,11 @@ import frappe from email_reply_parser import EmailReplyParser -from frappe import _ +from frappe import _, qb from frappe.desk.reportview import get_match_cond from frappe.model.document import Document from frappe.query_builder import Interval -from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp +from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today from frappe.utils.user import is_website_user @@ -249,12 +249,7 @@ class Project(Document): self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100 def update_purchase_costing(self): - total_purchase_cost = frappe.db.sql( - """select sum(base_net_amount) - from `tabPurchase Invoice Item` where project = %s and docstatus=1""", - self.name, - ) - + total_purchase_cost = calculate_total_purchase_cost(self.name) self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0 def update_sales_amount(self): @@ -695,3 +690,29 @@ def get_holiday_list(company=None): def get_users_email(doc): return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")] + + +def calculate_total_purchase_cost(project: str | None = None): + if project: + pitem = qb.DocType("Purchase Invoice Item") + frappe.qb.DocType("Purchase Invoice Item") + total_purchase_cost = ( + qb.from_(pitem) + .select(Sum(pitem.base_net_amount)) + .where((pitem.project == project) & (pitem.docstatus == 1)) + .run(as_list=True) + ) + return total_purchase_cost + return None + + +@frappe.whitelist() +def recalculate_project_total_purchase_cost(project: str | None = None): + if project: + total_purchase_cost = calculate_total_purchase_cost(project) + frappe.db.set_value( + "Project", + project, + "total_purchase_cost", + (total_purchase_cost and total_purchase_cost[0][0] or 0), + ) From 0fe6dcd74288d5f2df1ffc37485e22ee96329b9e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 24 Nov 2023 10:53:00 +0530 Subject: [PATCH 30/44] refactor: make update_project_cost optional through Buying Settings --- .../doctype/buying_settings/buying_settings.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 059999245d..0af93bfc90 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -17,6 +17,7 @@ "po_required", "pr_required", "blanket_order_allowance", + "project_update_frequency", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -172,6 +173,14 @@ "fieldname": "blanket_order_allowance", "fieldtype": "Float", "label": "Blanket Order Allowance (%)" + }, + { + "default": "Each Transaction", + "description": "How often should Project be updated of Total Purchase Cost ?", + "fieldname": "project_update_frequency", + "fieldtype": "Select", + "label": "Update frequency of Project", + "options": "Each Transaction\nManual" } ], "icon": "fa fa-cog", @@ -179,7 +188,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-25 14:03:32.520418", + "modified": "2023-11-24 10:55:51.287327", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", From dd016e6ced432b6f8754042fbad808b1cba56cec Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 24 Nov 2023 10:54:31 +0530 Subject: [PATCH 31/44] refactor: update project costing based on frequency --- .../doctype/purchase_invoice/purchase_invoice.py | 11 +++++++++-- erpnext/patches.txt | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e83525dbac..c6ae9377a0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -527,7 +527,11 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - self.update_project() + if ( + frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction" + ): + self.update_project() + update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) self.update_advance_tax_references() @@ -1262,7 +1266,10 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - self.update_project() + if ( + frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction" + ): + self.update_project() self.db_set("status", "Cancelled") unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a71d71b83a..4991b48216 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -351,5 +351,6 @@ erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_d erpnext.patches.v15_0.set_reserved_stock_in_bin erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field +execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger From 477d9fa87e3cd7476f19930d60d23e347e46e658 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 24 Nov 2023 22:27:01 +0100 Subject: [PATCH 32/44] feat: new Report "Lost Quotations" (#38309) --- .../crm/doctype/competitor/competitor.json | 13 +- .../report/lost_quotations/__init__.py | 0 .../report/lost_quotations/lost_quotations.js | 40 ++++++ .../lost_quotations/lost_quotations.json | 30 +++++ .../report/lost_quotations/lost_quotations.py | 98 ++++++++++++++ .../quotation_lost_reason.json | 123 +++++++----------- 6 files changed, 228 insertions(+), 76 deletions(-) create mode 100644 erpnext/selling/report/lost_quotations/__init__.py create mode 100644 erpnext/selling/report/lost_quotations/lost_quotations.js create mode 100644 erpnext/selling/report/lost_quotations/lost_quotations.json create mode 100644 erpnext/selling/report/lost_quotations/lost_quotations.py diff --git a/erpnext/crm/doctype/competitor/competitor.json b/erpnext/crm/doctype/competitor/competitor.json index 280441f16f..fd6da23921 100644 --- a/erpnext/crm/doctype/competitor/competitor.json +++ b/erpnext/crm/doctype/competitor/competitor.json @@ -29,8 +29,16 @@ } ], "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-10-21 12:43:59.106807", + "links": [ + { + "is_child_table": 1, + "link_doctype": "Competitor Detail", + "link_fieldname": "competitor", + "parent_doctype": "Quotation", + "table_fieldname": "competitors" + } + ], + "modified": "2023-11-23 19:33:54.284279", "modified_by": "Administrator", "module": "CRM", "name": "Competitor", @@ -64,5 +72,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/report/lost_quotations/__init__.py b/erpnext/selling/report/lost_quotations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.js b/erpnext/selling/report/lost_quotations/lost_quotations.js new file mode 100644 index 0000000000..78e76cbf02 --- /dev/null +++ b/erpnext/selling/report/lost_quotations/lost_quotations.js @@ -0,0 +1,40 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Lost Quotations"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + }, + { + label: "Timespan", + fieldtype: "Select", + fieldname: "timespan", + options: [ + "Last Week", + "Last Month", + "Last Quarter", + "Last 6 months", + "Last Year", + "This Week", + "This Month", + "This Quarter", + "This Year", + ], + default: "This Year", + reqd: 1, + }, + { + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + options: ["Lost Reason", "Competitor"], + default: "Lost Reason", + reqd: 1, + }, + ], +}; diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.json b/erpnext/selling/report/lost_quotations/lost_quotations.json new file mode 100644 index 0000000000..8915bab595 --- /dev/null +++ b/erpnext/selling/report/lost_quotations/lost_quotations.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-11-23 18:00:19.141922", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": null, + "letterhead": null, + "modified": "2023-11-23 19:27:28.854108", + "modified_by": "Administrator", + "module": "Selling", + "name": "Lost Quotations", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Quotation", + "report_name": "Lost Quotations", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.py b/erpnext/selling/report/lost_quotations/lost_quotations.py new file mode 100644 index 0000000000..7c0bfbdd52 --- /dev/null +++ b/erpnext/selling/report/lost_quotations/lost_quotations.py @@ -0,0 +1,98 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import Literal + +import frappe +from frappe import _ +from frappe.model.docstatus import DocStatus +from frappe.query_builder.functions import Coalesce, Count, Round, Sum +from frappe.utils.data import get_timespan_date_range + + +def execute(filters=None): + columns = get_columns(filters.get("group_by")) + from_date, to_date = get_timespan_date_range(filters.get("timespan").lower()) + data = get_data(filters.get("company"), from_date, to_date, filters.get("group_by")) + return columns, data + + +def get_columns(group_by: Literal["Lost Reason", "Competitor"]): + return [ + { + "fieldname": "lost_reason" if group_by == "Lost Reason" else "competitor", + "label": _("Lost Reason") if group_by == "Lost Reason" else _("Competitor"), + "fieldtype": "Link", + "options": "Quotation Lost Reason" if group_by == "Lost Reason" else "Competitor", + "width": 200, + }, + { + "filedname": "lost_quotations", + "label": _("Lost Quotations"), + "fieldtype": "Int", + "width": 150, + }, + { + "filedname": "lost_quotations_pct", + "label": _("Lost Quotations %"), + "fieldtype": "Percent", + "width": 200, + }, + { + "fieldname": "lost_value", + "label": _("Lost Value"), + "fieldtype": "Currency", + "width": 150, + }, + { + "filedname": "lost_value_pct", + "label": _("Lost Value %"), + "fieldtype": "Percent", + "width": 200, + }, + ] + + +def get_data( + company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"] +): + """Return quotation value grouped by lost reason or competitor""" + if group_by == "Lost Reason": + fieldname = "lost_reason" + dimension = frappe.qb.DocType("Quotation Lost Reason Detail") + elif group_by == "Competitor": + fieldname = "competitor" + dimension = frappe.qb.DocType("Competitor Detail") + else: + frappe.throw(_("Invalid Group By")) + + q = frappe.qb.DocType("Quotation") + + lost_quotation_condition = ( + (q.status == "Lost") + & (q.docstatus == DocStatus.submitted()) + & (q.transaction_date >= from_date) + & (q.transaction_date <= to_date) + & (q.company == company) + ) + + from_lost_quotations = frappe.qb.from_(q).where(lost_quotation_condition) + total_quotations = from_lost_quotations.select(Count(q.name)) + total_value = from_lost_quotations.select(Sum(q.base_net_total)) + + query = ( + frappe.qb.from_(q) + .select( + Coalesce(dimension[fieldname], _("Not Specified")), + Count(q.name).distinct(), + Round((Count(q.name).distinct() / total_quotations * 100), 2), + Sum(q.base_net_total), + Round((Sum(q.base_net_total) / total_value * 100), 2), + ) + .left_join(dimension) + .on(dimension.parent == q.name) + .where(lost_quotation_condition) + .groupby(dimension[fieldname]) + ) + + return query.run() diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json index 5d778eec0b..0eae08e870 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json +++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json @@ -1,83 +1,58 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:order_lost_reason", - "beta": 0, - "creation": "2013-01-10 16:34:24", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "field:order_lost_reason", + "creation": "2013-01-10 16:34:24", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "order_lost_reason" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "order_lost_reason", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Quotation Lost Reason", - "length": 0, - "no_copy": 0, - "oldfieldname": "order_lost_reason", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "order_lost_reason", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Quotation Lost Reason", + "oldfieldname": "order_lost_reason", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-07-25 05:24:25.533953", - "modified_by": "Administrator", - "module": "Setup", - "name": "Quotation Lost Reason", - "owner": "Administrator", + ], + "icon": "fa fa-flag", + "idx": 1, + "links": [ + { + "is_child_table": 1, + "link_doctype": "Quotation Lost Reason Detail", + "link_fieldname": "lost_reason", + "parent_doctype": "Quotation", + "table_fieldname": "lost_reasons" + } + ], + "modified": "2023-11-23 19:31:02.743353", + "modified_by": "Administrator", + "module": "Setup", + "name": "Quotation Lost Reason", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From d8245cef723575eaf52c2e39d25a6f9c83ce9ba0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 26 Nov 2023 16:13:39 +0530 Subject: [PATCH 33/44] fix: job card overlap validation (#38345) --- .../doctype/job_card/job_card.py | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index db6bc80838..f303531aee 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -185,7 +185,8 @@ class JobCard(Document): # override capacity for employee production_capacity = 1 - if time_logs and production_capacity > len(time_logs): + overlap_count = self.get_overlap_count(time_logs) + if time_logs and production_capacity > overlap_count: return {} if self.workstation_type and time_logs: @@ -195,6 +196,37 @@ class JobCard(Document): return time_logs[-1] + @staticmethod + def get_overlap_count(time_logs): + count = 1 + + # Check overlap exists or not between the overlapping time logs with the current Job Card + for idx, row in enumerate(time_logs): + next_idx = idx + if idx + 1 < len(time_logs): + next_idx = idx + 1 + next_row = time_logs[next_idx] + if row.name == next_row.name: + continue + + if ( + ( + get_datetime(next_row.from_time) >= get_datetime(row.from_time) + and get_datetime(next_row.from_time) <= get_datetime(row.to_time) + ) + or ( + get_datetime(next_row.to_time) >= get_datetime(row.from_time) + and get_datetime(next_row.to_time) <= get_datetime(row.to_time) + ) + or ( + get_datetime(next_row.from_time) <= get_datetime(row.from_time) + and get_datetime(next_row.to_time) >= get_datetime(row.to_time) + ) + ): + count += 1 + + return count + def get_time_logs(self, args, doctype, check_next_available_slot=False): jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType(doctype) @@ -211,7 +243,14 @@ class JobCard(Document): query = ( frappe.qb.from_(jctl) .from_(jc) - .select(jc.name.as_("name"), jctl.from_time, jctl.to_time, jc.workstation, jc.workstation_type) + .select( + jc.name.as_("name"), + jctl.name.as_("row_name"), + jctl.from_time, + jctl.to_time, + jc.workstation, + jc.workstation_type, + ) .where( (jctl.parent == jc.name) & (Criterion.any(time_conditions)) From 284a40aa63d09163e24b089cf0dcc517b8e62a91 Mon Sep 17 00:00:00 2001 From: Monolithon Administrator <5338127+monolithonadmin@users.noreply.github.com> Date: Sun, 26 Nov 2023 13:47:10 +0100 Subject: [PATCH 34/44] fix: Add name to Hungary - Chart of Accounts for Microenterprises json (#38278) Co-authored-by: Deepesh Garg --- ..._of_accounts_for_microenterprises_with_account_number.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json index 2cd6c0fc61..4013bb0926 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json @@ -1,4 +1,6 @@ { + "country_code": "hu", + "name": "Hungary - Chart of Accounts for Microenterprises", "tree": { "SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": { "account_number": 1, @@ -1651,4 +1653,4 @@ } } } -} \ No newline at end of file +} From 5426b93387c8a4599b11d5e064d4d8b37078aca1 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:07:55 +0100 Subject: [PATCH 35/44] refactor: bank transaction (#38182) --- .../bank_reconciliation_tool.py | 4 +- .../bank_transaction/bank_transaction.json | 17 ++- .../bank_transaction/bank_transaction.py | 139 ++++++++---------- 3 files changed, 77 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 7e2f763137..c2ddb39964 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -424,7 +424,9 @@ def reconcile_vouchers(bank_transaction_name, vouchers): vouchers = json.loads(vouchers) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction.add_payment_entries(vouchers) - return frappe.get_doc("Bank Transaction", bank_transaction_name) + transaction.save() + + return transaction @frappe.whitelist() diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index b32022e6fd..0328d51b89 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -13,6 +13,7 @@ "status", "bank_account", "company", + "amended_from", "section_break_4", "deposit", "withdrawal", @@ -25,10 +26,10 @@ "transaction_id", "transaction_type", "section_break_14", + "column_break_oufv", "payment_entries", "section_break_18", "allocated_amount", - "amended_from", "column_break_17", "unallocated_amount", "party_section", @@ -138,10 +139,12 @@ "fieldtype": "Section Break" }, { + "allow_on_submit": 1, "fieldname": "allocated_amount", "fieldtype": "Currency", "label": "Allocated Amount", - "options": "currency" + "options": "currency", + "read_only": 1 }, { "fieldname": "amended_from", @@ -157,10 +160,12 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "unallocated_amount", "fieldtype": "Currency", "label": "Unallocated Amount", - "options": "currency" + "options": "currency", + "read_only": 1 }, { "fieldname": "party_section", @@ -225,11 +230,15 @@ "fieldname": "bank_party_account_number", "fieldtype": "Data", "label": "Party Account No. (Bank Statement)" + }, + { + "fieldname": "column_break_oufv", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2023-06-06 13:58:12.821411", + "modified": "2023-11-18 18:32:47.203694", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 4649d23162..51c823a459 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,78 +2,73 @@ # For license information, please see license.txt import frappe +from frappe import _ from frappe.utils import flt from erpnext.controllers.status_updater import StatusUpdater class BankTransaction(StatusUpdater): - def after_insert(self): - self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) + def before_validate(self): + self.update_allocated_amount() - def on_submit(self): - self.clear_linked_payment_entries() + def validate(self): + self.validate_duplicate_references() + + def validate_duplicate_references(self): + """Make sure the same voucher is not allocated twice within the same Bank Transaction""" + if not self.payment_entries: + return + + pe = [] + for row in self.payment_entries: + reference = (row.payment_document, row.payment_entry) + if reference in pe: + frappe.throw( + _("{0} {1} is allocated twice in this Bank Transaction").format( + row.payment_document, row.payment_entry + ) + ) + pe.append(reference) + + def update_allocated_amount(self): + self.allocated_amount = ( + sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0 + ) + self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount + + def before_submit(self): + self.allocate_payment_entries() self.set_status() if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"): self.auto_set_party() - _saving_flag = False - - # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting - def on_update_after_submit(self): - "Run on save(). Avoid recursion caused by multiple saves" - if not self._saving_flag: - self._saving_flag = True - self.clear_linked_payment_entries() - self.update_allocations() - self._saving_flag = False + def before_update_after_submit(self): + self.validate_duplicate_references() + self.allocate_payment_entries() + self.update_allocated_amount() def on_cancel(self): - self.clear_linked_payment_entries(for_cancel=True) - self.set_status(update=True) + for payment_entry in self.payment_entries: + self.clear_linked_payment_entry(payment_entry, for_cancel=True) - def update_allocations(self): - "The doctype does not allow modifications after submission, so write to the db direct" - if self.payment_entries: - allocated_amount = sum(p.allocated_amount for p in self.payment_entries) - else: - allocated_amount = 0.0 - - amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.db_set("allocated_amount", flt(allocated_amount)) - self.db_set("unallocated_amount", amount - flt(allocated_amount)) - self.reload() self.set_status(update=True) def add_payment_entries(self, vouchers): "Add the vouchers with zero allocation. Save() will perform the allocations and clearance" if 0.0 >= self.unallocated_amount: - frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name)) + frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name)) - added = False for voucher in vouchers: - # Can't add same voucher twice - found = False - for pe in self.payment_entries: - if ( - pe.payment_document == voucher["payment_doctype"] - and pe.payment_entry == voucher["payment_name"] - ): - found = True - - if not found: - pe = { + self.append( + "payment_entries", + { "payment_document": voucher["payment_doctype"], "payment_entry": voucher["payment_name"], "allocated_amount": 0.0, # Temporary - } - child = self.append("payment_entries", pe) - added = True - - # runs on_update_after_submit - if added: - self.save() + }, + ) def allocate_payment_entries(self): """Refactored from bank reconciliation tool. @@ -90,6 +85,7 @@ class BankTransaction(StatusUpdater): - clear means: set the latest transaction date as clearance date """ remaining_amount = self.unallocated_amount + to_remove = [] for payment_entry in self.payment_entries: if payment_entry.allocated_amount == 0.0: unallocated_amount, should_clear, latest_transaction = get_clearance_details( @@ -99,49 +95,39 @@ class BankTransaction(StatusUpdater): if 0.0 == unallocated_amount: if should_clear: latest_transaction.clear_linked_payment_entry(payment_entry) - self.db_delete_payment_entry(payment_entry) + to_remove.append(payment_entry) elif remaining_amount <= 0.0: - self.db_delete_payment_entry(payment_entry) + to_remove.append(payment_entry) - elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount: - payment_entry.db_set("allocated_amount", unallocated_amount) + elif 0.0 < unallocated_amount <= remaining_amount: + payment_entry.allocated_amount = unallocated_amount remaining_amount -= unallocated_amount if should_clear: latest_transaction.clear_linked_payment_entry(payment_entry) - elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount: - payment_entry.db_set("allocated_amount", remaining_amount) + elif 0.0 < unallocated_amount: + payment_entry.allocated_amount = remaining_amount remaining_amount = 0.0 elif 0.0 > unallocated_amount: - self.db_delete_payment_entry(payment_entry) - frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount)) + frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount)) - self.reload() - - def db_delete_payment_entry(self, payment_entry): - frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name}) + for payment_entry in to_remove: + self.remove(to_remove) @frappe.whitelist() def remove_payment_entries(self): for payment_entry in self.payment_entries: self.remove_payment_entry(payment_entry) - # runs on_update_after_submit - self.save() + + self.save() # runs before_update_after_submit def remove_payment_entry(self, payment_entry): "Clear payment entry and clearance" self.clear_linked_payment_entry(payment_entry, for_cancel=True) self.remove(payment_entry) - def clear_linked_payment_entries(self, for_cancel=False): - if for_cancel: - for payment_entry in self.payment_entries: - self.clear_linked_payment_entry(payment_entry, for_cancel) - else: - self.allocate_payment_entries() - def clear_linked_payment_entry(self, payment_entry, for_cancel=False): clearance_date = None if for_cancel else self.date set_voucher_clearance( @@ -162,11 +148,10 @@ class BankTransaction(StatusUpdater): deposit=self.deposit, ).match() - if result: - party_type, party = result - frappe.db.set_value( - "Bank Transaction", self.name, field={"party_type": party_type, "party": party} - ) + if not result: + return + + self.party_type, self.party = result @frappe.whitelist() @@ -198,9 +183,7 @@ def get_clearance_details(transaction, payment_entry): if gle["gl_account"] == gl_bank_account: if gle["amount"] <= 0.0: frappe.throw( - frappe._("Voucher {0} value is broken: {1}").format( - payment_entry.payment_entry, gle["amount"] - ) + _("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"]) ) unmatched_gles -= 1 @@ -221,7 +204,7 @@ def get_clearance_details(transaction, payment_entry): def get_related_bank_gl_entries(doctype, docname): # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql - result = frappe.db.sql( + return frappe.db.sql( """ SELECT ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount, @@ -239,7 +222,6 @@ def get_related_bank_gl_entries(doctype, docname): dict(doctype=doctype, docname=docname), as_dict=True, ) - return result def get_total_allocated_amount(doctype, docname): @@ -365,6 +347,7 @@ def set_voucher_clearance(doctype, docname, clearance_date, self): if clearance_date: vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}] bt.add_payment_entries(vouchers) + bt.save() else: for pe in bt.payment_entries: if pe.payment_document == self.doctype and pe.payment_entry == self.name: From d9b3b9585472656c820fa241f32ed707f7df5c54 Mon Sep 17 00:00:00 2001 From: NandhiniDevi <95607404+Nandhinidevi123@users.noreply.github.com> Date: Sun, 26 Nov 2023 18:40:31 +0530 Subject: [PATCH 36/44] fix : Payment Reco Issue and chart of account importer (#38321) fix : Payment Reco Issue and chart of account importer --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 2 +- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 5a1c139bde..1e64eeeae6 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -113,7 +113,7 @@ def generate_data_from_csv(file_doc, as_dict=False): if as_dict: data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) else: - if not row[1]: + if not row[1] and len(row) > 1: row[1] = row[0] row[3] = row[2] data.append(row) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 1cff4c7f2d..0ad20c31c1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -508,7 +508,7 @@ class JournalEntry(AccountsController): ).format(d.reference_name, d.account) ) else: - dr_or_cr = "debit" if d.credit > 0 else "credit" + dr_or_cr = "debit" if flt(d.credit) > 0 else "credit" valid = False for jvd in against_entries: if flt(jvd[dr_or_cr]) > 0: From 592ce45da7659dcf4ca148f5924dbe0d783956bf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 27 Nov 2023 08:51:22 +0530 Subject: [PATCH 37/44] refactor: handle rounding loss on AR/AP reports --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 0e62ad61cc..7948e5f465 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -285,8 +285,8 @@ class ReceivablePayableReport(object): must_consider = False if self.filters.get("for_revaluation_journals"): - if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or ( - (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or ( + (abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision) ): must_consider = True else: From b9f5a1c85dc29acc22e704d866178a98f3035c1d Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 27 Nov 2023 09:05:22 +0530 Subject: [PATCH 38/44] fix: Negative Qty and Rates in SO/PO (#38252) * fix: Don't allow negative qty in SO/PO * fix: Type casting for safe comparisons * fix: Grammar in error message * fix: Negative rates should be allowed via Update Items in SO/PO * fix: Use `non_negative` property in docfield & emove code validation --- .../purchase_order/test_purchase_order.py | 17 ++++++++++- .../purchase_order_item.json | 3 +- erpnext/controllers/accounts_controller.py | 29 ++++++++++++++----- .../doctype/sales_order/test_sales_order.py | 29 +++++++++++++++++++ .../sales_order_item/sales_order_item.json | 3 +- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 55c01e8513..0f8574c84d 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.controllers.accounts_controller import update_child_qty_rate +from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.material_request.material_request import make_purchase_order @@ -27,6 +27,21 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( class TestPurchaseOrder(FrappeTestCase): + def test_purchase_order_qty(self): + po = create_purchase_order(qty=1, do_not_save=True) + po.append( + "items", + { + "item_code": "_Test Item", + "qty": -1, + "rate": 10, + }, + ) + self.assertRaises(frappe.NonNegativeError, po.save) + + po.items[1].qty = 0 + self.assertRaises(InvalidQtyError, po.save) + def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 2d706f41e5..98c1b388c1 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -214,6 +214,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "oldfieldname": "qty", "oldfieldtype": "Currency", "print_width": "60px", @@ -917,7 +918,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:34:27.267382", + "modified": "2023-11-24 13:24:41.298416", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e68894fa77..f551133a28 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -71,6 +71,10 @@ class AccountMissingError(frappe.ValidationError): pass +class InvalidQtyError(frappe.ValidationError): + pass + + force_item_fields = ( "item_group", "brand", @@ -911,10 +915,16 @@ class AccountsController(TransactionBase): return flt(args.get(field, 0) / self.get("conversion_rate", 1)) def validate_qty_is_not_zero(self): - if self.doctype != "Purchase Receipt": - for item in self.items: - if not item.qty: - frappe.throw(_("Item quantity can not be zero")) + if self.doctype == "Purchase Receipt": + return + + for item in self.items: + if not flt(item.qty): + frappe.throw( + msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx), + title=_("Invalid Quantity"), + exc=InvalidQtyError, + ) def validate_account_currency(self, account, account_currency=None): valid_currency = [self.company_currency] @@ -3142,16 +3152,19 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil conv_fac_precision = child_item.precision("conversion_factor") or 2 qty_precision = child_item.precision("qty") or 2 - if flt(child_item.billed_amt, rate_precision) > flt( - flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision - ): + # Amount cannot be lesser than billed amount, except for negative amounts + row_rate = flt(d.get("rate"), rate_precision) + amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt( + row_rate * flt(d.get("qty"), qty_precision), rate_precision + ) + if amount_below_billed_amt and row_rate > 0.0: frappe.throw( _("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format( child_item.idx, child_item.item_code ) ) else: - child_item.rate = flt(d.get("rate"), rate_precision) + child_item.rate = row_rate if d.get("conversion_factor"): if child_item.stock_uom == child_item.uom: diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d8b5878aa3..a518597aa6 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -51,6 +51,35 @@ class TestSalesOrder(FrappeTestCase): def tearDown(self): frappe.set_user("Administrator") + def test_sales_order_with_negative_rate(self): + """ + Test if negative rate is allowed in Sales Order via doc submission and update items + """ + so = make_sales_order(qty=1, rate=100, do_not_save=True) + so.append("items", {"item_code": "_Test Item", "qty": 1, "rate": -10}) + so.save() + so.submit() + + first_item = so.get("items")[0] + second_item = so.get("items")[1] + trans_item = json.dumps( + [ + { + "item_code": first_item.item_code, + "rate": first_item.rate, + "qty": first_item.qty, + "docname": first_item.name, + }, + { + "item_code": second_item.item_code, + "rate": -20, + "qty": second_item.qty, + "docname": second_item.name, + }, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) + def test_make_material_request(self): so = make_sales_order(do_not_submit=True) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index b4f73003ae..d4ccfc4753 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -200,6 +200,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "oldfieldname": "qty", "oldfieldtype": "Currency", "print_width": "100px", @@ -895,7 +896,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:12.787893", + "modified": "2023-11-24 13:24:55.756320", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 5a53a4b044be7e889080329056c7911970d92da8 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 27 Nov 2023 20:21:19 +0530 Subject: [PATCH 39/44] fix: make parameters of `create_subscription_process` optional (and other minor fixes) (#38360) --- .../doctype/process_subscription/process_subscription.py | 3 +-- erpnext/accounts/doctype/subscription/subscription.py | 2 +- erpnext/hooks.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py index 99269d6a7d..0aa9970cb8 100644 --- a/erpnext/accounts/doctype/process_subscription/process_subscription.py +++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py @@ -17,11 +17,10 @@ class ProcessSubscription(Document): def create_subscription_process( - subscription: str | None, posting_date: Union[str, datetime.date] | None + subscription: str | None = None, posting_date: Union[str, datetime.date] | None = None ): """Create a new Process Subscription document""" doc = frappe.new_doc("Process Subscription") doc.subscription = subscription doc.posting_date = getdate(posting_date) - doc.insert(ignore_permissions=True) doc.submit() diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 3cf7d284bb..a3d8c23418 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -676,7 +676,7 @@ def get_prorata_factor( def process_all( - subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None + subscription: str | None = None, posting_date: Optional["DateTimeLikeObject"] = None ) -> None: """ Task to updates the status of all `Subscription` apart from those that are cancelled diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c6ab6f12f6..857471f1fd 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -419,7 +419,6 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", ], "hourly_long": [ - "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.utilities.bulk_transaction.retry", ], @@ -450,6 +449,7 @@ scheduler_events = { "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", ], "daily_long": [ + "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", From ad3634be7c2607ab5c9c831ae01e5ddff7c9ba8b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Nov 2023 06:24:21 +0530 Subject: [PATCH 40/44] chore: fix flaky test case (#38369) --- .../doctype/work_order/test_work_order.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0ae7657c42..e2c8f07980 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -921,6 +921,20 @@ class TestWorkOrder(FrappeTestCase): "Test RM Item 2 for Scrap Item Test", ] + from_time = add_days(now(), -1) + job_cards = frappe.get_all( + "Job Card Time Log", + fields=["distinct parent as name", "docstatus"], + filters={"from_time": (">", from_time)}, + order_by="creation asc", + ) + + for job_card in job_cards: + if job_card.docstatus == 1: + frappe.get_doc("Job Card", job_card.name).cancel() + + frappe.delete_doc("Job Card Time Log", job_card.name) + company = "_Test Company with perpetual inventory" for item_code in items: create_item( From add238c892364ce25f3e7483cb5fd295f9666a56 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Nov 2023 08:21:45 +0530 Subject: [PATCH 41/44] fix: debit credit mismatch in multi-currecy asset purchase receipt (#38342) * fix: Debit credit mimatch in multicurrecy asset purchase receipt * test: multi currency purchase receipt * chore: update init files * test: roolback --- erpnext/assets/doctype/asset/test_asset.py | 103 ++++++------------ .../purchase_receipt/purchase_receipt.py | 2 +- erpnext/www/all-products/__init__.py | 0 erpnext/www/shop-by-category/__init__.py | 0 4 files changed, 37 insertions(+), 68 deletions(-) create mode 100644 erpnext/www/all-products/__init__.py create mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9e3ec6faa8..ca2b980579 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -149,12 +149,7 @@ class TestAsset(AssetSetup): ("Creditors - _TC", 0.0, 100000.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", - pi.name, - ) + gle = get_gl_entries("Purchase Invoice", pi.name) self.assertSequenceEqual(gle, expected_gle) pi.cancel() @@ -264,12 +259,7 @@ class TestAsset(AssetSetup): ), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Journal Entry' and voucher_no = %s - order by account""", - asset.journal_entry_for_scrap, - ) + gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap) self.assertSequenceEqual(gle, expected_gle) restore_asset(asset.name) @@ -345,13 +335,7 @@ class TestAsset(AssetSetup): ("Debtors - _TC", 25000.0, 0.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - + gle = get_gl_entries("Sales Invoice", si.name) self.assertSequenceEqual(gle, expected_gle) si.cancel() @@ -425,13 +409,7 @@ class TestAsset(AssetSetup): ("Debtors - _TC", 40000.0, 0.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - + gle = get_gl_entries("Sales Invoice", si.name) self.assertSequenceEqual(gle, expected_gle) def test_asset_with_maintenance_required_status_after_sale(self): @@ -572,13 +550,7 @@ class TestAsset(AssetSetup): ("CWIP Account - _TC", 5250.0, 0.0), ) - pr_gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no = %s - order by account""", - pr.name, - ) - + pr_gle = get_gl_entries("Purchase Receipt", pr.name) self.assertSequenceEqual(pr_gle, expected_gle) pi = make_invoice(pr.name) @@ -591,13 +563,7 @@ class TestAsset(AssetSetup): ("Creditors - _TC", 0.0, 5500.0), ) - pi_gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", - pi.name, - ) - + pi_gle = get_gl_entries("Purchase Invoice", pi.name) self.assertSequenceEqual(pi_gle, expected_gle) asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") @@ -624,13 +590,7 @@ class TestAsset(AssetSetup): expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0)) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Asset' and voucher_no = %s - order by account""", - asset_doc.name, - ) - + gle = get_gl_entries("Asset", asset_doc.name) self.assertSequenceEqual(gle, expected_gle) def test_asset_cwip_toggling_cases(self): @@ -653,10 +613,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 1 -- PR with cwip disabled, Asset with cwip enabled @@ -670,10 +627,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 2 -- PR with cwip enabled, Asset with cwip disabled @@ -686,10 +640,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertTrue(gle) # case 3 -- PI with cwip disabled, Asset with cwip enabled @@ -702,10 +653,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 4 -- PI with cwip enabled, Asset with cwip disabled @@ -718,10 +666,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertTrue(gle) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) @@ -1701,6 +1646,30 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, jv.insert) + def test_multi_currency_asset_pr_creation(self): + pr = make_purchase_receipt( + item_code="Macbook Pro", + qty=1, + rate=100.0, + location="Test Location", + supplier="_Test Supplier USD", + currency="USD", + ) + + pr.submit() + self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) + + +def get_gl_entries(doctype, docname): + gl_entry = frappe.qb.DocType("GL Entry") + return ( + frappe.qb.from_(gl_entry) + .select(gl_entry.account, gl_entry.debit, gl_entry.credit) + .where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname)) + .orderby(gl_entry.account) + .run() + ) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a7aa7e2ab4..8647528ea5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -571,7 +571,7 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.net_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount / self.conversion_rate) + flt(d.landed_cost_voucher_amount) ) diff --git a/erpnext/www/all-products/__init__.py b/erpnext/www/all-products/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 729fc738af7d78a6f75cb0f794f4c4451d74cd05 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 11:08:52 +0530 Subject: [PATCH 42/44] fix: product bundle search input --- .../doctype/product_bundle/product_bundle.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 2fd9cc1301..c934f0dffa 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -76,16 +76,19 @@ class ProductBundle(Document): @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") + item = frappe.qb.DocType("Item") - return ( + query = ( frappe.qb.from_(item) .select("*") .where( - (item.is_stock_item == 0) - & (item.is_fixed_asset == 0) - & (item.name.notin(product_bundles)) - & (item[searchfield].like(f"%{txt}%")) + (item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%")) ) .limit(page_len) .offset(start) - ).run() + ) + + if product_bundles: + query = query.where(item.name.notin(product_bundles)) + + return query.run() From 8c3713b649c7777e35be74b7afe437cec682359e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 11:12:55 +0530 Subject: [PATCH 43/44] fix: don't select all fields --- erpnext/selling/doctype/product_bundle/product_bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index c934f0dffa..3d4ffebbfb 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -80,7 +80,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): item = frappe.qb.DocType("Item") query = ( frappe.qb.from_(item) - .select("*") + .select(item.item_code, item.item_name) .where( (item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%")) ) From 8f00481c5f7742b120a232622fae7b3f7e3d2e86 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:35:28 +0100 Subject: [PATCH 44/44] fix: no fstring in translation (#38381) --- .../stock_and_account_value_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index b1da3ec1bd..416cf48871 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -166,4 +166,4 @@ def create_reposting_entries(rows, company): if entries: entries = ", ".join(entries) - frappe.msgprint(_(f"Reposting entries created: {entries}")) + frappe.msgprint(_("Reposting entries created: {0}").format(entries))