From d01a480149b2e27ed4d203eff5298ff3a1a1fb9d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 22 Nov 2023 09:59:32 +0530 Subject: [PATCH 01/44] refactor: optimize outstanding vouchers query (cherry picked from commit 8b04c1d4f6356bc332c5dd8f6f8711d778e030cd) --- .../test_payment_reconciliation.py | 1 + erpnext/accounts/utils.py | 22 +++++++++++++++++++ erpnext/selling/doctype/customer/customer.py | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 483b8014f5c..d7a73f0ce71 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1171,6 +1171,7 @@ class TestPaymentReconciliation(FrappeTestCase): # Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again. pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): customer = frappe.new_doc("Customer") diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 3da22cea7fc..ef9a8f36f28 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1826,6 +1826,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) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 7ef929fc222..78611f0ed76 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -663,7 +663,7 @@ def make_contact(args, is_primary_contact=1): "company_name": args.get(party_name_key), } ) - + contact = frappe.get_doc(values) if args.get("email_id"): From df70e048cfa2fa41002bfce08b02f523f591d442 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 21 Nov 2023 16:44:01 +0530 Subject: [PATCH 02/44] chore: rename 'unreconcile payments' to 'unreconcile payment' (cherry picked from commit 9006c9b7478ceee5f7662d62e8eb02a08a309906) --- .../{unreconcile_payments => unreconcile_payment}/__init__.py | 0 .../test_unreconcile_payment.py} | 2 +- .../unreconcile_payment.js} | 2 +- .../unreconcile_payment.json} | 2 +- .../unreconcile_payment.py} | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename erpnext/accounts/doctype/{unreconcile_payments => unreconcile_payment}/__init__.py (100%) rename erpnext/accounts/doctype/{unreconcile_payments/test_unreconcile_payments.py => unreconcile_payment/test_unreconcile_payment.py} (99%) rename erpnext/accounts/doctype/{unreconcile_payments/unreconcile_payments.js => unreconcile_payment/unreconcile_payment.js} (94%) rename erpnext/accounts/doctype/{unreconcile_payments/unreconcile_payments.json => unreconcile_payment/unreconcile_payment.json} (98%) rename erpnext/accounts/doctype/{unreconcile_payments/unreconcile_payments.py => unreconcile_payment/unreconcile_payment.py} (99%) diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/accounts/doctype/unreconcile_payment/__init__.py similarity index 100% rename from erpnext/accounts/doctype/unreconcile_payments/__init__.py rename to erpnext/accounts/doctype/unreconcile_payment/__init__.py diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py similarity index 99% rename from erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py rename to erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 78e04bff819..0f1a3511769 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -10,7 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): +class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js similarity index 94% rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js index c522567637f..70cefb13b57 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Unreconcile Payments", { +frappe.ui.form.on("Unreconcile Payment", { refresh(frm) { frm.set_query("voucher_type", function() { return { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json similarity index 98% rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json index f29e61b6ef6..17e0b4ddd31 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json @@ -61,7 +61,7 @@ "modified": "2023-08-28 17:42:50.261377", "modified_by": "Administrator", "module": "Accounts", - "name": "Unreconcile Payments", + "name": "Unreconcile Payment", "naming_rule": "Expression", "owner": "Administrator", "permissions": [ diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py similarity index 99% rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index 4f9fb50d463..9abd7523507 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -15,7 +15,7 @@ from erpnext.accounts.utils import ( ) -class UnreconcilePayments(Document): +class UnreconcilePayment(Document): def validate(self): self.supported_types = ["Payment Entry", "Journal Entry"] if not self.voucher_type in self.supported_types: From 100ce27a60fe70f7197e29b76fcf1d524c93f785 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 21 Nov 2023 16:51:06 +0530 Subject: [PATCH 03/44] chore: update new unreconcile doctype name in JS and PY files (cherry picked from commit 74f9e34182563b5dd3ef71d93b66596a12be5e91) --- .../doctype/journal_entry/journal_entry.js | 2 +- .../doctype/payment_entry/payment_entry.js | 4 ++-- .../doctype/payment_entry/payment_entry.py | 2 +- .../doctype/purchase_invoice/purchase_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.js | 4 ++-- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../test_unreconcile_payment.py | 8 ++++---- .../unreconcile_payment/unreconcile_payment.json | 4 ++-- .../unreconcile_payment/unreconcile_payment.py | 2 +- erpnext/controllers/accounts_controller.py | 6 +++--- erpnext/public/js/utils/unreconcile.js | 14 +++++++------- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 22b6880ad5e..9684a0d9d15 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -51,7 +51,7 @@ frappe.ui.form.on("Journal Entry", { }, __('Make')); } - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); }, before_save: function(frm) { if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9a6f8ec8ac1..26112409b7c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); @@ -160,7 +160,7 @@ frappe.ui.form.on('Payment Entry', { }, __('Actions')); } - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); }, validate_company: (frm) => { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cad20abfd3e..1af8f8d77c3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -148,7 +148,7 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", - "Unreconcile Payments", + "Unreconcile Payment", "Unreconcile Payment Entries", ) super(PaymentEntry, self).on_cancel() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 2eaa33767c9..4b0df12f454 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -180,7 +180,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm); } unblock_invoice() { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b1a7b10eea6..6763e446a5d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format @@ -184,7 +184,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); + erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm); } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index fa95ccdc57e..20b7382138f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -395,7 +395,7 @@ class SalesInvoice(SellingController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", - "Unreconcile Payments", + "Unreconcile Payment", "Unreconcile Payment Entries", "Payment Ledger Entry", "Serial and Batch Bundle", diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 0f1a3511769..f404d9981a3 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -73,7 +73,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe.doctype, "voucher_no": pe.name, @@ -138,7 +138,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe2.doctype, "voucher_no": pe2.name, @@ -196,7 +196,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe.doctype, "voucher_no": pe.name, @@ -281,7 +281,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): unreconcile = frappe.get_doc( { - "doctype": "Unreconcile Payments", + "doctype": "Unreconcile Payment", "company": self.company, "voucher_type": pe2.doctype, "voucher_no": pe2.name, diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json index 17e0b4ddd31..f906dc6cec6 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json @@ -21,7 +21,7 @@ "fieldtype": "Link", "label": "Amended From", "no_copy": 1, - "options": "Unreconcile Payments", + "options": "Unreconcile Payment", "print_hide": 1, "read_only": 1 }, @@ -90,4 +90,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index 9abd7523507..77906a78332 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -142,7 +142,7 @@ def create_unreconcile_doc_for_selection(selections=None): selections = frappe.json.loads(selections) # assuming each row is a unique voucher for row in selections: - unrecon = frappe.new_doc("Unreconcile Payments") + unrecon = frappe.new_doc("Unreconcile Payment") unrecon.company = row.get("company") unrecon.voucher_type = row.get("voucher_type") unrecon.voucher_no = row.get("voucher_no") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 35288617ad5..a84fa2330ff 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -239,7 +239,7 @@ class AccountsController(TransactionBase): references_map.setdefault(x.parent, []).append(x.name) for doc, rows in references_map.items(): - unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc) for row in rows: unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) @@ -248,9 +248,9 @@ class AccountsController(TransactionBase): unreconcile_doc.save(ignore_permissions=True) # delete docs upon parent doc deletion - unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name}) + unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name}) for x in unreconcile_docs: - _doc = frappe.get_doc("Unreconcile Payments", x.name) + _doc = frappe.get_doc("Unreconcile Payment", x.name) if _doc.docstatus == 1: _doc.cancel() _doc.delete() diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index fa00ed23620..79490a162d3 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -1,6 +1,6 @@ frappe.provide('erpnext.accounts'); -erpnext.accounts.unreconcile_payments = { +erpnext.accounts.unreconcile_payment = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry")) @@ -10,7 +10,7 @@ erpnext.accounts.unreconcile_payments = { } frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", + "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.doc_has_references", "args": { "doctype": frm.doc.doctype, "docname": frm.doc.name @@ -18,7 +18,7 @@ erpnext.accounts.unreconcile_payments = { callback: function(r) { if (r.message) { frm.add_custom_button(__("UnReconcile"), function() { - erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); + erpnext.accounts.unreconcile_payment.build_unreconcile_dialog(frm); }, __('Actions')); } } @@ -74,7 +74,7 @@ erpnext.accounts.unreconcile_payments = { // get linked payments frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.get_linked_payments_for_doc", "args": { "company": frm.doc.company, "doctype": frm.doc.doctype, @@ -96,8 +96,8 @@ erpnext.accounts.unreconcile_payments = { let selected_allocations = values.allocations.filter(x=>x.__checked); if (selected_allocations.length > 0) { - let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations); - erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map); + let selection_map = erpnext.accounts.unreconcile_payment.build_selection_map(frm, selected_allocations); + erpnext.accounts.unreconcile_payment.create_unreconcile_docs(selection_map); d.hide(); } else { @@ -115,7 +115,7 @@ erpnext.accounts.unreconcile_payments = { create_unreconcile_docs(selection_map) { frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection", + "method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.create_unreconcile_doc_for_selection", "args": { "selections": selection_map }, From 37d1f1ac67e9b2b62b51e0527637b9690e99234c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:13:32 +0530 Subject: [PATCH 04/44] fix: Supplier `Primary Contact` (backport #38268) (#38286) fix: Supplier `Primary Contact` (cherry picked from commit 627165dc7c6c1cd83d746b97c82210abeb9a2e29) Co-authored-by: s-aga-r --- 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 31bf439dbb4..b052f564a43 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 18613c595f32371997f17b24249f5d7fc13a8240 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:05:08 +0530 Subject: [PATCH 05/44] fix: don't depreciate assets with no schedule on scrapping (backport #38276) (#38293) fix: don't depreciate assets with no schedule on scrapping (#38276) fix: don't depreciate non-depreciable assets on scrapping (cherry picked from commit 816b1b6bd52b4f6adcb35d4039ad75892b0976ff) Co-authored-by: Anand Baburajan --- 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 84a428ca541..66930c0e7ce 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 72647b862469f8a6d5900f92a605088da4b8b131 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:09:23 +0100 Subject: [PATCH 06/44] feat: add Bank Transaction to connections in Journal and Payment Entry (backport #38297) (#38301) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../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 2eb54a54d54..906760ec312 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 4d50a35ed41..aa181564b06 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 4c9890a24e656bee30b1de74e192f8e82b2b57d9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:39:26 +0530 Subject: [PATCH 07/44] fix: patch - Duplicate entry quality inspection parameter (backport #38262) (#38264) fix: patch - Duplicate entry quality inspection parameter (#38262) (cherry picked from commit 0ca7527f7abddcab59edce434d0e68e1deebfc33) Co-authored-by: rohitwaghchaure --- erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index e53bdf8f19e..08ddbbf3375 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -21,6 +21,9 @@ def execute(): params = set({x.casefold(): x for x in params}.values()) for parameter in params: + if frappe.db.exists("Quality Inspection Parameter", parameter): + continue + frappe.get_doc( {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} ).insert(ignore_permissions=True) From 30c349b010d7ba5df29af1cfdeb7072b4f794321 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:42:37 +0530 Subject: [PATCH 08/44] add flt() for None type error (backport #38299) (#38306) add flt() for None type error (#38299) (cherry picked from commit 64b44a360af8c2d3a6a9cb99e03358b97cdf9fa5) Co-authored-by: NandhiniDevi <95607404+Nandhinidevi123@users.noreply.github.com> --- 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 85ef6f76d28..1cff4c7f2d4 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 99c1fbf9fc6cce11aad16a055d23a16c62925fed Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 23:39:53 +0530 Subject: [PATCH 09/44] fix: display all item rate stop messages (backport #38289) (#38307) fix: display all item rate stop messages (#38289) (cherry picked from commit 3f6d80503335a33bf2bff53a19870a6cdba00044) Co-authored-by: Devin Slauenwhite --- 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 7eba35dedd9..b083614a5f7 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 3720b7171bfea546fc5f80d2e0bb609d588ede0d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 11:44:34 +0530 Subject: [PATCH 10/44] chore: index to speed up queries on JE child table reference (cherry picked from commit 24fcd67f8bcf8cc8b387856983b97d8778eac084) --- .../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 3ba8cea94bb..3132fe9b12b 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 7282dd1a85b2ebea11f4fbd732a057a750fd766c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 16:56:35 +0530 Subject: [PATCH 11/44] chore: speed up Purchase Invoice cancellation (cherry picked from commit 1efff268b036dd27f7472bc7ec494aafac9edbc8) --- 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 d1677832ed9..f2094874e0e 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 314a91ac4d82d5487f74868dc12c808491544723 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 15:22:42 +0530 Subject: [PATCH 12/44] fix: skip fixed assets in parent (cherry picked from commit f9713eeb5690f9e71e01c6c570012f561a633c73) --- .../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 ac83c0f0462..4b401e7e67d 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 fcd53be1881554b16797689ea05ba248221fd07c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 15:23:17 +0530 Subject: [PATCH 13/44] feat: add disabled field in product bundle (cherry picked from commit ee76af76812fe04d86653fd1955ad133c8cb5df8) --- .../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 56155fb750a..c4f21b61b9e 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 fb517e823f66ee43e471bc74bca5bd6b959a4f53 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 15:24:56 +0530 Subject: [PATCH 14/44] fix: filter bundle items based on disabled check (cherry picked from commit 874774fe6c81c854fb3ade03e93f003c0fc8dd8a) --- 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 a9e9ad1a639..8b6f7158e04 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 5c12872f706d51b8e5b14da7db6564577e95ae70 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:04:05 +0530 Subject: [PATCH 15/44] fix: has_product_bundle util to only check for enabled bundles (cherry picked from commit 3543f86c639cf7383c37a5ed9a61d5787c9ebad4) --- 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 d34fbeb0dae..c3211b1d1c3 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 c0de9c0cef860ce10b1da13f8f4e0288af09ceb6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:10:49 +0530 Subject: [PATCH 16/44] fix: validation for existing bundles (cherry picked from commit 67f43d37df9e77c3e59562117fa44ea76273a536) --- 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 4b401e7e67d..2fd9cc13012 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 e4d9ef12938c8470df317ffce6b99fd34a3b6549 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:17:23 +0530 Subject: [PATCH 17/44] fix: condition in other bundle utils (cherry picked from commit 8bdb61cb87802723a0db1aaa29c30c15c740ec6b) --- 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 e36e97bc4b4..9091a77f994 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 a97198aa782..a23599b1806 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 c766cab0a16..d1a9cf26acc 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 3d46b323b324c62f42a37e423eb5ce2a63c26b88 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:26:09 +0530 Subject: [PATCH 18/44] fix: skip disabled bundles for non-report utils (cherry picked from commit 362f377f6127e9262ab7829a427d0a2051a77b2f) --- 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 66dd33a4000..f240136e9c2 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 d8935fe2030..cb34497f280 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 8b6f7158e04..35701c90deb 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 ed202095774..644df3d29a3 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 d076aca998a8ee6e1107c10a2bf6a3f2adf434ee Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 16:45:14 +0530 Subject: [PATCH 19/44] chore: linting issues (cherry picked from commit 16573378870bb9c6e80f39e80fa43e5b2cc0a69f) --- 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 c3211b1d1c3..5575a24b355 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 deed9f5840dea5485febcd18e29fcf1d9291fcf4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 17:39:48 +0530 Subject: [PATCH 20/44] perf: optimize total_purchase_cost update (cherry picked from commit aa17110bde44603574d4532afaa6cf1181423ac5) --- .../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 e1f0f1932e2..4356f803c7f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1279,13 +1279,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 28e695baf856dcda653b7998171015f8d0ce9168 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 17:53:42 +0530 Subject: [PATCH 21/44] refactor: provide UI button to recalculate when needed (cherry picked from commit bcbe6c4a531986836a0a2497e05bdbe1a327407a) --- 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 f366f775560..2dac399d88f 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 e9aed1afb4a..4f2e39539d5 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 c06388fe485a83070b570fa7205f9025248c3876 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 24 Nov 2023 10:53:00 +0530 Subject: [PATCH 22/44] refactor: make update_project_cost optional through Buying Settings (cherry picked from commit 0fe6dcd74288d5f2df1ffc37485e22ee96329b9e) --- .../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 059999245d1..0af93bfc902 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 7fc4e211bcc3246e4b7e73e5f38a3de77d7d4761 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 24 Nov 2023 10:54:31 +0530 Subject: [PATCH 23/44] refactor: update project costing based on frequency (cherry picked from commit dd016e6ced432b6f8754042fbad808b1cba56cec) --- .../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 4356f803c7f..32852643a19 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -525,7 +525,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() @@ -1260,7 +1264,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 2b423a6eace..55f79143bd1 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 ca2ad175d3ccd62289c0a9af26ba78debfbb170a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 18:38:33 +0530 Subject: [PATCH 24/44] fix: annual income and expenses in digest (cherry picked from commit 52305e3000decb84aad1a99557e13a0bb2b68ec4) --- 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 ef9a8f36f28..7c28d831ae7 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 4fc20e61036..b9e225728e8 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 b9a1fac7d8454399dad3f59093e88cec1f0c90dc Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 22 Nov 2023 19:28:57 +0530 Subject: [PATCH 25/44] fix: fiscal year using future date (cherry picked from commit 728cc9f725264e057d9331362b256ea8d0f80b83) --- 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 b9e225728e8..6ed44fff686 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 ea2c3487a3c33b86de3cb7532ffe9cecb174e897 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 26/44] feat: new Report "Lost Quotations" (#38309) (cherry picked from commit 477d9fa87e3cd7476f19930d60d23e347e46e658) --- .../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 280441f16fd..fd6da239212 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 00000000000..e69de29bb2d 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 00000000000..78e76cbf02a --- /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 00000000000..8915bab595e --- /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 00000000000..7c0bfbdd525 --- /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 5d778eec0b4..0eae08e8707 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 d6fe7eb10c9136ba4166f9f7ade032c2c7a645f5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:28:51 +0530 Subject: [PATCH 27/44] fix: job card overlap validation (backport #38345) (#38348) fix: job card overlap validation (#38345) (cherry picked from commit d8245cef723575eaf52c2e39d25a6f9c83ce9ba0) Co-authored-by: rohitwaghchaure --- .../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 db6bc80838f..f303531aee1 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 cda5ff40f18c406e232a34e83706b492ea1515ac Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 26 Nov 2023 18:57:01 +0530 Subject: [PATCH 28/44] refactor: bank transaction (#38182) refactor: bank transaction (#38182) (cherry picked from commit 5426b93387c8a4599b11d5e064d4d8b37078aca1) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../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 9a7a9a31d53..f01ae2e8d4d 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -355,7 +355,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 b32022e6fd8..0328d51b892 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 4649d231628..51c823a4592 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 3cbe59902aab249199297bc110ca2bbfc0e517ca Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 26 Nov 2023 22:25:51 +0530 Subject: [PATCH 29/44] fix(ux): Sales Order Stock Reservation Dialog (backport #38261) (#38344) * fix(ux): no need to select rows to reserve the stock (cherry picked from commit 9c889b37fb3e6572043c7a28706e43d051e2ff46) * fix: use field `sales_order_item` instead `name` (cherry picked from commit 73586fd9b2f9392d18f65a063b14ef2de2629615) * fix(ux): no need to select rows to unreserve the stock (cherry picked from commit 2a41da94d443dee51e615b395d18ad161d4e87fc) --------- Co-authored-by: s-aga-r --- .../doctype/sales_order/sales_order.js | 28 +++++++------------ erpnext/stock/doctype/pick_list/pick_list.py | 2 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../stock_reservation_entry.py | 10 +++++-- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3ad18daf193..97b214e33e5 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -214,13 +214,12 @@ frappe.ui.form.on("Sales Order", { label: __("Items to Reserve"), allow_bulk_edit: false, cannot_add_rows: true, - cannot_delete_rows: true, data: [], fields: [ { - fieldname: "name", + fieldname: "sales_order_item", fieldtype: "Data", - label: __("Name"), + label: __("Sales Order Item"), reqd: 1, read_only: 1, }, @@ -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(); }, @@ -292,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)) @@ -308,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,14 +312,13 @@ 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: [ { - fieldname: "name", + fieldname: "sre", fieldtype: "Link", - label: __("SRE"), + label: __("Stock Reservation Entry"), options: "Stock Reservation Entry", reqd: 1, read_only: 1, @@ -360,14 +355,14 @@ 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({ 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...'), @@ -377,9 +372,6 @@ frappe.ui.form.on("Sales Order", { } }); } - else { - frappe.msgprint(__("Please select items to unreserve.")); - } dialog.hide(); }, @@ -396,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/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 644df3d29a3..e7f620496cf 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 a5940f07d61..a7aa7e2ab4a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -781,7 +781,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 09542826f3c..cbfa4e0a432 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")) @@ -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 85bd649c6421c7675979b8413e6d0346af9d08f2 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 13 Nov 2023 20:24:32 +0530 Subject: [PATCH 30/44] refactor: validate reposting settings for editables inv (cherry picked from commit 780b827adcba571b46ee73404f9a038c36dd0eb9) --- .../repost_accounting_ledger.py | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 69cfe9fcd74..1d72a46c12f 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -10,12 +10,7 @@ from frappe.utils.data import comma_and class RepostAccountingLedger(Document): def __init__(self, *args, **kwargs): super(RepostAccountingLedger, self).__init__(*args, **kwargs) - self._allowed_types = [ - x.document_type - for x in frappe.db.get_all( - "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] - ) - ] + self._allowed_types = get_allowed_types_from_settings() def validate(self): self.validate_vouchers() @@ -56,15 +51,7 @@ class RepostAccountingLedger(Document): def validate_vouchers(self): if self.vouchers: - # Validate voucher types - voucher_types = set([x.voucher_type for x in self.vouchers]) - if disallowed_types := voucher_types.difference(self._allowed_types): - frappe.throw( - _("{0} types are not allowed. Only {1} are.").format( - frappe.bold(comma_and(list(disallowed_types))), - frappe.bold(comma_and(list(self._allowed_types))), - ) - ) + validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers]) def get_existing_ledger_entries(self): vouchers = [x.voucher_no for x in self.vouchers] @@ -168,6 +155,15 @@ def start_repost(account_repost_doc=str) -> None: frappe.db.commit() +def get_allowed_types_from_settings(): + return [ + x.document_type + for x in frappe.db.get_all( + "Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] + ) + ] + + def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): docs_with_deferred_revenue = frappe.db.get_all( "Sales Invoice Item", @@ -191,6 +187,25 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): ) +def validate_docs_for_voucher_types(doc_voucher_types): + allowed_types = get_allowed_types_from_settings() + # Validate voucher types + voucher_types = set(doc_voucher_types) + if disallowed_types := voucher_types.difference(allowed_types): + message = "are" if len(disallowed_types) > 1 else "is" + frappe.throw( + _("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format( + frappe.bold(comma_and(list(disallowed_types))), + message, + frappe.bold( + frappe.utils.get_link_to_form( + "Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings" + ) + ), + ) + ) + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters): From 6a3c3b4cac984ee24208e8a6ff80c4425a88a395 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 13 Nov 2023 20:25:26 +0530 Subject: [PATCH 31/44] fix: do not set repost flag without validating voucher (cherry picked from commit ad5edbb1de95befa1b6f312dcb7df6d8c5a8ce6c) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 32852643a19..c6ae9377a0d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -13,6 +13,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( validate_docs_for_deferred_accounting, + validate_docs_for_voucher_types, ) from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( check_if_return_invoice_linked_with_payment_entry, @@ -491,6 +492,7 @@ class PurchaseInvoice(BuyingController): def validate_for_repost(self): self.validate_write_off_account() self.validate_expense_account() + validate_docs_for_voucher_types(["Purchase Invoice"]) validate_docs_for_deferred_accounting([], [self.name]) def on_submit(self): From ac7615ac0191bbd8b9c7fd9bae9ac6d3b37d11af Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 13 Nov 2023 20:27:09 +0530 Subject: [PATCH 32/44] fix: allow on submit for child table fields (cherry picked from commit 5fae2f6d57bac332263d8c73e3d090f485b5844d) # Conflicts: # erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json --- .../purchase_invoice_item/purchase_invoice_item.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index bcedb7c9430..8fa6721720c 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -498,6 +498,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -505,6 +506,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "default": ":Company", "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "cost_center", @@ -916,7 +918,11 @@ "idx": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-11-14 18:33:48.547297", +======= + "modified": "2023-11-13 20:26:18.329983", +>>>>>>> 5fae2f6d57 (fix: allow on submit for child table fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From 25bf475d5a9feb8b41aafef9e4459aae4832c8c1 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 13 Nov 2023 20:28:44 +0530 Subject: [PATCH 33/44] fix: check reposting settings before allowing editable si (cherry picked from commit 894ae1fe0f8da1931f705d433c15b19410486186) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 20b7382138f..85cb3679c71 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -17,6 +17,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( ) from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( validate_docs_for_deferred_accounting, + validate_docs_for_voucher_types, ) from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, @@ -172,6 +173,7 @@ class SalesInvoice(SellingController): self.validate_write_off_account() self.validate_account_for_change_amount() self.validate_income_account() + validate_docs_for_voucher_types(["Sales Invoice"]) validate_docs_for_deferred_accounting([self.name], []) def validate_fixed_asset(self): From 378dc50aa49883dbcdbfcb67fcfd4741efad4e52 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 27 Nov 2023 09:09:04 +0530 Subject: [PATCH 34/44] chore: resolve conflict --- .../doctype/purchase_invoice_item/purchase_invoice_item.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 8fa6721720c..71796c9918d 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -918,11 +918,7 @@ "idx": 1, "istable": 1, "links": [], -<<<<<<< HEAD "modified": "2023-11-14 18:33:48.547297", -======= - "modified": "2023-11-13 20:26:18.329983", ->>>>>>> 5fae2f6d57 (fix: allow on submit for child table fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From 14174df86277ab3d6dee25f17156f637645ce89c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:30:16 +0530 Subject: [PATCH 35/44] fix: Negative Qty and Rates in SO/PO (backport #38252) (#38357) 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 (cherry picked from commit b9f5a1c85dc29acc22e704d866178a98f3035c1d) Co-authored-by: Marica --- .../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 55c01e85137..0f8574c84df 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 2d706f41e5e..98c1b388c14 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 a84fa2330ff..d1f24757ad4 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", @@ -910,10 +914,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] @@ -3139,16 +3149,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 d8b5878aa30..a518597aa6f 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 b4f73003aef..d4ccfc4753d 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 4699887f1c8b6b93de9971f4b810da95ea0afc9f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:31:12 +0530 Subject: [PATCH 36/44] fix: Payment Reco Issue and chart of account importer 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 5a1c139bdef..1e64eeeae63 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 1cff4c7f2d4..0ad20c31c15 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 8564d58afeac82d7ccc761a2e21ed35932c9bee6 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 (cherry picked from commit 592ce45da7659dcf4ca148f5924dbe0d783956bf) --- .../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 a2ade382d4a..28779cb7776 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -281,8 +281,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 721b429d9316e2901ac34f20bc9c6d71e8c00acc Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 27 Nov 2023 20:21:19 +0530 Subject: [PATCH 38/44] fix: make parameters of `create_subscription_process` optional (and other minor fixes) (#38360) (cherry picked from commit 5a53a4b044be7e889080329056c7911970d92da8) --- .../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 99269d6a7d5..0aa9970cb80 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 3cf7d284bbb..a3d8c234180 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 c6ab6f12f67..857471f1fda 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 a6f3a103db9f31326957f2f89a321b1a4e5722de Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:36:06 +0530 Subject: [PATCH 39/44] chore: fix flaky test case (backport #38369) (#38373) chore: fix flaky test case (#38369) (cherry picked from commit ad3634be7c2607ab5c9c831ae01e5ddff7c9ba8b) Co-authored-by: rohitwaghchaure --- .../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 c828c878eb7..802c23d660a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -920,6 +920,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 922aef665b25ef4c5642de46fbd817d14402e23e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:54:05 +0530 Subject: [PATCH 40/44] fix: Server Error while creating Product Bundle (backport #38377) (#38380) * fix: product bundle search input (cherry picked from commit 729fc738af7d78a6f75cb0f794f4c4451d74cd05) * fix: don't select all fields (cherry picked from commit 8c3713b649c7777e35be74b7afe437cec682359e) --------- Co-authored-by: s-aga-r --- .../doctype/product_bundle/product_bundle.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 2fd9cc13012..3d4ffebbfb4 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("*") + .select(item.item_code, item.item_name) .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 573c4d2bfcc053a8043b99c073c96e8d92636286 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 28 Nov 2023 17:11:35 +0530 Subject: [PATCH 41/44] chore: fix imports for renamed report (cherry picked from commit aee2e12f3944ae4db2dd77a31d0c544c1bacb65a) --- .../report/tds_computation_summary/tds_computation_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 82f97f18941..2b5566fb2ff 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -1,7 +1,7 @@ import frappe from frappe import _ -from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import ( +from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import ( get_result, get_tds_docs, ) From b65c22579d61973d0bc8cab25f8ee1c3b2cbdb8a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 18:31:23 +0530 Subject: [PATCH 42/44] fix: serial no status (backport #38391) (#38397) fix: serial no status (#38391) (cherry picked from commit 592fc81260779ec42b35c88c6b8de19a598911d0) Co-authored-by: rohitwaghchaure --- .../delivery_note/test_delivery_note.py | 19 +++++++++++++++ erpnext/stock/doctype/serial_no/serial_no.js | 19 +++++++++++++++ .../stock/doctype/serial_no/serial_no.json | 4 ++-- .../serial_no_ledger/serial_no_ledger.py | 24 +++++++++++++++---- erpnext/stock/serial_batch_bundle.py | 6 ++++- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 137c352e99a..94655747e43 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1247,6 +1247,25 @@ class TestDeliveryNote(FrappeTestCase): dn.reload() self.assertFalse(dn.items[0].target_warehouse) + def test_serial_no_status(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + "Test Serial Item For Status", + {"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TESTSERIAL.#####"}, + ) + + item_code = item.name + pi = make_purchase_receipt(qty=1, item_code=item.name) + pi.reload() + serial_no = get_serial_nos_from_bundle(pi.items[0].serial_and_batch_bundle) + + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + dn = create_delivery_note(qty=1, item_code=item_code, serial_no=serial_no) + dn.reload() + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/serial_no/serial_no.js b/erpnext/stock/doctype/serial_no/serial_no.js index 9d5555ed631..1cb9fd1800e 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.js +++ b/erpnext/stock/doctype/serial_no/serial_no.js @@ -18,3 +18,22 @@ cur_frm.cscript.onload = function() { frappe.ui.form.on("Serial No", "refresh", function(frm) { frm.toggle_enable("item_code", frm.doc.__islocal); }); + + +frappe.ui.form.on("Serial No", { + refresh(frm) { + frm.trigger("view_ledgers") + }, + + view_ledgers(frm) { + frm.add_custom_button(__("View Ledgers"), () => { + frappe.route_options = { + "item_code": frm.doc.item_code, + "serial_no": frm.doc.name, + "posting_date": frappe.datetime.now_date(), + "posting_time": frappe.datetime.now_time() + }; + frappe.set_route("query-report", "Serial No Ledger"); + }).addClass('btn-primary'); + } +}) \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index ed1b0af30a6..b4ece00fe64 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -269,7 +269,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "\nActive\nInactive\nExpired", + "options": "\nActive\nInactive\nDelivered\nExpired", "read_only": 1 }, { @@ -280,7 +280,7 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2023-04-16 15:58:46.139887", + "modified": "2023-11-28 15:37:59.489945", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index 7212b92bb31..ae12fbb3e4f 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -36,21 +36,27 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "company", "options": "Company", - "width": 150, + "width": 120, }, { "label": _("Warehouse"), "fieldtype": "Link", "fieldname": "warehouse", "options": "Warehouse", - "width": 150, + "width": 120, + }, + { + "label": _("Status"), + "fieldtype": "Data", + "fieldname": "status", + "width": 120, }, { "label": _("Serial No"), "fieldtype": "Link", "fieldname": "serial_no", "options": "Serial No", - "width": 150, + "width": 130, }, { "label": _("Valuation Rate"), @@ -58,6 +64,12 @@ def get_columns(filters): "fieldname": "valuation_rate", "width": 150, }, + { + "label": _("Qty"), + "fieldtype": "Float", + "fieldname": "qty", + "width": 150, + }, ] return columns @@ -83,12 +95,16 @@ def get_data(filters): "posting_time": row.posting_time, "voucher_type": row.voucher_type, "voucher_no": row.voucher_no, + "status": "Active" if row.actual_qty > 0 else "Delivered", "company": row.company, "warehouse": row.warehouse, + "qty": 1 if row.actual_qty > 0 else -1, } ) - serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []) + serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}] + if row.serial_and_batch_bundle: + serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []) for index, bundle_data in enumerate(serial_nos): if index == 0: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index da98455b5cb..de28be1c357 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -255,11 +255,15 @@ class SerialBatchBundle: if not serial_nos: return + status = "Inactive" + if self.sle.actual_qty < 0: + status = "Delivered" + sn_table = frappe.qb.DocType("Serial No") ( frappe.qb.update(sn_table) .set(sn_table.warehouse, warehouse) - .set(sn_table.status, "Active" if warehouse else "Inactive") + .set(sn_table.status, "Active" if warehouse else status) .where(sn_table.name.isin(serial_nos)) ).run() From b1b065daf1f79eb83834aaa033ecf89403e75877 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 28 Nov 2023 19:24:15 +0530 Subject: [PATCH 43/44] fix: create contact if existing customer doesn't have contact (cherry picked from commit 23b0b8ba36595f8d1a62e44f51967a4d9a56641f) --- erpnext/crm/doctype/lead/lead.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index e897ba41eb0..13dc291cf97 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -34,6 +34,15 @@ class Lead(SellingController, CRMNote): def before_insert(self): self.contact_doc = None if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"): + if self.source == "Existing Customer" and self.customer: + contact = frappe.db.get_value( + "Dynamic Link", + {"link_doctype": "Customer", "link_name": self.customer}, + "parent", + ) + if contact: + self.contact_doc = frappe.get_doc("Contact", contact) + return self.contact_doc = self.create_contact() def after_insert(self): From c1018555a0c8600a988b10483419dc0a92f2c415 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 21:36:05 +0530 Subject: [PATCH 44/44] fix: no fstring in translation (backport #38381) (#38387) fix: no fstring in translation (#38381) (cherry picked from commit 8f00481c5f7742b120a232622fae7b3f7e3d2e86) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../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 b1da3ec1bd1..416cf48871a 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))