From d3582ea399bb77d21bc1a2e1d902a3b3035628b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 25 Apr 2022 18:18:39 +0530 Subject: [PATCH 01/36] 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 9d4d76b8f63..174b7d7f463 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 b0ead459a01e281ff0ddb5cbe5ff22a5628a95a2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Apr 2022 13:30:13 +0530 Subject: [PATCH 02/36] 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 a700ad89a32..45e160d143e 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 03/36] 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 65e0cbb7a5c..7a6838680f0 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 b62b27bc4b3..7a90fb044f3 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 04/36] 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 4a99f068cd5..eacf480ef8f 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 95293d1dd5c..f27b0d55ef6 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 05/36] 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 dde52d7ad8e..a1d39d44231 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 06/36] 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 3b18a9ac26f..3c2d6b8d4cb 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 07/36] 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 3c2d6b8d4cb..df16643d460 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 08/36] 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 6703da60634..073820e9b9b 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 67bb29026f6a897bfda0dcccb4a0a2c0d3adeb7f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Apr 2022 20:15:15 +0530 Subject: [PATCH 09/36] fix: Ignore custom field validation while setup --- erpnext/regional/united_arab_emirates/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index cc647f85198..c77fdcc142d 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -244,7 +244,7 @@ def make_custom_fields(): "Supplier Quotation Item": invoice_item_fields, } - create_custom_fields(custom_fields) + create_custom_fields(custom_fields, ignore_validate=True) def add_print_formats(): From 5c3f9019cc4dda9e0c58a7bf9a824e64db995a21 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 21 Apr 2022 17:18:19 +0530 Subject: [PATCH 10/36] 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 fe2f14e19a6..1d0432bddbe 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 195e96486b3..8a6a0bae548 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 c604c711ef5..e984c082d48 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 33d7745c628..72524f036ec 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 11/36] 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 27b06d2dd93..1ba46f74148 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 12/36] 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 d3b4286be54..1d172ad0bf3 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 e94c34d7adc..4e67c84a0bc 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 026dd4e122a..4d05d7a345c 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 1ba46f74148..d1a9472f1f6 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 805286ddcc0..a96ebfcdee6 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 13/36] 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 72524f036ec..01448be6008 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 14/36] 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 1d172ad0bf3..a35e16f2c2f 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 01448be6008..3191f157828 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 15/36] 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 3191f157828..7564e8fd150 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 16/36] 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 7564e8fd150..3703e855025 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 17/36] 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 3703e855025..a94745677f5 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 18/36] 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 a94745677f5..9daf4f11c40 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 19/36] 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 9daf4f11c40..46c858878a3 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 20/36] 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 46c858878a3..60f5e34efbb 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 21/36] 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 60f5e34efbb..aa015755fe3 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 22/36] 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 aa015755fe3..94e9e53423b 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 23/36] 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 fe1b0d9f792..c928b57d58f 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 24/36] 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 c928b57d58f..ad7fd9a6976 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 94e9e53423b..53584f5079e 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 d1a9472f1f6..8ce05f15f7d 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 25/36] 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 c26660c5a25..e59c744712d 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 26/36] 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 8a6a0bae548..3797856db2f 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 53584f5079e..70d2f23070c 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 8ce05f15f7d..f552299806c 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 27/36] 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 a35e16f2c2f..b463213f50d 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 28/36] 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 859146bbcde..69749c93aff 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 29/36] 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 0b48f70eab6..26c9996dba9 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 30/36] 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 4e67c84a0bc..cb8eb30cb30 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 31/36] 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 e59c744712d..f8f5f80b341 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 00000000000..244c74778eb --- /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 32/36] 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 b5a45578c6d..f97e7ca9c68 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 e826e003c2f..1e59aae9a8d 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 33/36] 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 f8f5f80b341..cea3472447f 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 34/36] 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 fe0bb2c52df..270092722e5 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 35/36] 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 174b7d7f463..2d9ae9377fb 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 36/36] 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 64eb019b003..e07b5e5db52 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 5e214cf7b7a..a61bb9ee5f1 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