From 1babc5f367e5f114741dd29f063d68b4c308b113 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 08:05:10 +0530 Subject: [PATCH 01/81] feat: separated discount accounting settings for sales and purchase --- .../buying_settings/buying_settings.json | 10 ++- .../buying_settings/buying_settings.py | 62 +++++++++++++++++++ .../selling_settings/selling_settings.json | 12 +++- .../selling_settings/selling_settings.py | 58 +++++++++++++++++ 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 50321baa2e..90e6975f64 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -20,6 +20,7 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", + "enable_discount_accounting", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -133,6 +134,13 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", + "fieldname": "enable_discount_accounting", + "fieldtype": "Check", + "label": "Enable Discount Accounting" } ], "icon": "fa fa-cog", @@ -140,7 +148,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-27 17:57:58.367048", + "modified": "2022-04-08 07:48:12.632498", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 5507254bbc..4d266e1264 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -6,9 +6,14 @@ import frappe from frappe.model.document import Document +from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.utils import cint class BuyingSettings(Document): + def on_update(self): + self.toggle_discount_accounting_fields() + def validate(self): for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: frappe.db.set_default(key, self.get(key, "")) @@ -21,3 +26,60 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) + + def toggle_discount_accounting_fields(self): + enable_discount_accounting = cint(self.enable_discount_accounting) + + make_property_setter( + "Purchase Invoice Item", + "discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Purchase Invoice Item", + "discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Purchase Invoice Item", + "discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) + + make_property_setter( + "Purchase Invoice", + "additional_discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Purchase Invoice", + "additional_discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Purchase Invoice", + "additional_discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 7c4a3f63dc..2c880ee029 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -27,7 +27,8 @@ "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", - "hide_tax_id" + "hide_tax_id", + "enable_discount_accounting" ], "fields": [ { @@ -164,6 +165,13 @@ "fieldname": "editable_bundle_item_rates", "fieldtype": "Check", "label": "Calculate Product Bundle Price based on Child Items' Rates" + }, + { + "default": "0", + "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", + "fieldname": "enable_discount_accounting", + "fieldtype": "Check", + "label": "Enable Discount Accounting" } ], "icon": "fa fa-cog", @@ -171,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-02-04 15:41:59.939261", + "modified": "2022-04-08 07:48:48.074220", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 29e4712be3..c656c52c17 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -14,6 +14,7 @@ class SellingSettings(Document): def on_update(self): self.toggle_hide_tax_id() self.toggle_editable_rate_for_bundle_items() + self.toggle_discount_accounting_fields() def validate(self): for key in [ @@ -58,3 +59,60 @@ class SellingSettings(Document): "Check", validate_fields_for_doctype=False, ) + + def toggle_discount_accounting_fields(self): + enable_discount_accounting = cint(self.enable_discount_accounting) + + make_property_setter( + "Sales Invoice Item", + "discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Sales Invoice Item", + "discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Sales Invoice Item", + "discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) + + make_property_setter( + "Sales Invoice", + "additional_discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Sales Invoice", + "additional_discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Sales Invoice", + "additional_discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) \ No newline at end of file From 0fcdf1b6137306dfadab0dc5f3389416d3898121 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 08:10:27 +0530 Subject: [PATCH 02/81] removed discount-accounting from account settings --- .../accounts_settings/accounts_settings.py | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 835498176c..3b125a2986 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -28,7 +28,6 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() - self.toggle_discount_accounting_fields() self.validate_pending_reposts() def validate_stale_days(self): @@ -52,74 +51,6 @@ class AccountsSettings(Document): validate_fields_for_doctype=False, ) - def toggle_discount_accounting_fields(self): - enable_discount_accounting = cint(self.enable_discount_accounting) - - for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: - make_property_setter( - doctype, - "discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - doctype, - "discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - doctype, - "discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - for doctype in ["Sales Invoice", "Purchase Invoice"]: - make_property_setter( - doctype, - "additional_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - doctype, - "additional_discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - doctype, - "additional_discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - make_property_setter( - "Item", - "default_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - def validate_pending_reposts(self): if self.acc_frozen_upto: check_pending_reposting(self.acc_frozen_upto) From b6ce49760132dac9b61d20fd05a68590004050ac Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 15:08:21 +0530 Subject: [PATCH 03/81] fix: removed discount account field from account settings --- .../doctype/accounts_settings/accounts_settings.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 9a35a247dd..417611fecd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,7 +18,6 @@ "automatically_fetch_payment_terms", "column_break_17", "enable_common_party_accounting", - "enable_discount_accounting", "report_setting_section", "use_custom_cash_flow", "deferred_accounting_settings_section", @@ -272,13 +271,6 @@ "fieldtype": "Check", "label": "Create Ledger Entries for Change Amount" }, - { - "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", - "fieldtype": "Check", - "label": "Enable Discount Accounting" - }, { "default": "0", "description": "Learn about Common Party", @@ -354,7 +346,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-02-04 12:32:36.805652", + "modified": "2022-04-08 14:45:06.796418", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 786887768ee5d7063915cdd30e3e222df22fa5de Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 08:10:27 +0530 Subject: [PATCH 04/81] fix: removed discount-accounting code from account settings --- .../accounts_settings/accounts_settings.py | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 835498176c..3b125a2986 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -28,7 +28,6 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() - self.toggle_discount_accounting_fields() self.validate_pending_reposts() def validate_stale_days(self): @@ -52,74 +51,6 @@ class AccountsSettings(Document): validate_fields_for_doctype=False, ) - def toggle_discount_accounting_fields(self): - enable_discount_accounting = cint(self.enable_discount_accounting) - - for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: - make_property_setter( - doctype, - "discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - doctype, - "discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - doctype, - "discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - for doctype in ["Sales Invoice", "Purchase Invoice"]: - make_property_setter( - doctype, - "additional_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - doctype, - "additional_discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - doctype, - "additional_discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - make_property_setter( - "Item", - "default_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - def validate_pending_reposts(self): if self.acc_frozen_upto: check_pending_reposting(self.acc_frozen_upto) From 171c60ff37d6e1f95378a049f0fec66189aeac90 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 15:08:21 +0530 Subject: [PATCH 05/81] fix: removed discount account field from account settings --- .../doctype/accounts_settings/accounts_settings.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 9a35a247dd..417611fecd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,7 +18,6 @@ "automatically_fetch_payment_terms", "column_break_17", "enable_common_party_accounting", - "enable_discount_accounting", "report_setting_section", "use_custom_cash_flow", "deferred_accounting_settings_section", @@ -272,13 +271,6 @@ "fieldtype": "Check", "label": "Create Ledger Entries for Change Amount" }, - { - "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", - "fieldtype": "Check", - "label": "Enable Discount Accounting" - }, { "default": "0", "description": "Learn about Common Party", @@ -354,7 +346,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-02-04 12:32:36.805652", + "modified": "2022-04-08 14:45:06.796418", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 35e875c11131465fe41be6a215c74cf81d47e67e Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 17:27:53 +0530 Subject: [PATCH 06/81] fix(patch): enable discount account in buying and selling if exist in accounts settings --- .../v14_0/discount_accounting_separation.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/patches/v14_0/discount_accounting_separation.py diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py new file mode 100644 index 0000000000..bb236f783a --- /dev/null +++ b/erpnext/patches/v14_0/discount_accounting_separation.py @@ -0,0 +1,14 @@ +import frappe + +def execute(): + doc = frappe.get_doc("Accounts Settings") + discount_account = doc.enable_discount_accounting + if discount_account: + buying_settings = frappe.get_doc("Buying Settings") + selling_settings = frappe.get_doc("Selling Settings") + + buying_settings.enable_discount_accounting = 1 + selling_settings.enable_discount_accounting = 1 + + buying_settings.save() + selling_settings.save() \ No newline at end of file From 2173c8b11478e76f32807a6253b28456ba016239 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 17:29:08 +0530 Subject: [PATCH 07/81] fix(pathces): added to patches.txt --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a3bf78b532..24354c3842 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -364,3 +364,4 @@ erpnext.patches.v13_0.add_cost_center_in_loans erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances +erpnext.patches.v14_0.discount_accounting_separation \ No newline at end of file From ac7f1cbd87efe933f785a3476a478bb773bb7601 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 17:46:49 +0530 Subject: [PATCH 08/81] fix: minor fix --- erpnext/patches/v14_0/discount_accounting_separation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py index bb236f783a..e7e4a2ebcf 100644 --- a/erpnext/patches/v14_0/discount_accounting_separation.py +++ b/erpnext/patches/v14_0/discount_accounting_separation.py @@ -4,8 +4,8 @@ def execute(): doc = frappe.get_doc("Accounts Settings") discount_account = doc.enable_discount_accounting if discount_account: - buying_settings = frappe.get_doc("Buying Settings") - selling_settings = frappe.get_doc("Selling Settings") + buying_settings = frappe.get_doc("Buying Settings", "Buying Settings") + selling_settings = frappe.get_doc("Selling Settings", "Selling Settings") buying_settings.enable_discount_accounting = 1 selling_settings.enable_discount_accounting = 1 From ac16f3b71fc403d2ad1f37f7ca54dc6236aa909c Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Fri, 8 Apr 2022 18:20:41 +0530 Subject: [PATCH 09/81] fix: lock timeout exceeded --- .../patches/v14_0/discount_accounting_separation.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py index e7e4a2ebcf..3d4907cca3 100644 --- a/erpnext/patches/v14_0/discount_accounting_separation.py +++ b/erpnext/patches/v14_0/discount_accounting_separation.py @@ -4,11 +4,5 @@ def execute(): doc = frappe.get_doc("Accounts Settings") discount_account = doc.enable_discount_accounting if discount_account: - buying_settings = frappe.get_doc("Buying Settings", "Buying Settings") - selling_settings = frappe.get_doc("Selling Settings", "Selling Settings") - - buying_settings.enable_discount_accounting = 1 - selling_settings.enable_discount_accounting = 1 - - buying_settings.save() - selling_settings.save() \ No newline at end of file + for doctype in ["Buying Settings", "Selling Settings"]: + doc = frappe.db.set_value(doctype, doctype, 'enable_discount_accounting', 1, update_modified=False) \ No newline at end of file From dc2944a0410811ce800107fbb3a531b5eb440498 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Sat, 9 Apr 2022 03:55:25 +0530 Subject: [PATCH 10/81] fix[minor]: removed doc assignment --- erpnext/patches/v14_0/discount_accounting_separation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py index 3d4907cca3..a814522a38 100644 --- a/erpnext/patches/v14_0/discount_accounting_separation.py +++ b/erpnext/patches/v14_0/discount_accounting_separation.py @@ -5,4 +5,4 @@ def execute(): discount_account = doc.enable_discount_accounting if discount_account: for doctype in ["Buying Settings", "Selling Settings"]: - doc = frappe.db.set_value(doctype, doctype, 'enable_discount_accounting', 1, update_modified=False) \ No newline at end of file + frappe.db.set_value(doctype, doctype, 'enable_discount_accounting', 1, update_modified=False) \ No newline at end of file From a7a574237502446885e3a2e2c255920e4045dcd7 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Mon, 11 Apr 2022 14:28:36 +0530 Subject: [PATCH 11/81] fix: merge conflict(patch) --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 24354c3842..46d6e93db5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -364,4 +364,5 @@ erpnext.patches.v13_0.add_cost_center_in_loans erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances +erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v14_0.discount_accounting_separation \ No newline at end of file From 0eff74c6500a229c5ff80f15a45fdf889add52e7 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Thu, 14 Apr 2022 15:42:16 +0530 Subject: [PATCH 12/81] fix: tabspacing --- .../patches/v14_0/discount_accounting_separation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py index a814522a38..fd498052d8 100644 --- a/erpnext/patches/v14_0/discount_accounting_separation.py +++ b/erpnext/patches/v14_0/discount_accounting_separation.py @@ -1,8 +1,9 @@ import frappe + def execute(): - doc = frappe.get_doc("Accounts Settings") - discount_account = doc.enable_discount_accounting - if discount_account: - for doctype in ["Buying Settings", "Selling Settings"]: - frappe.db.set_value(doctype, doctype, 'enable_discount_accounting', 1, update_modified=False) \ No newline at end of file + doc = frappe.get_doc("Accounts Settings") + discount_account = doc.enable_discount_accounting + if discount_account: + for doctype in ["Buying Settings", "Selling Settings"]: + frappe.db.set_value(doctype, doctype, "enable_discount_accounting", 1, update_modified=False) From 01fde15bd5a0c111eadd567323b15f04d10731cc Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Thu, 14 Apr 2022 15:48:55 +0530 Subject: [PATCH 13/81] fix: dependent codes updated --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 +++--- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 +++--- erpnext/controllers/accounts_controller.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e6a46d0676..a222dd98e9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -669,7 +669,7 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") ) provisional_accounting_for_non_stock_items = cint( frappe.db.get_value( @@ -1157,7 +1157,7 @@ class PurchaseInvoice(BuyingController): # tax table gl entries valuation_tax = {} enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") ) for tax in self.get("taxes"): @@ -1250,7 +1250,7 @@ class PurchaseInvoice(BuyingController): def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): self._enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") ) return self._enable_discount_accounting diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1efd3dca0d..dd85d99a8c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1051,7 +1051,7 @@ class SalesInvoice(SellingController): def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) for tax in self.get("taxes"): @@ -1097,7 +1097,7 @@ class SalesInvoice(SellingController): def make_item_gl_entries(self, gl_entries): # income account gl entries enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) for item in self.get("items"): @@ -1276,7 +1276,7 @@ class SalesInvoice(SellingController): def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): self._enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) return self._enable_discount_accounting diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7cbd2bd6c7..5ebbb7a3f0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1080,7 +1080,7 @@ class AccountsController(TransactionBase): def make_discount_gl_entries(self, gl_entries): enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) if enable_discount_accounting: From 4130493b589cea2e4bfb0fe98cc7fcca2e64f45f Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Thu, 14 Apr 2022 16:03:08 +0530 Subject: [PATCH 14/81] fix: Label typo --- erpnext/buying/doctype/buying_settings/buying_settings.json | 4 ++-- .../selling/doctype/selling_settings/selling_settings.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 90e6975f64..89a9448716 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -140,7 +140,7 @@ "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", "fieldname": "enable_discount_accounting", "fieldtype": "Check", - "label": "Enable Discount Accounting" + "label": "Enable Discount Accounting for Buying" } ], "icon": "fa fa-cog", @@ -148,7 +148,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-08 07:48:12.632498", + "modified": "2022-04-14 15:56:42.340223", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 2c880ee029..005e24cfbe 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -171,7 +171,7 @@ "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", "fieldname": "enable_discount_accounting", "fieldtype": "Check", - "label": "Enable Discount Accounting" + "label": "Enable Discount Accounting for Selling" } ], "icon": "fa fa-cog", @@ -179,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-08 07:48:48.074220", + "modified": "2022-04-14 16:01:29.405642", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From ebb7bc201b900df0f335b898494cc68b5a93366f Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Mon, 18 Apr 2022 14:53:45 +0530 Subject: [PATCH 15/81] fix: library import formatting --- erpnext/buying/doctype/buying_settings/buying_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 4d266e1264..c52b59e4c0 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -5,8 +5,8 @@ import frappe -from frappe.model.document import Document from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.model.document import Document from frappe.utils import cint @@ -26,7 +26,7 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) - + def toggle_discount_accounting_fields(self): enable_discount_accounting = cint(self.enable_discount_accounting) @@ -82,4 +82,4 @@ class BuyingSettings(Document): "", "Code", validate_fields_for_doctype=False, - ) \ No newline at end of file + ) From bcfbb3e9c8d3cfc6818d21032bcf619eb33668aa Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Mon, 18 Apr 2022 15:04:47 +0530 Subject: [PATCH 16/81] fix: code formatting --- erpnext/selling/doctype/selling_settings/selling_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index c656c52c17..6c09894251 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -115,4 +115,4 @@ class SellingSettings(Document): "", "Code", validate_fields_for_doctype=False, - ) \ No newline at end of file + ) From 0ac11a5b305ecd6002bef94cefe04eba65c13f87 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 Apr 2022 12:18:11 +0530 Subject: [PATCH 17/81] fix: First preference to parent cost center rather than round off cost center --- erpnext/accounts/general_ledger.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f52e517f73..4dbc4d6a72 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -355,7 +355,7 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_ def make_round_off_gle(gl_map, debit_credit_diff, precision): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - gl_map[0].company + gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no ) round_off_account_exists = False round_off_gle = frappe._dict() @@ -396,10 +396,17 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): gl_map.append(round_off_gle) -def get_round_off_account_and_cost_center(company): +def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + + # Give first preference to parent cost center for round off GLE + if frappe.db.has_column(voucher_type, "cost_center"): + parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") + if parent_cost_center: + round_off_cost_center = parent_cost_center + if not round_off_account: frappe.throw(_("Please mention Round Off Account in Company")) From f058755ad362d55d45906c4db4a7bbe440adf6e4 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Wed, 20 Apr 2022 13:24:15 +0530 Subject: [PATCH 18/81] fix: test cases updated --- .../purchase_invoice/test_purchase_invoice.py | 6 +++--- .../doctype/sales_invoice/test_sales_invoice.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 59bd637e41..a12f06bc96 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1589,9 +1589,9 @@ def unlink_payment_on_cancel_of_invoice(enable=1): def enable_discount_accounting(enable=1): - accounts_settings = frappe.get_doc("Accounts Settings") - accounts_settings.enable_discount_accounting = enable - accounts_settings.save() + buying_settings = frappe.get_doc("Buying Settings") + buying_settings.enable_discount_accounting = enable + buying_settings.save() def make_purchase_invoice(**args): diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index caa70d00ef..eaab94108d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2685,9 +2685,6 @@ class TestSalesInvoice(unittest.TestCase): ) def test_sales_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( - enable_discount_accounting, - ) enable_discount_accounting() @@ -2708,9 +2705,6 @@ class TestSalesInvoice(unittest.TestCase): enable_discount_accounting(enable=0) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( - enable_discount_accounting, - ) enable_discount_accounting() additional_discount_account = create_account( @@ -3181,6 +3175,12 @@ class TestSalesInvoice(unittest.TestCase): ) +def enable_discount_accounting(enable=1): + selling_settings = frappe.get_doc("Selling Settings") + selling_settings.enable_discount_accounting = enable + selling_settings.save() + + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = "INV-2020-.#####" From f3fa6ac4c20a87ed55c54ca738b9bedcbc777da3 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Wed, 20 Apr 2022 16:01:12 +0530 Subject: [PATCH 19/81] fix: account setting seperation gl discount account creation --- erpnext/controllers/accounts_controller.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8558cea0f0..78645e0d4f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1079,9 +1079,14 @@ class AccountsController(TransactionBase): return amount, base_amount def make_discount_gl_entries(self, gl_entries): - enable_discount_accounting = cint( - frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") - ) + if self.doctype == "Purchase Invoice": + enable_discount_accounting = cint( + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") + ) + elif self.doctype == "Sales Invoice": + enable_discount_accounting = cint( + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") + ) if enable_discount_accounting: if self.doctype == "Purchase Invoice": From 1e143e7479d78a6dd53eeef8a161a1cb6c4e50f1 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Wed, 20 Apr 2022 16:06:55 +0530 Subject: [PATCH 20/81] fix: use @change_settings to enable_discount_account --- .../purchase_invoice/test_purchase_invoice.py | 13 ++++--------- .../doctype/sales_invoice/test_sales_invoice.py | 14 +++----------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index a12f06bc96..30d26acf3a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import change_settings from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext @@ -336,8 +337,8 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + @change_settings("Buying Settings", {"enable_discount_accounting": 1}) def test_purchase_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() discount_account = create_account( account_name="Discount Account", @@ -353,10 +354,10 @@ class TestPurchaseInvoice(unittest.TestCase): ] check_gl_entries(self, pi.name, expected_gle, nowdate()) - enable_discount_accounting(enable=0) + @change_settings("Buying Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() + additional_discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", @@ -1588,12 +1589,6 @@ def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings.save() -def enable_discount_accounting(enable=1): - buying_settings = frappe.get_doc("Buying Settings") - buying_settings.enable_discount_accounting = enable - buying_settings.save() - - def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eaab94108d..98a5755f71 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -7,6 +7,7 @@ import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import make_autoname +from frappe.tests.utils import change_settings from frappe.utils import add_days, flt, getdate, nowdate import erpnext @@ -2684,10 +2685,9 @@ class TestSalesInvoice(unittest.TestCase): sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" ) + @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_sales_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() - discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", @@ -2702,11 +2702,10 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - enable_discount_accounting(enable=0) + @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() additional_discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", @@ -2737,7 +2736,6 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - enable_discount_accounting(enable=0) def test_asset_depreciation_on_sale_with_pro_rata(self): """ @@ -3175,12 +3173,6 @@ class TestSalesInvoice(unittest.TestCase): ) -def enable_discount_accounting(enable=1): - selling_settings = frappe.get_doc("Selling Settings") - selling_settings.enable_discount_accounting = enable - selling_settings.save() - - def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = "INV-2020-.#####" From 13092bcc2dbb312ef5367ca74f6bfe90e72950b2 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Wed, 20 Apr 2022 16:46:29 +0530 Subject: [PATCH 21/81] fix: enable discount roll_back --- .../doctype/purchase_invoice/test_purchase_invoice.py | 8 ++++++++ .../accounts/doctype/sales_invoice/test_sales_invoice.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 30d26acf3a..e894252f5d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -354,6 +354,7 @@ class TestPurchaseInvoice(unittest.TestCase): ] check_gl_entries(self, pi.name, expected_gle, nowdate()) + enable_discount_accounting(enable=0) @change_settings("Buying Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): @@ -389,6 +390,7 @@ class TestPurchaseInvoice(unittest.TestCase): ] check_gl_entries(self, pi.name, expected_gle, nowdate()) + enable_discount_accounting(enable=0) def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) @@ -1530,6 +1532,12 @@ class TestPurchaseInvoice(unittest.TestCase): company.save() +def enable_discount_accounting(enable=1): + buying_settings = frappe.get_doc("Buying Settings") + buying_settings.enable_discount_accounting = enable + buying_settings.save() + + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( """select account, debit, credit, posting_date diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 98a5755f71..e4df20f6b7 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2702,6 +2702,7 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + enable_discount_accounting(enable=0) @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): @@ -2736,6 +2737,7 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + enable_discount_accounting(enable=0) def test_asset_depreciation_on_sale_with_pro_rata(self): """ @@ -3173,6 +3175,12 @@ class TestSalesInvoice(unittest.TestCase): ) +def enable_discount_accounting(enable=1): + selling_settings = frappe.get_doc("Selling Settings") + selling_settings.enable_discount_accounting = enable + selling_settings.save() + + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = "INV-2020-.#####" From c3e27b5556bc2ffe1c4bd6ab0b3731b8017af04c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 Apr 2022 19:07:53 +0530 Subject: [PATCH 22/81] fix: Loan doctypes in bank reconciliation --- .../doctype/bank_clearance/bank_clearance.py | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 96779d75be..ed5a699463 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -5,7 +5,10 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.utils import flt, fmt_money, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt, fmt_money, getdate + +import erpnext form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} @@ -76,6 +79,52 @@ class BankClearance(Document): as_dict=1, ) + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + + loan_disbursements = ( + frappe.qb.from_(loan_disbursement) + .select( + ConstantColumn("Loan Disbursement").as_("payment_document"), + loan_disbursement.name.as_("payment_entry"), + loan_disbursement.disbursed_amount.as_("credit"), + ConstantColumn(0).as_("debit"), + loan_disbursement.reference_number.as_("cheque_number"), + loan_disbursement.reference_date.as_("cheque_date"), + loan_disbursement.disbursement_date.as_("posting_date"), + loan_disbursement.applicant.as_("against_account"), + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.disbursement_date >= self.from_date) + .where(loan_disbursement.disbursement_date <= self.to_date) + .where(loan_disbursement.clearance_date.isnull()) + .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) + .orderby(loan_disbursement.disbursement_date) + .orderby(loan_disbursement.name, frappe.qb.desc) + ).run(as_dict=1) + + loan_repayment = frappe.qb.DocType("Loan Repayment") + + loan_repayments = ( + frappe.qb.from_(loan_repayment) + .select( + ConstantColumn("Loan Repayment").as_("doctype"), + loan_repayment.name.as_("payment_entry"), + loan_repayment.amount_paid.as_("debit"), + ConstantColumn(0).as_("credit"), + loan_repayment.reference_number.as_("cheque_number"), + loan_repayment.reference_date.as_("cheque_date"), + loan_repayment.applicant.as_("against_account"), + loan_repayment.posting_date, + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.clearance_date.isnull()) + .where(loan_repayment.posting_date >= self.from_date) + .where(loan_repayment.posting_date <= self.to_date) + .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) + .orderby(loan_repayment.posting_date) + .orderby(loan_repayment.name, frappe.qb.desc) + ).run(as_dict=1) + pos_sales_invoices, pos_purchase_invoices = [], [] if self.include_pos_transactions: pos_sales_invoices = frappe.db.sql( @@ -114,18 +163,26 @@ class BankClearance(Document): entries = sorted( list(payment_entries) - + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), - key=lambda k: k["posting_date"] or getdate(nowdate()), + + list(journal_entries) + + list(pos_sales_invoices) + + list(pos_purchase_invoices) + + list(loan_disbursements) + + list(loan_repayments), + key=lambda k: getdate(k["posting_date"]), ) self.set("payment_entries", []) self.total_amount = 0.0 + default_currency = erpnext.get_default_currency() for d in entries: row = self.append("payment_entries", {}) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) + if not d.get("account_currency"): + d.account_currency = default_currency + formatted_amount = fmt_money(abs(amount), 2, d.account_currency) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) From 3d0e68acaa82aa0e1a6ab50e835f192297bd7bd2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 Apr 2022 19:49:53 +0530 Subject: [PATCH 23/81] fix: select doctype as payment_document --- erpnext/accounts/doctype/bank_clearance/bank_clearance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index ed5a699463..0f617b5dda 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -107,7 +107,7 @@ class BankClearance(Document): loan_repayments = ( frappe.qb.from_(loan_repayment) .select( - ConstantColumn("Loan Repayment").as_("doctype"), + ConstantColumn("Loan Repayment").as_("payment_document"), loan_repayment.name.as_("payment_entry"), loan_repayment.amount_paid.as_("debit"), ConstantColumn(0).as_("credit"), @@ -185,6 +185,7 @@ class BankClearance(Document): formatted_amount = fmt_money(abs(amount), 2, d.account_currency) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) + d.posting_date = getdate(d.posting_date) d.pop("credit") d.pop("debit") From 2c78cc2014496ed7f1b358d19b66cacecf8967ec Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Thu, 21 Apr 2022 02:40:59 +0530 Subject: [PATCH 24/81] fix: removed manual rollback - enable discount account --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 2 -- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e894252f5d..42455bf431 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -354,7 +354,6 @@ class TestPurchaseInvoice(unittest.TestCase): ] check_gl_entries(self, pi.name, expected_gle, nowdate()) - enable_discount_accounting(enable=0) @change_settings("Buying Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): @@ -390,7 +389,6 @@ class TestPurchaseInvoice(unittest.TestCase): ] check_gl_entries(self, pi.name, expected_gle, nowdate()) - enable_discount_accounting(enable=0) def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e4df20f6b7..3c52c6ed9a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2702,7 +2702,6 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - enable_discount_accounting(enable=0) @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): @@ -2737,7 +2736,6 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - enable_discount_accounting(enable=0) def test_asset_depreciation_on_sale_with_pro_rata(self): """ From 02e17dbded5cd954563090e659bebd409a808559 Mon Sep 17 00:00:00 2001 From: rahib-hassan Date: Thu, 21 Apr 2022 02:42:42 +0530 Subject: [PATCH 25/81] fix: removed manual enable-discount-account method --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3c52c6ed9a..98a5755f71 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3173,12 +3173,6 @@ class TestSalesInvoice(unittest.TestCase): ) -def enable_discount_accounting(enable=1): - selling_settings = frappe.get_doc("Selling Settings") - selling_settings.enable_discount_accounting = enable - selling_settings.save() - - def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = "INV-2020-.#####" From c42547d40f5b23dd0c0a045005c49677863215fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 21 Apr 2022 13:26:44 +0530 Subject: [PATCH 26/81] fix: Use parent cost center for Sales and Purchase Invoice --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 4 +++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a5f9e24e15..e37900b02e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1369,7 +1369,9 @@ class PurchaseInvoice(BuyingController): if ( not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) gl_entries.append( self.get_gl_dict( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1efd3dca0d..5b7f1ce8fd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1466,7 +1466,9 @@ class SalesInvoice(SellingController): and self.base_rounding_adjustment and not self.is_internal_transfer() ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Sales Invoice", self.name + ) gl_entries.append( self.get_gl_dict( From 8a8476bb5c9e487a80d0631b685a4a596caa297e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 21 Apr 2022 18:53:18 +0530 Subject: [PATCH 27/81] test: Add test coverage for bank clearance --- .../bank_clearance/test_bank_clearance.py | 121 +++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index 706fbbe245..fbc44ee312 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -1,9 +1,126 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_months, getdate + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.loan_management.doctype.loan.test_loan import ( + create_loan, + create_loan_accounts, + create_loan_type, + create_repayment_entry, + make_loan_disbursement_entry, +) + class TestBankClearance(unittest.TestCase): - pass + @classmethod + def setUpClass(cls): + make_bank_account() + create_loan_accounts() + create_loan_masters() + add_transactions() + + @classmethod + def tearDownClass(cls): + payment_entry = frappe.get_doc( + "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} + ) + payment_entry.cancel() + payment_entry.delete() + + loan = frappe.get_doc("Loan", {"applicant": "_Test Customer"}) + + ld_doc = frappe.get_doc("Loan Disbursement", {"against_loan": loan.name}) + ld_doc.cancel() + ld_doc.delete() + + lr_doc = frappe.get_doc("Loan Repayment", {"against_loan": loan.name}) + lr_doc.cancel() + lr_doc.delete() + + lia = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) + lia.delete() + + plia = frappe.get_doc("Process Loan Interest Accrual", {"loan": loan.name}) + plia.cancel() + plia.delete() + + loan.load_from_db() + loan.cancel() + loan.flags.ignore_links = True + loan.delete() + + # Basic test case to test if bank clearance tool doesn't break + # Detailed test can be added later + def test_bank_clearance(self): + bank_clearance = frappe.get_doc("Bank Clearance") + bank_clearance.account = "_Test Bank Clearance - _TC" + bank_clearance.from_date = add_months(getdate(), -1) + bank_clearance.to_date = getdate() + bank_clearance.get_payment_entries() + self.assertEqual(len(bank_clearance.payment_entries), 3) + + +def make_bank_account(): + if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"): + frappe.get_doc( + { + "doctype": "Account", + "account_type": "Bank", + "account_name": "_Test Bank Clearance", + "company": "_Test Company", + "parent_account": "Bank Accounts - _TC", + } + ).insert() + + +def create_loan_masters(): + create_loan_type( + "Clearance Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "_Test Bank Clearance - _TC", + "_Test Bank Clearance - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) + + +def add_transactions(): + make_payment_entry() + make_loan() + + +def make_loan(): + loan = create_loan( + "_Test Customer", + "Clearance Loan", + 280000, + "Repay Over Number of Periods", + 20, + applicant_type="Customer", + ) + loan.submit() + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate()) + repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount) + repayment_entry.save() + repayment_entry.submit() + + +def make_payment_entry(): + pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690) + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC") + pe.reference_no = "Conrad Oct 18" + pe.reference_date = "2018-10-24" + pe.insert() + pe.submit() From 9bb132fdd3d98a149427be3341e268d818e3bfa1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 22 Apr 2022 13:39:43 +0530 Subject: [PATCH 28/81] fix: Do not validate while creating accounting dimension --- .../doctype/accounting_dimension/accounting_dimension.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 445378300b..3f1998a3b3 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): if doctype == "Budget": add_dimension_to_budget_doctype(df.copy(), doc) else: - create_custom_field(doctype, df) + create_custom_field(doctype, df, ignore_validate=True) count += 1 @@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc): } ) - create_custom_field("Budget", df) + create_custom_field("Budget", df, ignore_validate=True) property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") From 0eacc99ab705af446a0464f09ac92af760f5d9cf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Apr 2022 12:08:30 +0530 Subject: [PATCH 29/81] test: Fixes in test case --- .../bank_clearance/test_bank_clearance.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index fbc44ee312..c09aa1c013 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -30,25 +30,33 @@ class TestBankClearance(unittest.TestCase): payment_entry = frappe.get_doc( "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} ) - payment_entry.cancel() - payment_entry.delete() + + if payment_entry.docstatus == 1: + payment_entry.cancel() + payment_entry.delete() loan = frappe.get_doc("Loan", {"applicant": "_Test Customer"}) ld_doc = frappe.get_doc("Loan Disbursement", {"against_loan": loan.name}) - ld_doc.cancel() - ld_doc.delete() + + if ld_doc.docstatus == 1: + ld_doc.cancel() + ld_doc.delete() lr_doc = frappe.get_doc("Loan Repayment", {"against_loan": loan.name}) - lr_doc.cancel() - lr_doc.delete() + + if lr_doc.docstatus == 1: + lr_doc.cancel() + lr_doc.delete() lia = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) lia.delete() plia = frappe.get_doc("Process Loan Interest Accrual", {"loan": loan.name}) - plia.cancel() - plia.delete() + + if plia.docstatus == 1: + plia.cancel() + plia.delete() loan.load_from_db() loan.cancel() From d4d83f4bb6f34353347f4b613bfe7c9554343425 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Apr 2022 12:33:35 +0530 Subject: [PATCH 30/81] test: Fixes in test case --- .../accounts/doctype/bank_clearance/test_bank_clearance.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index c09aa1c013..18645c7517 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -27,6 +27,8 @@ class TestBankClearance(unittest.TestCase): @classmethod def tearDownClass(cls): + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1) + payment_entry = frappe.get_doc( "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} ) @@ -63,6 +65,8 @@ class TestBankClearance(unittest.TestCase): loan.flags.ignore_links = True loan.delete() + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0) + # Basic test case to test if bank clearance tool doesn't break # Detailed test can be added later def test_bank_clearance(self): From 015812b0b8216d0d66e005700691dfa1ace74bf7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Apr 2022 21:40:08 +0530 Subject: [PATCH 31/81] fix: Add accounting dimensions for round off GL Entry --- erpnext/accounts/general_ledger.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 4dbc4d6a72..2d36835869 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -392,10 +392,22 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): } ) + update_accounting_dimensions(round_off_gle) + if not round_off_account_exists: gl_map.append(round_off_gle) +def update_accounting_dimensions(round_off_gle): + dimensions = get_accounting_dimensions() + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) + + def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] From c312cd3725680cff91aa9e41da5a40fa91af7784 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 24 Apr 2022 18:11:32 +0530 Subject: [PATCH 32/81] fix: Check if accounting dimension exists --- erpnext/accounts/general_ledger.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 2d36835869..56f84c2392 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -400,12 +400,20 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): def update_accounting_dimensions(round_off_gle): dimensions = get_accounting_dimensions() - dimension_values = frappe.db.get_value( - round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions - ) + meta = frappe.get_meta(round_off_gle["voucher_type"]) + has_all_dimensions = True for dimension in dimensions: - round_off_gle[dimension] = dimension_values.get(dimension) + if not meta.has_field(dimension): + has_all_dimensions = False + + if dimensions and has_all_dimensions: + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): @@ -413,8 +421,10 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + meta = frappe.get_meta(voucher_type) + # Give first preference to parent cost center for round off GLE - if frappe.db.has_column(voucher_type, "cost_center"): + if meta.has_field("cost_center"): parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") if parent_cost_center: round_off_cost_center = parent_cost_center From c515abc392a8ae6b230ff39e32aae6a8ca4a6dfe Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 24 Apr 2022 19:20:29 +0530 Subject: [PATCH 33/81] test: Remove teardown method --- .../bank_clearance/test_bank_clearance.py | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index 18645c7517..c1e55f6f72 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -25,48 +25,6 @@ class TestBankClearance(unittest.TestCase): create_loan_masters() add_transactions() - @classmethod - def tearDownClass(cls): - frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1) - - payment_entry = frappe.get_doc( - "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} - ) - - if payment_entry.docstatus == 1: - payment_entry.cancel() - payment_entry.delete() - - loan = frappe.get_doc("Loan", {"applicant": "_Test Customer"}) - - ld_doc = frappe.get_doc("Loan Disbursement", {"against_loan": loan.name}) - - if ld_doc.docstatus == 1: - ld_doc.cancel() - ld_doc.delete() - - lr_doc = frappe.get_doc("Loan Repayment", {"against_loan": loan.name}) - - if lr_doc.docstatus == 1: - lr_doc.cancel() - lr_doc.delete() - - lia = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) - lia.delete() - - plia = frappe.get_doc("Process Loan Interest Accrual", {"loan": loan.name}) - - if plia.docstatus == 1: - plia.cancel() - plia.delete() - - loan.load_from_db() - loan.cancel() - loan.flags.ignore_links = True - loan.delete() - - frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0) - # Basic test case to test if bank clearance tool doesn't break # Detailed test can be added later def test_bank_clearance(self): From d6a43a39aef6915276e9d433f0d2d085c045ab27 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 11:46:01 +0530 Subject: [PATCH 34/81] test: tie breaking in SLEs (#30796) [skip ci] --- .../test_stock_ledger_entry.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index b4fac82935..5850ec7be6 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -2,12 +2,12 @@ # See license.txt import json -from datetime import timedelta from uuid import uuid4 import frappe from frappe.core.page.permission_manager.permission_manager import reset from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.query_builder.functions import CombineDatetime from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, today from frappe.utils.data import add_to_date @@ -1126,6 +1126,63 @@ class TestStockLedgerEntry(FrappeTestCase): # original amount self.assertEqual(50, _get_stock_credit(final_consumption)) + def test_tie_breaking(self): + frappe.flags.dont_execute_stock_reposts = True + self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + posting_date = "2022-01-01" + posting_time = "00:00:01" + sle = frappe.qb.DocType("Stock Ledger Entry") + + def ordered_qty_after_transaction(): + return ( + frappe.qb.from_(sle) + .select("qty_after_transaction") + .where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.creation) + ).run(pluck=True) + + first = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=10, + posting_time=posting_time, + posting_date=posting_date, + do_not_submit=True, + ) + second = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=1, + posting_date=posting_date, + posting_time=posting_time, + do_not_submit=True, + ) + + first.submit() + second.submit() + + self.assertEqual([10, 11], ordered_qty_after_transaction()) + + first.cancel() + self.assertEqual([1], ordered_qty_after_transaction()) + + backdated = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=1, + posting_date="2021-01-01", + posting_time=posting_time, + ) + self.assertEqual([1, 2], ordered_qty_after_transaction()) + + backdated.cancel() + self.assertEqual([1], ordered_qty_after_transaction()) + def create_repack_entry(**args): args = frappe._dict(args) From 02aaa6546ce4766aed84908bca79747cd77b0250 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 25 Apr 2022 12:42:06 +0530 Subject: [PATCH 35/81] chore: Remove dead code --- .../doctype/purchase_invoice/test_purchase_invoice.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 42455bf431..30d26acf3a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1530,12 +1530,6 @@ class TestPurchaseInvoice(unittest.TestCase): company.save() -def enable_discount_accounting(enable=1): - buying_settings = frappe.get_doc("Buying Settings") - buying_settings.enable_discount_accounting = enable - buying_settings.save() - - def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( """select account, debit, credit, posting_date From 8e6c7a6bf7b1b934ae239a77e8767c8d8e9c375b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 25 Apr 2022 14:02:46 +0530 Subject: [PATCH 36/81] fix(india): cess value not considered while validating e-invoice totals --- erpnext/regional/india/e_invoice/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index f317569312..5518e1cb35 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -552,6 +552,7 @@ def validate_totals(einvoice): + flt(value_details["CgstVal"]) + flt(value_details["SgstVal"]) + flt(value_details["IgstVal"]) + + flt(value_details["CesVal"]) + flt(value_details["OthChrg"]) + flt(value_details["RndOffAmt"]) - flt(value_details["Discount"]) From 3fa1c634790095bf7eabc135ed717e124efa4ff0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 25 Apr 2022 16:29:26 +0530 Subject: [PATCH 37/81] test: Unit test for round off entry dimensions --- .../sales_invoice/test_sales_invoice.py | 23 +++++++++++++++++++ erpnext/accounts/general_ledger.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index caa70d00ef..b22ec44f8a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1977,6 +1977,13 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_rounding_adjustment_3(self): + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + disable_dimension, + ) + + create_dimension() + si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: @@ -2004,6 +2011,10 @@ class TestSalesInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + + si.cost_center = "_Test Cost Center 2 - _TC" + si.location = "Block 1" + si.save() si.submit() self.assertEqual(si.net_total, 4007.16) @@ -2039,6 +2050,18 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(debit_credit_diff, 0) + round_off_gle = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"}, + ["cost_center", "location"], + as_dict=1, + ) + + self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") + self.assertEqual(round_off_gle.location, "Block 1") + + disable_dimension() + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 56f84c2392..89034ebb8c 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -409,7 +409,7 @@ def update_accounting_dimensions(round_off_gle): if dimensions and has_all_dimensions: dimension_values = frappe.db.get_value( - round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1 ) for dimension in dimensions: From d3582ea399bb77d21bc1a2e1d902a3b3035628b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 25 Apr 2022 18:18:39 +0530 Subject: [PATCH 38/81] fix: Allow multi-currency opening invoices --- .../opening_invoice_creation_tool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 9d4d76b8f6..174b7d7f46 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -154,6 +154,7 @@ class OpeningInvoiceCreationTool(Document): "income_account" if row.party_type == "Customer" else "expense_account" ) default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos") + default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency") rate = flt(row.outstanding_amount) / flt(row.qty) item_dict = frappe._dict( @@ -166,6 +167,7 @@ class OpeningInvoiceCreationTool(Document): "description": row.item_name or "Opening Invoice Item", income_expense_account_field: row.temporary_opening_account, "cost_center": cost_center, + "currency": default_currency, } ) From e1c168766198d44dc7e28746bcad9f4c266c3a39 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 20:01:48 +0530 Subject: [PATCH 39/81] refactor: use db agnostic `CombineDatetime` --- erpnext/stock/stock_ledger.py | 10 ++++------ erpnext/stock/utils.py | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a781479cf6..7e5c231d9c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -8,9 +8,8 @@ from typing import Optional, Set, Tuple import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate -from pypika import CustomFunction import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -1158,16 +1157,15 @@ def get_batch_incoming_rate( item_code, warehouse, batch_no, posting_date, posting_time, creation=None ): - Timestamp = CustomFunction("timestamp", ["date", "time"]) - sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp( + timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( posting_date, posting_time ) if creation: timestamp_condition |= ( - Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time) + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(posting_date, posting_time) ) & (sle.creation < creation) batch_details = ( diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index d40218e143..2120304097 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -7,6 +7,7 @@ from typing import Dict, Optional import frappe from frappe import _ +from frappe.query_builder.functions import CombineDatetime from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext @@ -143,12 +144,10 @@ def get_stock_balance( def get_serial_nos_data_after_transactions(args): - from pypika import CustomFunction serial_nos = set() args = frappe._dict(args) sle = frappe.qb.DocType("Stock Ledger Entry") - Timestamp = CustomFunction("timestamp", ["date", "time"]) stock_ledger_entries = ( frappe.qb.from_(sle) @@ -157,7 +156,8 @@ def get_serial_nos_data_after_transactions(args): (sle.item_code == args.item_code) & (sle.warehouse == args.warehouse) & ( - Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time) + CombineDatetime(sle.posting_date, sle.posting_time) + < CombineDatetime(args.posting_date, args.posting_time) ) & (sle.is_cancelled == 0) ) From cdac2b8c63e5856498e5c4cbcc871a1178f7cb80 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 20:43:07 +0530 Subject: [PATCH 40/81] fix: ignore duplicate fixtures --- erpnext/regional/india/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 446faaa708..062c2ef5c5 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -1219,7 +1219,7 @@ def make_fixtures(company=None): try: doc = frappe.get_doc(d) doc.flags.ignore_permissions = True - doc.insert() + doc.insert(ignore_if_duplicate=True) except frappe.NameError: frappe.clear_messages() except frappe.DuplicateEntryError: From ce75fe0ec405c68f1c5dd7b4a5d1bbde1af70705 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 22 Apr 2022 11:44:30 +0530 Subject: [PATCH 41/81] fix: out of range date value --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c4aa8a4711..27a6eaf08b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1167,7 +1167,7 @@ class StockEntry(StockController): from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s where i.name=%s and i.disabled=0 - and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""", + and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""", (self.company, args.get("item_code"), nowdate()), as_dict=1, ) From 0fdfc1e76e7aa89268e89a67c968b7acb6d980ec Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 24 Apr 2022 19:19:22 +0530 Subject: [PATCH 42/81] fix: only query fields that exist --- erpnext/crm/doctype/opportunity/opportunity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 96c730c668..19b4d68e1c 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -54,11 +54,11 @@ class Opportunity(TransactionBase): self.calculate_totals() def map_fields(self): - for field in self.meta.fields: - if not self.get(field.fieldname): + for field in self.meta.get_valid_columns(): + if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): try: - value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname) - frappe.db.set(self, field.fieldname, value) + value = frappe.db.get_value(self.opportunity_from, self.party_name, field) + frappe.db.set(self, field, value) except Exception: continue From 7d2587c0a9b2ab30ab45bbc9c2af40b1f2ade1fb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 24 Apr 2022 19:26:15 +0530 Subject: [PATCH 43/81] fix: nullish check and table name --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/stock/doctype/item_price/item_price.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index fefb2e59f0..220ce1dbd8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -753,7 +753,7 @@ class BOM(WebsiteGenerator): bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit - FROM `tabBOM Explosion Item` bom_item, tabBOM bom + FROM `tabBOM Explosion Item` bom_item, `tabBOM` bom WHERE bom_item.parent = bom.name AND bom.name = %s diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 562f7b9e12..ab797cd039 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -64,7 +64,7 @@ class ItemPrice(Document): if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: - conditions += "and (isnull({0}) or {0} = '')".format(field) + conditions += "and ({0} is null or {0} = '')".format(field) price_list_rate = frappe.db.sql( """ From 7e555d3d8c47bc7f304905a3d52b02e9da6b0283 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 24 Apr 2022 19:47:07 +0530 Subject: [PATCH 44/81] fix: item price query for postgres postgres doesn't like bad type comparisons and doesn't have `isnull` funciton --- .../stock/doctype/item_price/item_price.py | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index ab797cd039..bcd31ada83 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -5,6 +5,8 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Cast_ from frappe.utils import getdate @@ -48,35 +50,57 @@ class ItemPrice(Document): ) def check_duplicates(self): - conditions = ( - """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" - ) - for field in [ + item_price = frappe.qb.DocType("Item Price") + + query = ( + frappe.qb.from_(item_price) + .select(item_price.price_list_rate) + .where( + (item_price.item_code == self.item_code) + & (item_price.price_list == self.price_list) + & (item_price.name != self.name) + ) + ) + data_fields = ( "uom", "valid_from", "valid_upto", - "packing_unit", "customer", "supplier", "batch_no", - ]: - if self.get(field): - conditions += " and {0} = %({0})s ".format(field) - else: - conditions += "and ({0} is null or {0} = '')".format(field) - - price_list_rate = frappe.db.sql( - """ - select price_list_rate - from `tabItem Price` - {conditions} - """.format( - conditions=conditions - ), - self.as_dict(), ) + number_fields = ["packing_unit"] + + for field in data_fields: + if self.get(field): + query = query.where(item_price[field] == self.get(field)) + else: + query = query.where( + Criterion.any( + [ + item_price[field].isnull(), + Cast_(item_price[field], "varchar") == "", + ] + ) + ) + + for field in number_fields: + if self.get(field): + query = query.where(item_price[field] == self.get(field)) + else: + query = query.where( + Criterion.any( + [ + item_price[field].isnull(), + item_price[field] == 0, + ] + ) + ) + + price_list_rate = query.run(as_dict=True) + if price_list_rate: frappe.throw( _( From a90e7e32a42362f5a03ad1467c7a1e5757fe95fe Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 24 Apr 2022 20:05:36 +0530 Subject: [PATCH 45/81] fix: proper quoting in sql queries --- erpnext/accounts/party.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b0b3049d48..ac1dfec7a5 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -881,11 +881,11 @@ def get_default_contact(doctype, name): """ SELECT dl.parent, c.is_primary_contact, c.is_billing_contact FROM `tabDynamic Link` dl - INNER JOIN tabContact c ON c.name = dl.parent + INNER JOIN `tabContact` c ON c.name = dl.parent WHERE dl.link_doctype=%s AND dl.link_name=%s AND - dl.parenttype = "Contact" + dl.parenttype = 'Contact' ORDER BY is_primary_contact DESC, is_billing_contact DESC """, (doctype, name), From 052171f51c17c77459c05423a87ca935dd0ab555 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 24 Apr 2022 20:14:01 +0530 Subject: [PATCH 46/81] fix: date condition in tax rule --- erpnext/accounts/doctype/tax_rule/tax_rule.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 27b78e9fab..5bfca96bb1 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -163,10 +163,15 @@ def get_party_details(party, party_type, args=None): def get_tax_template(posting_date, args): """Get matching tax rule""" args = frappe._dict(args) + from_date = to_date = posting_date + if not posting_date: + from_date = "1900-01-01" + to_date = "4000-01-01" + conditions = [ """(from_date is null or from_date <= '{0}') - and (to_date is null or to_date >= '{0}')""".format( - posting_date + and (to_date is null or to_date >= '{1}')""".format( + from_date, to_date ) ] From 88e257ff6d89ba03e3b273be33fec62714751b69 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 24 Apr 2022 20:17:17 +0530 Subject: [PATCH 47/81] fix: column name in party query --- erpnext/accounts/party.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index ac1dfec7a5..db741d97e1 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -831,9 +831,9 @@ def get_party_shipping_address(doctype, name): "where " "dl.link_doctype=%s " "and dl.link_name=%s " - 'and dl.parenttype="Address" ' + "and dl.parenttype='Address' " "and ifnull(ta.disabled, 0) = 0 and" - '(ta.address_type="Shipping" or ta.is_shipping_address=1) ' + "(ta.address_type='Shipping' or ta.is_shipping_address=1) " "order by ta.is_shipping_address desc, ta.address_type desc limit 1", (doctype, name), ) From b0ead459a01e281ff0ddb5cbe5ff22a5628a95a2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Apr 2022 13:30:13 +0530 Subject: [PATCH 48/81] feat: add payment terms status rpr to Selling homepage --- .../selling/workspace/selling/selling.json | 362 ++++++++++++------ 1 file changed, 252 insertions(+), 110 deletions(-) diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index a700ad89a3..45e160d143 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -5,7 +5,7 @@ "label": "Sales Order Trends" } ], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-01-28 11:49:12.092882", "docstatus": 0, "doctype": "Workspace", @@ -314,118 +314,11 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Key Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 1, - "label": "Sales Analytics", - "link_count": 0, - "link_to": "Sales Analytics", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Sales Order", - "hidden": 0, - "is_query_report": 1, - "label": "Sales Order Analysis", - "link_count": 0, - "link_to": "Sales Order Analysis", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Sales Funnel", - "link_count": 0, - "link_to": "sales-funnel", - "link_type": "Page", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Sales Order", - "hidden": 0, - "is_query_report": 1, - "label": "Sales Order Trends", - "link_count": 0, - "link_to": "Sales Order Trends", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Quotation", - "hidden": 0, - "is_query_report": 1, - "label": "Quotation Trends", - "link_count": 0, - "link_to": "Quotation Trends", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Customer", - "hidden": 0, - "is_query_report": 1, - "label": "Customer Acquisition and Loyalty", - "link_count": 0, - "link_to": "Customer Acquisition and Loyalty", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Order", - "hidden": 0, - "is_query_report": 1, - "label": "Inactive Customers", - "link_count": 0, - "link_to": "Inactive Customers", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Order", - "hidden": 0, - "is_query_report": 1, - "label": "Sales Person-wise Transaction Summary", - "link_count": 0, - "link_to": "Sales Person-wise Transaction Summary", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 1, - "label": "Item-wise Sales History", - "link_count": 0, - "link_to": "Item-wise Sales History", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, "label": "Other Reports", - "link_count": 0, + "link_count": 12, "onboard": 0, "type": "Card Break" }, @@ -560,9 +453,258 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Key Reports", + "link_count": 22, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Analytics", + "link_count": 0, + "link_to": "Sales Analytics", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Order Analysis", + "link_count": 0, + "link_to": "Sales Order Analysis", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Sales Funnel", + "link_count": 0, + "link_to": "sales-funnel", + "link_type": "Page", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Order Trends", + "link_count": 0, + "link_to": "Sales Order Trends", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Quotation", + "hidden": 0, + "is_query_report": 1, + "label": "Quotation Trends", + "link_count": 0, + "link_to": "Quotation Trends", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Customer", + "hidden": 0, + "is_query_report": 1, + "label": "Customer Acquisition and Loyalty", + "link_count": 0, + "link_to": "Customer Acquisition and Loyalty", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Inactive Customers", + "link_count": 0, + "link_to": "Inactive Customers", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Person-wise Transaction Summary", + "link_count": 0, + "link_to": "Sales Person-wise Transaction Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Item-wise Sales History", + "link_count": 0, + "link_to": "Item-wise Sales History", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Lead", + "hidden": 0, + "is_query_report": 1, + "label": "Lead Details", + "link_count": 0, + "link_to": "Lead Details", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Address", + "hidden": 0, + "is_query_report": 1, + "label": "Customer Addresses And Contacts", + "link_count": 0, + "link_to": "Address And Contacts", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 1, + "label": "Available Stock for Packing Items", + "link_count": 0, + "link_to": "Available Stock for Packing Items", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Pending SO Items For Purchase Request", + "link_count": 0, + "link_to": "Pending SO Items For Purchase Request", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Delivery Note", + "hidden": 0, + "is_query_report": 1, + "label": "Delivery Note Trends", + "link_count": 0, + "link_to": "Delivery Note Trends", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Invoice", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Invoice Trends", + "link_count": 0, + "link_to": "Sales Invoice Trends", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Customer", + "hidden": 0, + "is_query_report": 1, + "label": "Customer Credit Balance", + "link_count": 0, + "link_to": "Customer Credit Balance", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Customer", + "hidden": 0, + "is_query_report": 1, + "label": "Customers Without Any Sales Transactions", + "link_count": 0, + "link_to": "Customers Without Any Sales Transactions", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Customer", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Partners Commission", + "link_count": 0, + "link_to": "Sales Partners Commission", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Territory Target Variance Based On Item Group", + "link_count": 0, + "link_to": "Territory Target Variance Based On Item Group", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Person Target Variance Based On Item Group", + "link_count": 0, + "link_to": "Sales Person Target Variance Based On Item Group", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Sales Order", + "hidden": 0, + "is_query_report": 1, + "label": "Sales Partner Target Variance Based On Item Group", + "link_count": 0, + "link_to": "Sales Partner Target Variance based on Item Group", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Payment Terms Status for Sales Order", + "link_count": 0, + "link_to": "Payment Terms Status for Sales Order", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2022-01-13 17:43:02.778627", + "modified": "2022-04-26 13:29:55.087240", "modified_by": "Administrator", "module": "Selling", "name": "Selling", From 03a6103fe58b35ab18ea669d6cc4e82ff1534200 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 26 Apr 2022 14:28:33 +0530 Subject: [PATCH 49/81] fix(pos): search field doesn't reset on checkout --- .../page/point_of_sale/pos_controller.js | 4 +-- .../page/point_of_sale/pos_item_selector.js | 36 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 65e0cbb7a5..7a6838680f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -343,9 +343,9 @@ erpnext.PointOfSale.Controller = class { toggle_other_sections: (show) => { if (show) { this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; - this.item_selector.$component.css('display', 'none'); + this.item_selector.toggle_component(false); } else { - this.item_selector.$component.css('display', 'flex'); + this.item_selector.toggle_component(true); } }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b62b27bc4b..7a90fb044f 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -179,6 +179,25 @@ erpnext.PointOfSale.ItemSelector = class { }); this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); + + this.attach_clear_btn(); + } + + attach_clear_btn() { + this.search_field.$wrapper.find('.control-input').append( + ` + + ${frappe.utils.icon('close', 'sm')} + + ` + ); + + this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn'); + + this.$clear_search_btn.on('click', 'a', () => { + this.set_search_value(''); + this.search_field.set_focus(); + }); } set_search_value(value) { @@ -252,6 +271,16 @@ erpnext.PointOfSale.ItemSelector = class { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); + + this.$clear_search_btn.toggle( + Boolean(this.search_field.$input.val()) + ); + }); + + this.search_field.$input.on('focus', () => { + this.$clear_search_btn.toggle( + Boolean(this.search_field.$input.val()) + ); }); } @@ -284,7 +313,7 @@ erpnext.PointOfSale.ItemSelector = class { if (this.items.length == 1) { this.$items_container.find(".item-wrapper").click(); frappe.utils.play_sound("submit"); - $(this.search_field.$input[0]).val("").trigger("input"); + this.set_search_value(''); } else if (this.items.length == 0 && this.barcode_scanned) { // only show alert of barcode is scanned and enter is pressed frappe.show_alert({ @@ -293,7 +322,7 @@ erpnext.PointOfSale.ItemSelector = class { }); frappe.utils.play_sound("error"); this.barcode_scanned = false; - $(this.search_field.$input[0]).val("").trigger("input"); + this.set_search_value(''); } }); } @@ -350,6 +379,7 @@ erpnext.PointOfSale.ItemSelector = class { } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + this.set_search_value(''); + this.$component.css('display', show ? 'flex': 'none'); } }; From b1ac5ff9d27eb54f7c5d1ae2e2ccaaa44bffdcc5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 26 Apr 2022 14:41:36 +0530 Subject: [PATCH 50/81] fix(pos): number pad translations --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 8 ++++---- erpnext/selling/page/point_of_sale/pos_number_pad.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 4a99f068cd..eacf480ef8 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -130,10 +130,10 @@ erpnext.PointOfSale.ItemCart = class { }, cols: 5, keys: [ - [ 1, 2, 3, __('Quantity') ], - [ 4, 5, 6, __('Discount') ], - [ 7, 8, 9, __('Rate') ], - [ '.', 0, __('Delete'), __('Remove') ] + [ 1, 2, 3, 'Quantity' ], + [ 4, 5, 6, 'Discount' ], + [ 7, 8, 9, 'Rate' ], + [ '.', 0, 'Delete', 'Remove' ] ], css_classes: [ [ '', '', '', 'col-span-2' ], diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index 95293d1dd5..f27b0d55ef 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -25,7 +25,7 @@ erpnext.PointOfSale.NumberPad = class { const fieldname = fieldnames && fieldnames[number] ? fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; - return a2 + `
${number}
`; + return a2 + `
${__(number)}
`; }, ''); }, ''); } From 1cc8c79807ce5f864b0d3af1126272223ac3f5e1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 Apr 2022 17:43:08 +0530 Subject: [PATCH 51/81] test: fix flaky carry forwarded leave expiry test (#30810) --- erpnext/hr/doctype/leave_allocation/test_leave_allocation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index dde52d7ad8..a1d39d4423 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -18,6 +18,7 @@ class TestLeaveAllocation(FrappeTestCase): def setUp(self): frappe.db.delete("Leave Period") frappe.db.delete("Leave Allocation") + frappe.db.delete("Leave Ledger Entry") emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company") self.employee = frappe.get_doc("Employee", emp_id) From 44331f4f1ffb722f1ca2450156c86e16d127697e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 26 Apr 2022 17:23:56 +0530 Subject: [PATCH 52/81] chore: Warn users about multiple warehouse accounts - Get distinct accounts that warehouse value has been booked against - If same account as the one being set, ignore - If not same account or multiple accounts: warn user that it makes it harder to track mismatches --- erpnext/stock/doctype/warehouse/warehouse.py | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 3b18a9ac26..3c2d6b8d4c 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -36,6 +36,9 @@ class Warehouse(NestedSet): self.set_onload("account", account) load_address_and_contact(self) + def validate(self): + self.warn_about_multiple_warehouse_account() + def on_update(self): self.update_nsm_model() @@ -70,6 +73,53 @@ class Warehouse(NestedSet): self.update_nsm_model() self.unlink_from_items() + def warn_about_multiple_warehouse_account(self): + "If Warehouse value is split across multiple accounts, warn." + + def get_accounts_where_value_is_booked(name): + sle = frappe.qb.DocType("Stock Ledger Entry") + gle = frappe.qb.DocType("GL Entry") + ac = frappe.qb.DocType("Account") + + return ( + frappe.qb.from_(sle) + .join(gle) + .on(sle.voucher_no == gle.voucher_no) + .join(ac) + .on(ac.name == gle.account) + .select(gle.account) + .distinct() + .where((sle.warehouse == name) & (ac.root_type == "Asset")) + .orderby(sle.creation) + .run(as_dict=True) + ) + + if self.is_new(): + return + + old_wh_account = frappe.db.get_value("Warehouse", self.name, "account") + + # WH account is being changed or set get all accounts against which wh value is booked + if self.account != old_wh_account: + accounts = get_accounts_where_value_is_booked(self.name) + accounts = [d.account for d in accounts] + + if not accounts or (len(accounts) == 1 and self.account in accounts): + # if same singular account has stock value booked ignore + return + + warning = _("Warehouse's Stock Value has already been booked in the following accounts:") + account_str = "
" + ", ".join(frappe.bold(ac) for ac in accounts) + reason = "

" + _( + "Booking stock value across multiple accounts will make it harder to track stock and account value." + ) + + frappe.msgprint( + warning + account_str + reason, + title=_("Multiple Warehouse Accounts"), + indicator="orange", + ) + def check_if_sle_exists(self): return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name}) From e2a163d4e9ca0e26c0bef8de83b4265db083518c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 26 Apr 2022 17:50:51 +0530 Subject: [PATCH 53/81] fix: Use `account_type == 'Stock'` to filter stock accounts --- erpnext/stock/doctype/warehouse/warehouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 3c2d6b8d4c..df16643d46 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -89,7 +89,7 @@ class Warehouse(NestedSet): .on(ac.name == gle.account) .select(gle.account) .distinct() - .where((sle.warehouse == name) & (ac.root_type == "Asset")) + .where((sle.warehouse == name) & (ac.account_type == "Stock")) .orderby(sle.creation) .run(as_dict=True) ) From 24a7acaabc23226031af9e2155c3f56f4b09fa25 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 26 Apr 2022 19:22:26 +0530 Subject: [PATCH 54/81] chore: incorrect translations --- erpnext/translations/ru.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 6703da6063..073820e9b9 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -3034,7 +3034,7 @@ To Date,До, To Date cannot be before From Date,На сегодняшний день не может быть раньше от даты, To Date cannot be less than From Date,"Дата не может быть меньше, чем с даты", To Date must be greater than From Date,"До даты должно быть больше, чем с даты", -To Date should be within the Fiscal Year. Assuming To Date = {0},Дата должна быть в пределах финансового года. Предположим, до даты = {0}, +"To Date should be within the Fiscal Year. Assuming To Date = {0}","Дата должна быть в пределах финансового года. Предположим, до даты = {0}", To Datetime,Для DateTime, To Deliver,Для доставки, To Deliver and Bill,Для доставки и оплаты, @@ -6917,7 +6917,7 @@ Time after the end of shift during which check-out is considered for attendance. Working Hours Threshold for Half Day,Порог рабочего времени на полдня, Working hours below which Half Day is marked. (Zero to disable),"Рабочее время, ниже которого отмечается полдня. (Ноль отключить)", Working Hours Threshold for Absent,Порог рабочего времени для отсутствующих, -Working hours below which Absent is marked. (Zero to disable),Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения),", +"Working hours below which Absent is marked. (Zero to disable)","Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения)", Process Attendance After,Посещаемость процесса после, Attendance will be marked automatically only after this date.,Посещаемость будет отмечена автоматически только после этой даты., Last Sync of Checkin,Последняя синхронизация регистрации, From 5c3f9019cc4dda9e0c58a7bf9a824e64db995a21 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 17:18:19 +0530 Subject: [PATCH 55/81] refactor: misc pick list refactors - make tracking fields read only and no-copy :facepalm: - collapse print settings section, most users configure it once and forget about it, not need to show this. - call pick list grouping function directly - use get_descendants_of instead of obscure db function --- erpnext/selling/doctype/sales_order/sales_order.json | 3 ++- .../selling/doctype/sales_order_item/sales_order_item.json | 6 ++++-- erpnext/stock/doctype/pick_list/pick_list.json | 4 +++- erpnext/stock/doctype/pick_list/pick_list.py | 6 +++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index fe2f14e19a..1d0432bddb 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1520,6 +1520,7 @@ "fieldname": "per_picked", "fieldtype": "Percent", "label": "% Picked", + "no_copy": 1, "read_only": 1 } ], @@ -1527,7 +1528,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-03-15 21:38:31.437586", + "modified": "2022-04-21 08:16:48.316074", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", 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 195e96486b..8a6a0bae54 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -803,13 +803,15 @@ { "fieldname": "picked_qty", "fieldtype": "Float", - "label": "Picked Qty" + "label": "Picked Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-03-15 20:17:33.984799", + "modified": "2022-04-21 08:15:14.010319", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index c604c711ef..e984c082d4 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -114,6 +114,7 @@ "set_only_once": 1 }, { + "collapsible": 1, "fieldname": "print_settings_section", "fieldtype": "Section Break", "label": "Print Settings" @@ -129,7 +130,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-10-05 15:08:40.369957", + "modified": "2022-04-21 07:56:40.646473", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -199,5 +200,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 33d7745c62..72524f036e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -11,6 +11,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.utils import cint, floor, flt, today +from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( make_delivery_note as create_delivery_note_from_sales_order, @@ -109,7 +110,7 @@ class PickList(Document): from_warehouses = None if self.parent_warehouse: - from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse) + from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -190,8 +191,7 @@ class PickList(Document): frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) def before_print(self, settings=None): - if self.get("group_same_items"): - self.group_similar_items() + self.group_similar_items() def group_similar_items(self): group_item_qty = defaultdict(float) From 7d5682020ad59853ebec83431219671a3648d4b9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 20 Apr 2022 18:57:33 +0530 Subject: [PATCH 56/81] test: bundles in picklist --- .../stock/doctype/pick_list/test_pick_list.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 27b06d2dd9..1ba46f7414 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -3,11 +3,10 @@ import frappe from frappe import _dict - -test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] - from frappe.tests.utils import FrappeTestCase +from erpnext.selling.doctype.sales_order.sales_order import create_pick_list +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -15,6 +14,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( EmptyStockReconciliationItemsError, ) +test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] + class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): @@ -579,6 +580,19 @@ class TestPickList(FrappeTestCase): if dn_item.item_code == "_Test Item 2": self.assertEqual(dn_item.qty, 2) + def test_picklist_with_bundles(self): + # from test_records.json + bundle = "_Test Product Bundle Item" + bundle_items = {"_Test Item": 5, "_Test Item Home Desktop 100": 2} + + so = make_sales_order(item_code=bundle, qty=1) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(len(pl.locations), 2) + for item in pl.locations: + self.assertEqual(item.stock_qty, bundle_items[item.item_code]) + # def test_pick_list_skips_items_in_expired_batch(self): # pass From 36c5e8a14fd9cebac5c5f22cf062ca3a47a416f0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 12:33:38 +0530 Subject: [PATCH 57/81] feat: Pick list from SO with Product Bundle --- .../doctype/sales_order/sales_order.py | 31 +++++++++++++++++-- .../doctype/packed_item/packed_item.json | 9 +++++- .../stock/doctype/packed_item/packed_item.py | 6 +++- .../stock/doctype/pick_list/test_pick_list.py | 8 +++-- .../pick_list_item/pick_list_item.json | 13 +++++++- 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d3b4286be5..1d172ad0bf 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1232,10 +1232,27 @@ def make_inter_company_purchase_order(source_name, target_doc=None): @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - def update_item_quantity(source, target, source_parent): + from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle + + def update_item_quantity(source, target, source_parent) -> None: target.qty = flt(source.qty) - flt(source.delivered_qty) target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) + def update_packed_item_qty(source, target, source_parent) -> None: + qty = flt(source.qty) + for item in source_parent.items: + if source.parent_detail_docname == item.name: + pending_percent = (item.qty - item.delivered_qty) / item.qty + target.qty = target.stock_qty = qty * pending_percent + return + + def should_pick_order_item(item) -> bool: + return ( + abs(item.delivered_qty) < abs(item.qty) + and item.delivered_by_supplier != 1 + and not is_product_bundle(item.item_code) + ) + doc = get_mapped_doc( "Sales Order", source_name, @@ -1245,8 +1262,16 @@ def create_pick_list(source_name, target_doc=None): "doctype": "Pick List Item", "field_map": {"parent": "sales_order", "name": "sales_order_item"}, "postprocess": update_item_quantity, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) - and doc.delivered_by_supplier != 1, + "condition": should_pick_order_item, + }, + "Packed Item": { + "doctype": "Pick List Item", + "field_map": { + "parent": "sales_order", + "name": "sales_order_item", + "parent_detail_docname": "product_bundle_item", + }, + "postprocess": update_packed_item_qty, }, }, target_doc, diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index e94c34d7ad..4e67c84a0b 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -29,6 +29,7 @@ "ordered_qty", "column_break_16", "incoming_rate", + "picked_qty", "page_break", "prevdoc_doctype", "parent_detail_docname" @@ -234,13 +235,19 @@ "label": "Ordered Qty", "no_copy": 1, "read_only": 1 + }, + { + "depends_on": "picked_qty", + "fieldname": "picked_qty", + "fieldtype": "Float", + "label": "Picked Qty" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-03-10 15:42:00.265915", + "modified": "2022-04-21 08:05:29.785362", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 026dd4e122..4d05d7a345 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -32,7 +32,7 @@ def make_packing_list(doc): reset = reset_packing_list(doc) for item_row in doc.get("items"): - if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): + if is_product_bundle(item_row.item_code): for bundle_item in get_product_bundle_items(item_row.item_code): pi_row = add_packed_item_row( doc=doc, @@ -54,6 +54,10 @@ def make_packing_list(doc): set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item +def is_product_bundle(item_code: str) -> bool: + return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code})) + + def get_indexed_packed_items_table(doc): """ Create dict from stale packed items table like: diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 1ba46f7414..d1a9472f1f 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -10,6 +10,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( EmptyStockReconciliationItemsError, ) @@ -582,16 +583,19 @@ class TestPickList(FrappeTestCase): def test_picklist_with_bundles(self): # from test_records.json + warehouse = "_Test Warehouse - _TC" bundle = "_Test Product Bundle Item" bundle_items = {"_Test Item": 5, "_Test Item Home Desktop 100": 2} + for item in bundle_items: + make_stock_entry(item=item, to_warehouse=warehouse, qty=10, rate=10) - so = make_sales_order(item_code=bundle, qty=1) + so = make_sales_order(item_code=bundle, qty=3) pl = create_pick_list(so.name) pl.save() self.assertEqual(len(pl.locations), 2) for item in pl.locations: - self.assertEqual(item.stock_qty, bundle_items[item.item_code]) + self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3) # def test_pick_list_skips_items_in_expired_batch(self): # pass diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 805286ddcc..a96ebfcdee 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -27,6 +27,7 @@ "column_break_15", "sales_order", "sales_order_item", + "product_bundle_item", "material_request", "material_request_item" ], @@ -146,6 +147,7 @@ { "fieldname": "sales_order_item", "fieldtype": "Data", + "hidden": 1, "label": "Sales Order Item", "read_only": 1 }, @@ -177,11 +179,19 @@ "fieldtype": "Data", "label": "Item Group", "read_only": 1 + }, + { + "description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle", + "fieldname": "product_bundle_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Product Bundle Item", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-09-28 12:02:16.923056", + "modified": "2022-04-22 05:27:38.497997", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", @@ -190,5 +200,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From e64cc66df74dd1015b3b015adfacc4f665806f7c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 22 Apr 2022 15:22:36 +0530 Subject: [PATCH 58/81] refactor: sales order status update - rename badly named variables - support updated packed items --- erpnext/stock/doctype/pick_list/pick_list.py | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 72524f036e..01448be600 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -46,7 +46,7 @@ class PickList(Document): if item.sales_order_item: # update the picked_qty in SO Item - self.update_so(item.sales_order_item, item.picked_qty, item.item_code) + self.update_sales_order_item(item, item.picked_qty, item.item_code) if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue @@ -70,14 +70,13 @@ class PickList(Document): # update picked_qty in SO Item on cancel of PL for item in self.get("locations"): if item.sales_order_item: - self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code) + self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) + + def update_sales_order_item(self, item, picked_qty, item_code): + item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" - def update_so(self, so_item, picked_qty, item_code): - so_doc = frappe.get_doc( - "Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent") - ) already_picked, actual_qty = frappe.db.get_value( - "Sales Order Item", so_item, ["picked_qty", "qty"] + item_table, item.sales_order_item, ["picked_qty", "qty"] ) if self.docstatus == 1: @@ -87,20 +86,22 @@ class PickList(Document): frappe.throw( _( "You are picking more than required quantity for {}. Check if there is any other pick list created for {}" - ).format(item_code, so_doc.name) + ).format(item_code, item.sales_order) ) - frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty) + frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty) + # TODO: only do this once after all items + sales_order = frappe.get_doc("Sales Order", item.sales_order) total_picked_qty = 0 total_so_qty = 0 - for item in so_doc.get("items"): - total_picked_qty += flt(item.picked_qty) - total_so_qty += flt(item.stock_qty) + for so_item in sales_order.get("items"): + total_picked_qty += flt(so_item.picked_qty) + total_so_qty += flt(so_item.stock_qty) total_picked_qty = total_picked_qty + picked_qty per_picked = total_picked_qty / total_so_qty * 100 - so_doc.db_set("per_picked", flt(per_picked), update_modified=False) + sales_order.db_set("per_picked", flt(per_picked), update_modified=False) @frappe.whitelist() def set_item_locations(self, save=False): From c3fc0a4f55789ee273c7acbc0fb711def4af3190 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 22 Apr 2022 15:31:41 +0530 Subject: [PATCH 59/81] perf: single update per Sales Order. For each SO item the sales order picking status was being updated, this isn't required and wasteful. --- .../doctype/sales_order/sales_order.py | 10 +++++++ erpnext/stock/doctype/pick_list/pick_list.py | 29 +++++++++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 1d172ad0bf..a35e16f2c2 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -385,6 +385,16 @@ class SalesOrder(SellingController): if tot_qty != 0: self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False) + def update_picking_status(self): + total_picked_qty = 0.0 + total_qty = 0.0 + for so_item in self.items: + total_picked_qty += flt(so_item.picked_qty) + total_qty += flt(so_item.stock_qty) + per_picked = total_picked_qty / total_qty * 100 + + self.db_set("per_picked", flt(per_picked), update_modified=False) + def set_indicator(self): """Set indicator for portal""" if self.per_billed < 100 and self.per_delivered < 100: diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 01448be600..3191f15782 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -5,6 +5,7 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby from operator import itemgetter +from typing import Set import frappe from frappe import _ @@ -39,6 +40,8 @@ class PickList(Document): ) def before_submit(self): + + update_sales_orders = set() for item in self.locations: # if the user has not entered any picked qty, set it to stock_qty, before submit if item.picked_qty == 0: @@ -47,6 +50,7 @@ class PickList(Document): if item.sales_order_item: # update the picked_qty in SO Item self.update_sales_order_item(item, item.picked_qty, item.item_code) + update_sales_orders.add(item.sales_order) if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue @@ -66,11 +70,18 @@ class PickList(Document): title=_("Quantity Mismatch"), ) + self.update_sales_order_picking_status(update_sales_orders) + def before_cancel(self): - # update picked_qty in SO Item on cancel of PL + """Deduct picked qty on cancelling pick list""" + updated_sales_orders = set() + for item in self.get("locations"): if item.sales_order_item: self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) + updated_sales_orders.add(item.sales_order) + + self.update_sales_order_picking_status(updated_sales_orders) def update_sales_order_item(self, item, picked_qty, item_code): item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" @@ -91,17 +102,11 @@ class PickList(Document): frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty) - # TODO: only do this once after all items - sales_order = frappe.get_doc("Sales Order", item.sales_order) - total_picked_qty = 0 - total_so_qty = 0 - for so_item in sales_order.get("items"): - total_picked_qty += flt(so_item.picked_qty) - total_so_qty += flt(so_item.stock_qty) - total_picked_qty = total_picked_qty + picked_qty - per_picked = total_picked_qty / total_so_qty * 100 - - sales_order.db_set("per_picked", flt(per_picked), update_modified=False) + @staticmethod + def update_sales_order_picking_status(sales_orders: Set[str]) -> None: + for sales_order in sales_orders: + if sales_order: + frappe.get_doc("Sales Order", sales_order).update_picking_status() @frappe.whitelist() def set_item_locations(self, save=False): From 60bc26fdbe9540cbb40a01732d469f3579314be2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 13:42:42 +0530 Subject: [PATCH 60/81] feat: back-update min picked qty for a bundle --- erpnext/stock/doctype/pick_list/pick_list.py | 55 +++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 3191f15782..7564e8fd15 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -5,7 +5,7 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby from operator import itemgetter -from typing import Set +from typing import Dict, List, Set import frappe from frappe import _ @@ -40,7 +40,6 @@ class PickList(Document): ) def before_submit(self): - update_sales_orders = set() for item in self.locations: # if the user has not entered any picked qty, set it to stock_qty, before submit @@ -70,6 +69,7 @@ class PickList(Document): title=_("Quantity Mismatch"), ) + self.update_bundle_picked_qty() self.update_sales_order_picking_status(update_sales_orders) def before_cancel(self): @@ -81,6 +81,7 @@ class PickList(Document): self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) updated_sales_orders.add(item.sales_order) + self.update_bundle_picked_qty() self.update_sales_order_picking_status(updated_sales_orders) def update_sales_order_item(self, item, picked_qty, item_code): @@ -223,6 +224,56 @@ class PickList(Document): for idx, item in enumerate(self.locations, start=1): item.idx = idx + def update_bundle_picked_qty(self): + """Ensure that picked quantity is sufficient for fulfilling a whole number of.""" + + product_bundles = self._get_product_bundles() + product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) + + for so_row, item_code in product_bundles.items(): + picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) + item_table = "Sales Order Item" + already_picked = frappe.db.get_value(item_table, so_row, "picked_qty") + frappe.db.set_value( + item_table, + so_row, + "picked_qty", + already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), + ) + + def _get_product_bundles(self) -> Dict[str, str]: + # Dict[so_item_row: item_code] + product_bundles = {} + for item in self.locations: + if not item.product_bundle_item: + continue + bundle_item_code = frappe.db.get_value( + "Sales Order Item", item.product_bundle_item, "item_code" + ) + product_bundles[item.product_bundle_item] = bundle_item_code + return product_bundles + + def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]: + # 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}) + product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} + return product_bundle_qty_map + + def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> float: + """Compute how many full bundles can be created from picked items.""" + possible_bundles = [] + for item in self.locations: + if item.product_bundle_item != bundle_row: + continue + + if qty_in_bundle := bundle_items.get(item.item_code): + possible_bundles.append(item.picked_qty / qty_in_bundle) + else: + possible_bundles.append(0) + return min(possible_bundles) + def validate_item_locations(pick_list): if not pick_list.locations: From 3ddad6891ae6184e7bdcd12abb1cfa3f6993344d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 14:22:29 +0530 Subject: [PATCH 61/81] refactor: simplify needlessly complicated code --- erpnext/stock/doctype/pick_list/pick_list.py | 56 ++++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 7564e8fd15..3703e85502 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -549,21 +549,21 @@ def create_dn_wo_so(pick_list): def create_dn_with_so(sales_dict, pick_list): delivery_note = None + item_table_mapper = { + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "so_detail", + "parent": "against_sales_order", + }, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, + } + for customer in sales_dict: for so in sales_dict[customer]: delivery_note = None delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True) - - item_table_mapper = { - "doctype": "Delivery Note Item", - "field_map": { - "rate": "rate", - "name": "so_detail", - "parent": "against_sales_order", - }, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) - and doc.delivered_by_supplier != 1, - } break if delivery_note: # map all items of all sales orders of that customer @@ -577,28 +577,26 @@ def create_dn_with_so(sales_dict, pick_list): def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): for location in pick_list.locations: - if location.sales_order == sales_order: - if location.sales_order_item: - sales_order_item = frappe.get_cached_doc( - "Sales Order Item", {"name": location.sales_order_item} - ) - else: - sales_order_item = None + if location.sales_order != sales_order: + continue - source_doc, table_mapper = ( - [sales_order_item, item_mapper] if sales_order_item else [location, item_mapper] - ) + if location.sales_order_item: + sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item) + else: + sales_order_item = None - dn_item = map_child_doc(source_doc, delivery_note, table_mapper) + source_doc = sales_order_item or location - if dn_item: - dn_item.pick_list_item = location.name - dn_item.warehouse = location.warehouse - dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) - dn_item.batch_no = location.batch_no - dn_item.serial_no = location.serial_no + dn_item = map_child_doc(source_doc, delivery_note, item_mapper) - update_delivery_note_item(source_doc, dn_item, delivery_note) + if dn_item: + dn_item.pick_list_item = location.name + dn_item.warehouse = location.warehouse + dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) + dn_item.batch_no = location.batch_no + dn_item.serial_no = location.serial_no + + update_delivery_note_item(source_doc, dn_item, delivery_note) set_delivery_note_missing_values(delivery_note) delivery_note.pick_list = pick_list.name From 277b51b40482447639d2c0b9d5440e758ea0b76f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 15:02:08 +0530 Subject: [PATCH 62/81] refactor: groupby using keys instead of int index --- erpnext/stock/doctype/pick_list/pick_list.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 3703e85502..a94745677f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -4,7 +4,6 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby -from operator import itemgetter from typing import Dict, List, Set import frappe @@ -507,11 +506,13 @@ def create_delivery_note(source_name, target_doc=None): for location in pick_list.locations: if location.sales_order: sales_orders.append( - [frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order] + frappe.db.get_value( + "Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True + ) ) - # Group sales orders by customer - for key, keydata in groupby(sales_orders, key=itemgetter(0)): - sales_dict[key] = set([d[1] for d in keydata]) + + for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]): + sales_dict[customer] = {row.sales_order for row in rows} if sales_dict: delivery_note = create_dn_with_so(sales_dict, pick_list) From f574121741800425e9eb8b34a78e26d8ed1d877a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 15:05:10 +0530 Subject: [PATCH 63/81] refactor: simpler check for non-SO items --- erpnext/stock/doctype/pick_list/pick_list.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a94745677f..9daf4f11c4 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -517,13 +517,7 @@ def create_delivery_note(source_name, target_doc=None): if sales_dict: delivery_note = create_dn_with_so(sales_dict, pick_list) - is_item_wo_so = 0 - for location in pick_list.locations: - if not location.sales_order: - is_item_wo_so = 1 - break - if is_item_wo_so == 1: - # Create a DN for items without sales orders as well + if not all(item.sales_order for item in pick_list.locations): delivery_note = create_dn_wo_so(pick_list) frappe.msgprint(_("Delivery Note(s) created for the Pick List")) From 23cb0d684d5e10fd12688b55caddc0911ff4e927 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 17:06:26 +0530 Subject: [PATCH 64/81] feat: create DN from pick list with bundle items --- erpnext/stock/doctype/pick_list/pick_list.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 9daf4f11c4..46c858878a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -572,7 +572,7 @@ def create_dn_with_so(sales_dict, pick_list): def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): for location in pick_list.locations: - if location.sales_order != sales_order: + if location.sales_order != sales_order or location.product_bundle_item: continue if location.sales_order_item: @@ -592,6 +592,8 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): dn_item.serial_no = location.serial_no update_delivery_note_item(source_doc, dn_item, delivery_note) + + add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper) set_delivery_note_missing_values(delivery_note) delivery_note.pick_list = pick_list.name @@ -599,6 +601,22 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") +def add_product_bundles_to_delivery_note( + pick_list: "PickList", delivery_note, item_mapper +) -> None: + product_bundles = pick_list._get_product_bundles() + product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) + + for so_row, item_code in product_bundles.items(): + sales_order_item = frappe.get_doc("Sales Order Item", so_row) + dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) + # TODO: post process packed items and update stock details + dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( + so_row, product_bundle_qty_map[item_code] + ) + update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) + + @frappe.whitelist() def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) From 25485edfd966301966a8ccc366da1fa0fd38db1f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 17:12:37 +0530 Subject: [PATCH 65/81] refactor: remove unnecssary vars also remove misleading docstring --- erpnext/stock/doctype/pick_list/pick_list.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 46c858878a..60f5e34efb 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -224,8 +224,6 @@ class PickList(Document): item.idx = idx def update_bundle_picked_qty(self): - """Ensure that picked quantity is sufficient for fulfilling a whole number of.""" - product_bundles = self._get_product_bundles() product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) @@ -246,10 +244,11 @@ class PickList(Document): for item in self.locations: if not item.product_bundle_item: continue - bundle_item_code = frappe.db.get_value( - "Sales Order Item", item.product_bundle_item, "item_code" + product_bundles[item.product_bundle_item] = frappe.db.get_value( + "Sales Order Item", + item.product_bundle_item, + "item_code", ) - product_bundles[item.product_bundle_item] = bundle_item_code return product_bundles def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]: From 41aa4b352407190c4f2ed6bc223cb4510555206c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 17:16:44 +0530 Subject: [PATCH 66/81] fix: round off bundle qty This is to accomodate bundles that might allow floating point qty. --- erpnext/stock/doctype/pick_list/pick_list.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 60f5e34efb..aa015755fe 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -261,6 +261,8 @@ class PickList(Document): def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> float: """Compute how many full bundles can be created from picked items.""" + precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction") + possible_bundles = [] for item in self.locations: if item.product_bundle_item != bundle_row: @@ -270,7 +272,7 @@ class PickList(Document): possible_bundles.append(item.picked_qty / qty_in_bundle) else: possible_bundles.append(0) - return min(possible_bundles) + return flt(min(possible_bundles), precision or 6) def validate_item_locations(pick_list): From 1ac275ce61d0e113aac43a8b5ad81ba7c53b8f65 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 17:32:48 +0530 Subject: [PATCH 67/81] feat: transfer picklist stock info to packing list --- erpnext/stock/doctype/pick_list/pick_list.py | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index aa015755fe..94e9e53423 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -565,7 +565,10 @@ def create_dn_with_so(sales_dict, pick_list): # map all items of all sales orders of that customer for so in sales_dict[customer]: map_pl_locations(pick_list, item_table_mapper, delivery_note, so) - delivery_note.insert(ignore_mandatory=True) + delivery_note.flags.ignore_mandatory = True + delivery_note.insert() + update_packed_item_details(pick_list, delivery_note) + delivery_note.save() return delivery_note @@ -605,6 +608,10 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): def add_product_bundles_to_delivery_note( pick_list: "PickList", delivery_note, item_mapper ) -> None: + """Add product bundles found in pick list to delivery note. + + When mapping pick list items, the bundle item itself isn't part of the + locations. Dynamically fetch and add parent bundle item into DN.""" product_bundles = pick_list._get_product_bundles() product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) @@ -618,6 +625,31 @@ def add_product_bundles_to_delivery_note( update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) +def update_packed_item_details(pick_list: "PickList", delivery_note) -> None: + """Update stock details on packed items table of delivery note.""" + + def _find_so_row(packed_item): + for item in delivery_note.items: + if packed_item.parent_detail_docname == item.name: + return item.so_detail + + def _find_pick_list_location(bundle_row, packed_item): + if not bundle_row: + return + for loc in pick_list.locations: + if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code: + return loc + + for packed_item in delivery_note.packed_items: + so_row = _find_so_row(packed_item) + location = _find_pick_list_location(so_row, packed_item) + if not location: + continue + packed_item.warehouse = location.warehouse + packed_item.batch_no = location.batch_no + packed_item.serial_no = location.serial_no + + @frappe.whitelist() def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) From ee54ece8fd7dd70d154e3483d372f43681fbc514 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 19:24:33 +0530 Subject: [PATCH 68/81] test: product bundle fixture --- .../doctype/packed_item/test_packed_item.py | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index fe1b0d9f79..c928b57d58 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,10 +1,12 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from typing import List, Optional, Tuple + +import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_to_date, nowdate -from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item @@ -12,6 +14,25 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +def create_product_bundle(quantities: Optional[List[int]] = None) -> Tuple[str, List[str]]: + """Get a new product_bundle for use in tests""" + if not quantities: + quantities = [2, 2] + + bundle = make_item(properties={"is_stock_item": 0}).name + + bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle}) + + components = [] + for qty in quantities: + compoenent = make_item().name + components.append(compoenent) + bundle_doc.append("items", {"item_code": compoenent, "qty": qty}) + + bundle_doc.insert() + return bundle, components + + class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." @@ -19,22 +40,11 @@ class TestPackedItem(FrappeTestCase): def setUpClass(cls) -> None: super().setUpClass() cls.warehouse = "_Test Warehouse - _TC" - cls.bundle = "_Test Product Bundle X" - cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] - cls.bundle2 = "_Test Product Bundle Y" - cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"] - - make_item(cls.bundle, {"is_stock_item": 0}) - make_item(cls.bundle2, {"is_stock_item": 0}) - for item in cls.bundle_items + cls.bundle2_items: - make_item(item, {"is_stock_item": 1}) - - make_item("_Test Normal Stock Item", {"is_stock_item": 1}) - - make_product_bundle(cls.bundle, cls.bundle_items, qty=2) - make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2) + cls.bundle, cls.bundle_items = create_product_bundle() + cls.bundle2, cls.bundle2_items = create_product_bundle() + cls.normal_item = make_item().name for item in cls.bundle_items + cls.bundle2_items: make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100) @@ -58,7 +68,7 @@ class TestPackedItem(FrappeTestCase): self.assertEqual(so.packed_items[1].qty, 4) # change item code to non bundle item - so.items[0].item_code = "_Test Normal Stock Item" + so.items[0].item_code = self.normal_item so.save() self.assertEqual(len(so.packed_items), 0) From 9e60acdf56d15334f55a71c80f3c2e580dc46024 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 25 Apr 2022 19:30:42 +0530 Subject: [PATCH 69/81] test: test bundle - picklist behaviour --- .../doctype/packed_item/test_packed_item.py | 18 ++++-- erpnext/stock/doctype/pick_list/pick_list.py | 1 - .../stock/doctype/pick_list/test_pick_list.py | 61 +++++++++++++++---- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index c928b57d58..ad7fd9a697 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -14,8 +14,13 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -def create_product_bundle(quantities: Optional[List[int]] = None) -> Tuple[str, List[str]]: - """Get a new product_bundle for use in tests""" +def create_product_bundle( + quantities: Optional[List[int]] = None, warehouse: Optional[str] = None +) -> Tuple[str, List[str]]: + """Get a new product_bundle for use in tests. + + Create 10x required stock if warehouse is specified. + """ if not quantities: quantities = [2, 2] @@ -28,8 +33,11 @@ def create_product_bundle(quantities: Optional[List[int]] = None) -> Tuple[str, compoenent = make_item().name components.append(compoenent) bundle_doc.append("items", {"item_code": compoenent, "qty": qty}) + if warehouse: + make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100) bundle_doc.insert() + return bundle, components @@ -41,12 +49,10 @@ class TestPackedItem(FrappeTestCase): super().setUpClass() cls.warehouse = "_Test Warehouse - _TC" - cls.bundle, cls.bundle_items = create_product_bundle() - cls.bundle2, cls.bundle2_items = create_product_bundle() + cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse) + cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse) cls.normal_item = make_item().name - for item in cls.bundle_items + cls.bundle2_items: - make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100) def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 94e9e53423..53584f5079 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -618,7 +618,6 @@ def add_product_bundles_to_delivery_note( for so_row, item_code in product_bundles.items(): sales_order_item = frappe.get_doc("Sales Order Item", so_row) dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) - # TODO: post process packed items and update stock details dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( so_row, product_bundle_qty_map[item_code] ) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index d1a9472f1f..8ce05f15f7 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -8,6 +8,7 @@ from frappe.tests.utils import FrappeTestCase from erpnext.selling.doctype.sales_order.sales_order import create_pick_list from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -584,12 +585,12 @@ class TestPickList(FrappeTestCase): def test_picklist_with_bundles(self): # from test_records.json warehouse = "_Test Warehouse - _TC" - bundle = "_Test Product Bundle Item" - bundle_items = {"_Test Item": 5, "_Test Item Home Desktop 100": 2} - for item in bundle_items: - make_stock_entry(item=item, to_warehouse=warehouse, qty=10, rate=10) - so = make_sales_order(item_code=bundle, qty=3) + quantities = [5, 2] + bundle, components = create_product_bundle(quantities, warehouse=warehouse) + bundle_items = dict(zip(components, quantities)) + + so = make_sales_order(item_code=bundle, qty=3, rate=42) pl = create_pick_list(so.name) pl.save() @@ -597,14 +598,48 @@ class TestPickList(FrappeTestCase): for item in pl.locations: self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3) - # def test_pick_list_skips_items_in_expired_batch(self): - # pass + # check picking status on sales order + pl.submit() + so.reload() + self.assertEqual(so.per_picked, 100) - # def test_pick_list_from_sales_order(self): - # pass + # deliver + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + self.assertEqual(dn.packed_items[0].warehouse, warehouse) + so.reload() + self.assertEqual(so.per_delivered, 100) - # def test_pick_list_from_work_order(self): - # pass + def test_picklist_with_partial_bundles(self): + # from test_records.json + warehouse = "_Test Warehouse - _TC" - # def test_pick_list_from_material_request(self): - # pass + quantities = [5, 2] + bundle, components = create_product_bundle(quantities, warehouse=warehouse) + + so = make_sales_order(item_code=bundle, qty=4, rate=42) + + pl = create_pick_list(so.name) + for loc in pl.locations: + loc.picked_qty = loc.qty / 2 + + pl.save().submit() + so.reload() + self.assertEqual(so.per_picked, 50) + + # deliver half qty + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + so.reload() + self.assertEqual(so.per_delivered, 50) + + pl = create_pick_list(so.name) + pl.save().submit() + so.reload() + self.assertEqual(so.per_picked, 100) + + # deliver remaining + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + so.reload() + self.assertEqual(so.per_delivered, 100) From 85e8dd9a4960980e1ce681d942016f68081493e0 Mon Sep 17 00:00:00 2001 From: gavin Date: Wed, 27 Apr 2022 12:41:16 +0530 Subject: [PATCH 70/81] chore: Add "Try on PWD" button (#30817) --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c26660c5a2..e59c744712 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,17 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a --- -
+ +> Login for the PWD site: (username: Administrator, password: admin) + ### Containerized Installation Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details. From 8207697e43a8baa3353c0c984e8e99b14fcb5b55 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 12:15:26 +0530 Subject: [PATCH 71/81] fix: compare against stock qty while validating Other changes: - only allow whole number of bundles to get picked --- .../sales_order_item/sales_order_item.json | 4 ++-- erpnext/stock/doctype/pick_list/pick_list.py | 9 ++++++--- .../stock/doctype/pick_list/test_pick_list.py | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 6 deletions(-) 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 8a6a0bae54..3797856db2 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -803,7 +803,7 @@ { "fieldname": "picked_qty", "fieldtype": "Float", - "label": "Picked Qty", + "label": "Picked Qty (in Stock UOM)", "no_copy": 1, "read_only": 1 } @@ -811,7 +811,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-04-21 08:15:14.010319", + "modified": "2022-04-27 03:15:34.366563", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 53584f5079..70d2f23070 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -85,9 +85,12 @@ class PickList(Document): def update_sales_order_item(self, item, picked_qty, item_code): item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" + stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty" already_picked, actual_qty = frappe.db.get_value( - item_table, item.sales_order_item, ["picked_qty", "qty"] + item_table, + item.sales_order_item, + ["picked_qty", stock_qty_field], ) if self.docstatus == 1: @@ -259,7 +262,7 @@ class PickList(Document): product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map - def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> float: + def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: """Compute how many full bundles can be created from picked items.""" precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction") @@ -272,7 +275,7 @@ class PickList(Document): possible_bundles.append(item.picked_qty / qty_in_bundle) else: possible_bundles.append(0) - return flt(min(possible_bundles), precision or 6) + return int(flt(min(possible_bundles), precision or 6)) def validate_item_locations(pick_list): diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 8ce05f15f7..f552299806 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -582,8 +582,23 @@ class TestPickList(FrappeTestCase): if dn_item.item_code == "_Test Item 2": self.assertEqual(dn_item.qty, 2) + def test_picklist_with_multi_uom(self): + warehouse = "_Test Warehouse - _TC" + item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name + make_stock_entry(item=item, to_warehouse=warehouse, qty=1000) + + so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box") + pl = create_pick_list(so.name) + # pick half the qty + for loc in pl.locations: + loc.picked_qty = loc.stock_qty / 2 + pl.save() + pl.submit() + + so.reload() + self.assertEqual(so.per_picked, 50) + def test_picklist_with_bundles(self): - # from test_records.json warehouse = "_Test Warehouse - _TC" quantities = [5, 2] From 47e1a0104c83824a315a666c9a21956265c2b7d2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 13:31:23 +0530 Subject: [PATCH 72/81] fix: dont map picked qty and consider pick qty for new PL Co-Authored-By: marination --- erpnext/selling/doctype/sales_order/sales_order.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a35e16f2c2..b463213f50 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1245,14 +1245,18 @@ def create_pick_list(source_name, target_doc=None): from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle def update_item_quantity(source, target, source_parent) -> None: - target.qty = flt(source.qty) - flt(source.delivered_qty) - target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) + picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1) + qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty)) + + target.qty = qty_to_be_picked + target.stock_qty = qty_to_be_picked * flt(source.conversion_factor) def update_packed_item_qty(source, target, source_parent) -> None: qty = flt(source.qty) for item in source_parent.items: if source.parent_detail_docname == item.name: - pending_percent = (item.qty - item.delivered_qty) / item.qty + picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1) + pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty target.qty = target.stock_qty = qty * pending_percent return @@ -1281,6 +1285,7 @@ def create_pick_list(source_name, target_doc=None): "name": "sales_order_item", "parent_detail_docname": "product_bundle_item", }, + "field_no_map": ["picked_qty"], "postprocess": update_packed_item_qty, }, }, From 3ae9fa98c4de9a1c016f6bcddd0b8ce5f1cbb5c5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 14:33:26 +0530 Subject: [PATCH 73/81] ci: failfast when merge conflict exists (#30823) [skip ci] --- .github/helper/install.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 859146bbcd..69749c93af 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -2,6 +2,13 @@ set -e +# Check for merge conflicts before proceeding +python -m compileall -f "${GITHUB_WORKSPACE}" +if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 +fi + cd ~ || exit sudo apt-get install redis-server libcups2-dev From 9a8e3ef2356ac080d8922d5072a4e8a58c5eb676 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 14:41:50 +0530 Subject: [PATCH 74/81] fix(UX): only show pick list when picking is pending [skip ci] --- erpnext/selling/doctype/sales_order/sales_order.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 0b48f70eab..26c9996dba 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -152,7 +152,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } } - this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) { + this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + } const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; From ebd5f0b1bb4a2792bb768b720b800ca5d8724085 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 14:53:20 +0530 Subject: [PATCH 75/81] chore: make picked qty read only --- erpnext/stock/doctype/packed_item/packed_item.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 4e67c84a0b..cb8eb30cb3 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -237,17 +237,18 @@ "read_only": 1 }, { - "depends_on": "picked_qty", "fieldname": "picked_qty", "fieldtype": "Float", - "label": "Picked Qty" + "label": "Picked Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-04-21 08:05:29.785362", + "modified": "2022-04-27 05:23:08.683245", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 0a07cd8f68f50c358dd9726feab830b34a35d2c9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 15:47:28 +0530 Subject: [PATCH 76/81] chore: "refactor" readme (#30825) [skip ci] --- README.md | 69 ++++++++++----------------------------------- TRADEMARK_POLICY.md | 36 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 54 deletions(-) create mode 100644 TRADEMARK_POLICY.md diff --git a/README.md b/README.md index e59c744712..f8f5f80b34 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@
- + + +

ERPNext

ERP made simple

@@ -32,14 +34,9 @@ ERPNext as a monolith includes the following areas for managing businesses: 1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext) 1. [And More](https://erpnext.com/docs/user/manual/en/) -ERPNext requires MariaDB. - ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript. -- [User Guide](https://erpnext.com/docs/user) -- [Discussion Forum](https://discuss.erpnext.com/) - ---- +## Installation
@@ -56,21 +53,20 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details. -### Full Install +### Manual Install The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details. New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt). ---- -## License +## Learning and community -GNU/General Public License (see [license.txt](license.txt)) +1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. +2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext. +3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers. +4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users. -The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors. - ---- ## Contributing @@ -78,49 +74,14 @@ The ERPNext code is licensed as GNU General Public License (v3) and the Document 1. [Report Security Vulnerabilities](https://erpnext.com/security) 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) 1. [Translations](https://translate.erpnext.com) -1. [Chart of Accounts](https://charts.erpnext.com) ---- -## Learning +## License -1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. +GNU/General Public License (see [license.txt](license.txt)) ---- +The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors. -## Logo and Trademark +## Logo and Trademark Policy -The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd. - -### Introduction - -Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind: - -- We’d like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext. -- We’d like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo. -- We’d like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources. - -### Frappe Trademark Usage Policy - -Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name. - -We will grant permission to use the ERPNext name and logo for projects that meet the following criteria: - -- The primary purpose of your project is to promote the spread and improvement of the ERPNext software. -- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business). -Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed. -- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name. - -Use of the ERPNext name and logo is additionally allowed in the following situations: - -All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.” - -Similarly, it’s OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name. - -We do not allow the use of the trademark in advertising, including AdSense/AdWords. - -Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them. - -When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification. - -(inspired by WordPress) +Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md). diff --git a/TRADEMARK_POLICY.md b/TRADEMARK_POLICY.md new file mode 100644 index 0000000000..244c74778e --- /dev/null +++ b/TRADEMARK_POLICY.md @@ -0,0 +1,36 @@ +## Logo and Trademark Policy + +The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd. + +### Introduction + +Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind: + +- We’d like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext. +- We’d like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo. +- We’d like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources. + +### Frappe Trademark Usage Policy + +Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name. + +We will grant permission to use the ERPNext name and logo for projects that meet the following criteria: + +- The primary purpose of your project is to promote the spread and improvement of the ERPNext software. +- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business). +Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed. +- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name. + +Use of the ERPNext name and logo is additionally allowed in the following situations: + +All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.” + +Similarly, it’s OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name. + +We do not allow the use of the trademark in advertising, including AdSense/AdWords. + +Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them. + +When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification. + +(inspired by WordPress) From 777f0204dd74a06dc6e9527cfb55a2f2ec103663 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Apr 2022 17:47:40 +0530 Subject: [PATCH 77/81] test: create new item instead of using with _Test Item (#30827) Unnecessary dependency causes flake in stock. The test was reposting all entries for item just to test some behavior that is best tested on a newly created item _anyway_. --- .../delivery_note/test_delivery_note.py | 54 ------------------- .../test_stock_reconciliation.py | 22 ++++---- 2 files changed, 12 insertions(+), 64 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index b5a45578c6..f97e7ca9c6 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -78,56 +78,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertFalse(get_gl_entries("Delivery Note", dn.name)) - # def test_delivery_note_gl_entry(self): - # company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - - # set_valuation_method("_Test Item", "FIFO") - - # make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - - # stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - # prev_bal = get_balance_on(stock_in_hand_account) - - # dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") - - # gl_entries = get_gl_entries("Delivery Note", dn.name) - # self.assertTrue(gl_entries) - - # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - - # expected_values = { - # stock_in_hand_account: [0.0, stock_value_difference], - # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - # } - # for i, gle in enumerate(gl_entries): - # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - - # # check stock in hand balance - # bal = get_balance_on(stock_in_hand_account) - # self.assertEqual(bal, prev_bal - stock_value_difference) - - # # back dated incoming entry - # make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1", - # qty=5, basic_rate=100) - - # gl_entries = get_gl_entries("Delivery Note", dn.name) - # self.assertTrue(gl_entries) - - # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - - # expected_values = { - # stock_in_hand_account: [0.0, stock_value_difference], - # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - # } - # for i, gle in enumerate(gl_entries): - # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - - # dn.cancel() - # self.assertTrue(get_gl_entries("Delivery Note", dn.name)) - # set_perpetual_inventory(0, company) - def test_delivery_note_gl_entry_packing_item(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") @@ -854,8 +804,6 @@ class TestDeliveryNote(FrappeTestCase): company="_Test Company with perpetual inventory", ) - company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) @@ -881,8 +829,6 @@ class TestDeliveryNote(FrappeTestCase): def test_delivery_note_cost_center_with_balance_sheet_account(self): cost_center = "Main - TCP1" - company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index e826e003c2..1e59aae9a8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -30,7 +30,6 @@ class TestStockReconciliation(FrappeTestCase): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) def tearDown(self): - frappe.flags.dont_execute_stock_reposts = None frappe.local.future_sle = {} def test_reco_for_fifo(self): @@ -40,7 +39,9 @@ class TestStockReconciliation(FrappeTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1") + item_code = make_item(properties={"valuation_method": valuation_method}).name + + se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code) company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") # [[qty, valuation_rate, posting_date, # posting_time, expected_stock_value, bin_qty, bin_valuation]] @@ -54,11 +55,9 @@ class TestStockReconciliation(FrappeTestCase): ] for d in input_data: - set_valuation_method("_Test Item", valuation_method) - last_sle = get_previous_sle( { - "item_code": "_Test Item", + "item_code": item_code, "warehouse": "Stores - TCP1", "posting_date": d[2], "posting_time": d[3], @@ -67,6 +66,7 @@ class TestStockReconciliation(FrappeTestCase): # submit stock reconciliation stock_reco = create_stock_reconciliation( + item_code=item_code, qty=d[0], rate=d[1], posting_date=d[2], @@ -481,9 +481,11 @@ class TestStockReconciliation(FrappeTestCase): """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + frappe.db.rollback() + # repost will make this test useless, qty should update in realtime without reposts frappe.flags.dont_execute_stock_reposts = True - frappe.db.rollback() + self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") item_code = make_item().name warehouse = "_Test Warehouse - _TC" @@ -594,26 +596,26 @@ def create_batch_item_with_batch(item_name, batch_id): b.save() -def insert_existing_sle(warehouse): +def insert_existing_sle(warehouse, item_code="_Test Item"): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry se1 = make_stock_entry( posting_date="2012-12-15", posting_time="02:00", - item_code="_Test Item", + item_code=item_code, target=warehouse, qty=10, basic_rate=700, ) se2 = make_stock_entry( - posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15 + posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15 ) se3 = make_stock_entry( posting_date="2013-01-05", posting_time="07:00", - item_code="_Test Item", + item_code=item_code, target=warehouse, qty=15, basic_rate=1200, From 3c7176d69010485a368e32cf2d84588e180ad788 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Thu, 28 Apr 2022 11:07:02 +0530 Subject: [PATCH 78/81] fix: Update links to Frappe Cloud (#30833) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8f5f80b34..cea3472447 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a ## Installation
- + From 389c2853eb24f7df23bde228dea8cd240d2563c6 Mon Sep 17 00:00:00 2001 From: gavin Date: Thu, 28 Apr 2022 14:57:43 +0530 Subject: [PATCH 79/81] docs: Trimmed whitespace from "Try on FCloud" button ERPNext Port of https://github.com/frappe/frappe/pull/16793/commits/b1effcab4b001c89b6b6b825976aea42e2172b02 --- .github/try-on-f-cloud-button.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg index fe0bb2c52d..270092722e 100644 --- a/.github/try-on-f-cloud-button.svg +++ b/.github/try-on-f-cloud-button.svg @@ -1,4 +1,4 @@ - + From a8452c2ba241a943f99ca3856985f728e11d47b1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 29 Apr 2022 14:27:03 +0530 Subject: [PATCH 80/81] fix: Multi currency opening invoices --- .../opening_invoice_creation_tool.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 174b7d7f46..2d9ae9377f 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -114,10 +114,13 @@ class OpeningInvoiceCreationTool(Document): ) or {} ) + + default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency") + if company_details: invoice.update( { - "currency": company_details.get("default_currency"), + "currency": default_currency or company_details.get("default_currency"), "letter_head": company_details.get("default_letter_head"), } ) @@ -154,7 +157,6 @@ class OpeningInvoiceCreationTool(Document): "income_account" if row.party_type == "Customer" else "expense_account" ) default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos") - default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency") rate = flt(row.outstanding_amount) / flt(row.qty) item_dict = frappe._dict( @@ -167,7 +169,6 @@ class OpeningInvoiceCreationTool(Document): "description": row.item_name or "Opening Invoice Item", income_expense_account_field: row.temporary_opening_account, "cost_center": cost_center, - "currency": default_currency, } ) From 65742947e76875598efd459f1c67d0835c009fc9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 29 Apr 2022 15:46:00 +0530 Subject: [PATCH 81/81] fix(UX): record reason for skipping attendance or marking absent for auto attendance (#30844) --- .../employee_checkin/employee_checkin.py | 57 ++++++++++++++----- erpnext/hr/doctype/shift_type/shift_type.py | 12 +++- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 64eb019b00..e07b5e5db5 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_datetime +from frappe.utils import cint, get_datetime, get_link_to_form from erpnext.hr.doctype.attendance.attendance import ( get_duplicate_attendance_record, @@ -130,14 +130,11 @@ def mark_attendance_and_link_log( """ log_names = [x.name for x in logs] employee = logs[0].employee + if attendance_status == "Skip": - frappe.db.sql( - """update `tabEmployee Checkin` - set skip_auto_attendance = %s - where name in %s""", - ("1", log_names), - ) + skip_attendance_in_checkins(log_names) return None + elif attendance_status in ("Present", "Absent", "Half Day"): employee_doc = frappe.get_doc("Employee", employee) duplicate = get_duplicate_attendance_record(employee, attendance_date, shift) @@ -159,6 +156,12 @@ def mark_attendance_and_link_log( } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() + + if attendance_status == "Absent": + attendance.add_comment( + text=_("Employee was marked Absent for not meeting the working hours threshold.") + ) + frappe.db.sql( """update `tabEmployee Checkin` set attendance = %s @@ -167,13 +170,10 @@ def mark_attendance_and_link_log( ) return attendance else: - frappe.db.sql( - """update `tabEmployee Checkin` - set skip_auto_attendance = %s - where name in %s""", - ("1", log_names), - ) + skip_attendance_in_checkins(log_names) + add_comment_in_checkins(log_names, duplicate, overlapping) return None + else: frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status)) @@ -241,3 +241,34 @@ def time_diff_in_hours(start, end): def find_index_in_dict(dict_list, key, value): return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) + + +def add_comment_in_checkins(log_names, duplicate, overlapping): + if duplicate: + text = _("Auto Attendance skipped due to duplicate attendance record: {}").format( + get_link_to_form("Attendance", duplicate[0].name) + ) + else: + text = _("Auto Attendance skipped due to overlapping attendance record: {}").format( + get_link_to_form("Attendance", overlapping.name) + ) + + for name in log_names: + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": "Employee Checkin", + "reference_name": name, + "content": text, + } + ).insert(ignore_permissions=True) + + +def skip_attendance_in_checkins(log_names): + EmployeeCheckin = frappe.qb.DocType("Employee Checkin") + ( + frappe.qb.update(EmployeeCheckin) + .set("skip_auto_attendance", 1) + .where(EmployeeCheckin.name.isin(log_names)) + ).run() diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 5e214cf7b7..a61bb9ee5f 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -134,7 +134,17 @@ class ShiftType(Document): shift_details = get_employee_shift(employee, timestamp, True) if shift_details and shift_details.shift_type.name == self.name: - mark_attendance(employee, date, "Absent", self.name) + attendance = mark_attendance(employee, date, "Absent", self.name) + if attendance: + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": "Attendance", + "reference_name": attendance, + "content": frappe._("Employee was marked Absent due to missing Employee Checkins."), + } + ).insert(ignore_permissions=True) def get_start_and_end_dates(self, employee): """Returns start and end dates for checking attendance and marking absent