From 61eb9b6c6848253da4108fbe1483c94ea4ec8a26 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Mon, 14 Mar 2022 17:20:49 +0530 Subject: [PATCH 01/61] feat: Create single PL/DN from several SO. New PR from latest develop to avoid rebase --- erpnext/stock/doctype/pick_list/pick_list.py | 168 +++++++++++++----- .../stock/doctype/pick_list/test_pick_list.py | 98 +++++++++- 2 files changed, 214 insertions(+), 52 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index b2eaecb5868..3ec0324a11b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -24,8 +24,21 @@ class PickList(Document): def before_save(self): self.set_item_locations() + # set percentage picked in SO + for i in self.get('locations'): + if i.sales_order and frappe.db.get_value("Sales Order",i.sales_order,"per_picked") == 100: + frappe.throw("Row " + str(i.idx) + " has been picked already!") + def before_submit(self): 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: + item.picked_qty = item.stock_qty + + 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) + if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'): continue if not item.serial_no: @@ -37,6 +50,28 @@ class PickList(Document): frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity') .format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch")) + def before_cancel(self): + #update picked_qty in SO Item on cancel of PL + for i in self.get('locations'): + if i.sales_order_item: + self.update_so(i.sales_order_item,0,i.item_code) + + 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"]) + + if self.docstatus == 1: + if (((already_picked + picked_qty)/ actual_qty)*100) > (100 + flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance'))): + frappe.throw('You are picking more than required quantity for ' + item_code + '. Check if there is any other pick list created for '+so_doc.name) + + frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty) + + total_picked_qty = sum(flt(d.picked_qty) for d in so_doc.get('items')) + picked_qty + total_so_qty = sum(flt(d.stock_qty) for d in so_doc.get('items')) + per_picked = total_picked_qty/total_so_qty * 100 + + so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False) + @frappe.whitelist() def set_item_locations(self, save=False): self.validate_for_qty() @@ -64,10 +99,6 @@ class PickList(Document): item_doc.name = None for row in locations: - row.update({ - 'picked_qty': row.stock_qty - }) - location = item_doc.as_dict() location.update(row) self.append('locations', location) @@ -340,63 +371,106 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ def create_delivery_note(source_name, target_doc=None): pick_list = frappe.get_doc('Pick List', source_name) validate_item_locations(pick_list) - - sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order] - sales_orders = set(sales_orders) - + sales_dict = dict() + sales_orders = [] delivery_note = None - for sales_order in sales_orders: - delivery_note = create_delivery_note_from_sales_order(sales_order, - delivery_note, skip_item_mapping=True) + for d in pick_list.locations: + if d.sales_order: + sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order]) + + from itertools import groupby + from operator import itemgetter - # map rows without sales orders as well - if not delivery_note: + # 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]) + + if sales_dict: + delivery_note = create_dn_with_so(sales_dict,pick_list) + + is_item_wo_so = 0 + for n in pick_list.locations : + if not n.sales_order: + is_item_wo_so = 1 + break; + if is_item_wo_so == 1: + # Create a DN for items without sales orders as well + delivery_note = create_dn_wo_so(pick_list) + + frappe.msgprint(_('Delivery Note(s) created for the Pick List')) + return delivery_note + +def create_dn_wo_so(pick_list): delivery_note = frappe.new_doc("Delivery Note") - 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 - } - - item_table_mapper_without_so = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'name', - 'parent': '', + item_table_mapper_without_so = { + 'doctype': 'Delivery Note Item', + 'field_map': { + 'rate': 'rate', + 'name': 'name', + 'parent': '', + } } - } + map_pl_locations(pick_list,item_table_mapper_without_so,delivery_note) + delivery_note.insert(ignore_mandatory = True) + + return delivery_note + + +def create_dn_with_so(sales_dict,pick_list): + delivery_note = None + + 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 + for so in sales_dict[customer]: + map_pl_locations(pick_list,item_table_mapper,delivery_note,so) + delivery_note.insert(ignore_mandatory = True) + + return delivery_note + +def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None): for location in pick_list.locations: - 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: + 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 - source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \ - else [location, item_table_mapper_without_so] + source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \ + else [location, item_mapper] - dn_item = map_child_doc(source_doc, delivery_note, table_mapper) + dn_item = map_child_doc(source_doc, delivery_note, table_mapper) - if dn_item: - 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) + if dn_item: + 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 - delivery_note.customer = pick_list.customer if pick_list.customer else None + delivery_note.company = pick_list.company + delivery_note.customer = frappe.get_value("Sales Order",sales_order,"customer") - return delivery_note @frappe.whitelist() def create_stock_entry(pick_list): @@ -561,4 +635,4 @@ def update_common_item_properties(item, location): item.material_request = location.material_request item.serial_no = location.serial_no item.batch_no = location.batch_no - item.material_request_item = location.material_request_item + item.material_request_item = location.material_request_item \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index f3b6b89784a..82d8fe90111 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -7,7 +7,6 @@ from frappe import _dict test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] from frappe.tests.utils import FrappeTestCase - from erpnext.stock.doctype.item.test_item import create_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 @@ -188,7 +187,6 @@ class TestPickList(FrappeTestCase): }] }) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) pr1.cancel() @@ -311,6 +309,7 @@ class TestPickList(FrappeTestCase): 'item_code': '_Test Item', 'qty': 1, 'conversion_factor': 5, + 'stock_qty':5, 'delivery_date': frappe.utils.today() }, { 'item_code': '_Test Item', @@ -329,9 +328,9 @@ class TestPickList(FrappeTestCase): 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', - 'qty': 1, - 'stock_qty': 5, - 'conversion_factor': 5, + 'qty': 2, + 'stock_qty': 1, + 'conversion_factor': 0.5, 'sales_order': sales_order.name, 'sales_order_item': sales_order.items[0].name , }, { @@ -389,6 +388,95 @@ class TestPickList(FrappeTestCase): for expected_item, created_item in zip(expected_items, pl.locations): _compare_dicts(expected_item, created_item) + def test_multiple_dn_creation(self): + sales_order_1 = frappe.get_doc({ + 'doctype': 'Sales Order', + 'customer': '_Test Customer', + 'company': '_Test Company', + 'items': [{ + 'item_code': '_Test Item', + 'qty': 1, + 'conversion_factor': 1, + 'delivery_date': frappe.utils.today() + }], + }).insert() + sales_order_1.submit() + sales_order_2 = frappe.get_doc({ + 'doctype': 'Sales Order', + 'customer': '_Test Customer 1', + 'company': '_Test Company', + 'items': [{ + 'item_code': '_Test Item 2', + 'qty': 1, + 'conversion_factor': 1, + 'delivery_date': frappe.utils.today() + }, + ], + }).insert() + sales_order_2.submit() + pick_list = frappe.get_doc({ + 'doctype': 'Pick List', + 'company': '_Test Company', + 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', + 'picker':'P001', + 'locations': [{ + 'item_code': '_Test Item ', + 'qty': 1, + 'stock_qty': 1, + 'conversion_factor': 1, + 'sales_order': sales_order_1.name, + 'sales_order_item': sales_order_1.items[0].name , + }, { + 'item_code': '_Test Item 2', + 'qty': 1, + 'stock_qty': 1, + 'conversion_factor': 1, + 'sales_order': sales_order_2.name, + 'sales_order_item': sales_order_2.items[0].name , + } + ] + }) + pick_list.set_item_locations() + pick_list.submit() + create_delivery_note(pick_list.name) + for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer"},fields={"name"}): + for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): + self.assertEqual(dn_item.item_code, '_Test Item') + self.assertEqual(dn_item.against_sales_order,sales_order_1.name) + for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer 1"},fields={"name"}): + for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): + self.assertEqual(dn_item.item_code, '_Test Item 2') + self.assertEqual(dn_item.against_sales_order,sales_order_2.name) + #test DN creation without so + pick_list_1 = frappe.get_doc({ + 'doctype': 'Pick List', + 'company': '_Test Company', + 'purpose': 'Delivery', + 'picker':'P001', + 'locations': [{ + 'item_code': '_Test Item ', + 'qty': 1, + 'stock_qty': 1, + 'conversion_factor': 1, + }, { + 'item_code': '_Test Item 2', + 'qty': 2, + 'stock_qty': 2, + 'conversion_factor': 1, + } + ] + }) + pick_list_1.set_item_locations() + pick_list_1.submit() + create_delivery_note(pick_list_1.name) + for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list_1.name},fields={"name"}): + for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): + if dn_item.item_code == '_Test Item': + self.assertEqual(dn_item.qty,1) + if dn_item.item_code == '_Test Item 2': + self.assertEqual(dn_item.qty,2) + # def test_pick_list_skips_items_in_expired_batch(self): # pass From 466df6bdb76cbd7da88f4d092e608b4c39889929 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Mon, 14 Mar 2022 17:29:02 +0530 Subject: [PATCH 02/61] to enable selection of SO irrespective of customer,removed validation for customer --- erpnext/stock/doctype/pick_list/pick_list.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 730fd7a829c..ababf3dc420 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -146,10 +146,7 @@ frappe.ui.form.on('Pick List', { customer: frm.doc.customer }; frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => { - if (!frm.doc.customer) { - frappe.msgprint(__('Please select Customer first')); - return; - } + erpnext.utils.map_current_doc({ method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list', source_doctype: 'Sales Order', From f33a725a9e137d5b9d7290baa072152e26740328 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Tue, 15 Mar 2022 15:35:51 +0530 Subject: [PATCH 03/61] fixed spacings --- erpnext/stock/doctype/pick_list/pick_list.js | 1 - erpnext/stock/doctype/pick_list/pick_list.py | 6 ++---- erpnext/stock/doctype/pick_list/test_pick_list.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index ababf3dc420..13b74b5eb16 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -146,7 +146,6 @@ frappe.ui.form.on('Pick List', { customer: frm.doc.customer }; frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => { - erpnext.utils.map_current_doc({ method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list', source_doctype: 'Sales Order', diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 3ec0324a11b..915400aa646 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -3,6 +3,8 @@ import json from collections import OrderedDict, defaultdict +from itertools import groupby +from operator import itemgetter import frappe from frappe import _ @@ -377,10 +379,6 @@ def create_delivery_note(source_name, target_doc=None): for d in pick_list.locations: if d.sales_order: sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order]) - - from itertools import groupby - from operator import itemgetter - # 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]) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 82d8fe90111..f60104c09ac 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -7,6 +7,7 @@ from frappe import _dict test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.item.test_item import create_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 @@ -16,7 +17,6 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( class TestPickList(FrappeTestCase): - def test_pick_list_picks_warehouse_for_each_item(self): try: frappe.get_doc({ From fbcb413d9689222442f56197ab14bc17bd9b72bb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Mar 2022 18:20:11 +0530 Subject: [PATCH 04/61] fix: Error in bank reco statement --- .../bank_reconciliation_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index b72d2669775..fe644ed3569 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -267,7 +267,7 @@ def get_loan_amount(filters): total_amount += flt(amount) - return amount + return total_amount def get_balance_row(label, amount, account_currency): if amount > 0: From a68213d82e7a98e289f04cfc5ee58c2f6d5730d9 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Tue, 15 Mar 2022 20:18:23 +0530 Subject: [PATCH 05/61] added new field - Picked Qty, in Sales Order Item --- .../doctype/sales_order_item/sales_order_item.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 7e55499533b..195e96486b3 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -23,6 +23,7 @@ "quantity_and_rate", "qty", "stock_uom", + "picked_qty", "col_break2", "uom", "conversion_factor", @@ -798,12 +799,17 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "fieldname": "picked_qty", + "fieldtype": "Float", + "label": "Picked Qty" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-24 14:41:57.325799", + "modified": "2022-03-15 20:17:33.984799", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 9f7fee7a4f601a455003a2e7b2deddeb7acfbcab Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Tue, 15 Mar 2022 21:40:51 +0530 Subject: [PATCH 06/61] Added new field in SO - % Picked --- erpnext/selling/doctype/sales_order/sales_order.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7e99a062439..fe2f14e19a6 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -130,6 +130,7 @@ "per_delivered", "column_break_81", "per_billed", + "per_picked", "billing_status", "sales_team_section_break", "sales_partner", @@ -1514,13 +1515,19 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "fieldname": "per_picked", + "fieldtype": "Percent", + "label": "% Picked", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:16:40.775704", + "modified": "2022-03-15 21:38:31.437586", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1594,6 +1601,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "customer_name", "track_changes": 1, From e970616b511e8aaa1a07fd96030029992ed2e6f6 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Tue, 15 Mar 2022 21:52:34 +0530 Subject: [PATCH 07/61] removed semicolon for break statement --- erpnext/stock/doctype/pick_list/pick_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 915400aa646..598378a95cb 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -390,7 +390,7 @@ def create_delivery_note(source_name, target_doc=None): for n in pick_list.locations : if not n.sales_order: is_item_wo_so = 1 - break; + break if is_item_wo_so == 1: # Create a DN for items without sales orders as well delivery_note = create_dn_wo_so(pick_list) @@ -433,7 +433,7 @@ def create_dn_with_so(sales_dict,pick_list): }, 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 } - break; + break if delivery_note: # map all items of all sales orders of that customer for so in sales_dict[customer]: From 06936cf1c024ffa4ed471790b8859aeadc5c5575 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Mar 2022 09:02:04 +0530 Subject: [PATCH 08/61] fix: Validate income/expense account in sales and purchase invoice --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 ++++++ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 +++++++ erpnext/controllers/accounts_controller.py | 6 +++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2c3156175fb..7654aa44dc6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -32,6 +32,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.buying_controller import BuyingController from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( @@ -107,6 +108,7 @@ class PurchaseInvoice(BuyingController): self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "stock_qty") self.set_expense_account(for_validate=True) + self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") @@ -311,6 +313,10 @@ class PurchaseInvoice(BuyingController): elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) + def validate_expense_account(self): + for item in self.get('items'): + validate_account_head(item.idx, item.expense_account, self.company, 'Expense') + def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 862ac81ff38..5aabc21c14e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -37,6 +37,7 @@ from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_regain, make_depreciation_entry, ) +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.setup.doctype.company.company import update_company_current_month_sales @@ -108,6 +109,8 @@ class SalesInvoice(SellingController): self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() + self.validate_income_account() + validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference) if cint(self.is_pos): @@ -175,6 +178,10 @@ class SalesInvoice(SellingController): if cost_center_company != self.company: frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company))) + def validate_income_account(self): + for item in self.get('items'): + validate_account_head(item.idx, item.income_account, self.company, 'Income') + def set_tax_withholding(self): tax_withholding_details = get_party_tax_withholding_details(self) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a94af10cde4..34ff45708be 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1566,12 +1566,12 @@ def validate_taxes_and_charges(tax): tax.rate = None -def validate_account_head(idx, account, company): +def validate_account_head(idx, account, company, context=''): account_company = frappe.get_cached_value('Account', account, 'company') if account_company != company: - frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) + frappe.throw(_('Row {0}: {3} Account {1} does not belong to Company {2}') + .format(idx, frappe.bold(account), frappe.bold(company), context), title=_('Invalid Account')) def validate_cost_center(tax, doc): From 0211f27e83255bc3c40b8866a3a4fc49909b8279 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Wed, 16 Mar 2022 12:28:30 +0530 Subject: [PATCH 09/61] as per review comments - changed for loop --- erpnext/stock/doctype/pick_list/pick_list.py | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 598378a95cb..373dfac85d6 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -27,9 +27,9 @@ class PickList(Document): self.set_item_locations() # set percentage picked in SO - for i in self.get('locations'): - if i.sales_order and frappe.db.get_value("Sales Order",i.sales_order,"per_picked") == 100: - frappe.throw("Row " + str(i.idx) + " has been picked already!") + for location in self.get('locations'): + if location.sales_order and frappe.db.get_value("Sales Order",location.sales_order,"per_picked") == 100: + frappe.throw("Row " + str(location.idx) + " has been picked already!") def before_submit(self): for item in self.locations: @@ -54,9 +54,9 @@ class PickList(Document): def before_cancel(self): #update picked_qty in SO Item on cancel of PL - for i in self.get('locations'): - if i.sales_order_item: - self.update_so(i.sales_order_item,0,i.item_code) + for location in self.get('locations'): + if location.sales_order_item: + self.update_so(location.sales_order_item,0,location.item_code) 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")) @@ -68,8 +68,12 @@ class PickList(Document): frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty) - total_picked_qty = sum(flt(d.picked_qty) for d in so_doc.get('items')) + picked_qty - total_so_qty = sum(flt(d.stock_qty) for d in so_doc.get('items')) + 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) + 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) @@ -376,9 +380,9 @@ def create_delivery_note(source_name, target_doc=None): sales_dict = dict() sales_orders = [] delivery_note = None - for d in pick_list.locations: - if d.sales_order: - sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order]) + 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]) # 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]) @@ -387,8 +391,8 @@ def create_delivery_note(source_name, target_doc=None): delivery_note = create_dn_with_so(sales_dict,pick_list) is_item_wo_so = 0 - for n in pick_list.locations : - if not n.sales_order: + for location in pick_list.locations : + if not location.sales_order: is_item_wo_so = 1 break if is_item_wo_so == 1: From a12895ec035af45cacb04fd9459df6eb153024a4 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Wed, 16 Mar 2022 13:56:54 +0530 Subject: [PATCH 10/61] corrected spacing --- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 373dfac85d6..3a496866cf1 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -73,7 +73,7 @@ class PickList(Document): for item in so_doc.get('items'): total_picked_qty += flt(item.picked_qty) total_so_qty += flt(item.stock_qty) - total_picked_qty = total_picked_qty + picked_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) From 119273e633ec8e56c7d5c4649ef81c3deeb5f7d2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Mar 2022 11:56:10 +0530 Subject: [PATCH 11/61] fix: custom cash flow mapper doesn't show any data --- .../report/cash_flow/custom_cash_flow.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index 45d147e7a21..ec0c9a7c7ea 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -30,12 +30,12 @@ def get_mappers_from_db(): def get_accounts_in_mappers(mapping_names): return frappe.db.sql(''' select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability, - cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment + cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account from `tabCash Flow Mapping Accounts` cfma join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name where cfma.parent in (%s) order by cfm.is_working_capital - ''', (', '.join('"%s"' % d for d in mapping_names))) + ''', (', '.join('%s' % d for d in mapping_names))) def setup_mappers(mappers): @@ -57,31 +57,31 @@ def setup_mappers(mappers): account_types = [ dict( - name=account[0], label=account[1], is_working_capital=account[2], + name=account[0], account_name=account[7], label=account[1], is_working_capital=account[2], is_income_tax_liability=account[3], is_income_tax_expense=account[4] ) for account in accounts if not account[3]] finance_costs_adjustments = [ dict( - name=account[0], label=account[1], is_finance_cost=account[5], + name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5], is_finance_cost_adjustment=account[6] ) for account in accounts if account[6]] tax_liabilities = [ dict( - name=account[0], label=account[1], is_income_tax_liability=account[3], + name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3], is_income_tax_expense=account[4] ) for account in accounts if account[3]] tax_expenses = [ dict( - name=account[0], label=account[1], is_income_tax_liability=account[3], + name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3], is_income_tax_expense=account[4] ) for account in accounts if account[4]] finance_costs = [ dict( - name=account[0], label=account[1], is_finance_cost=account[5]) + name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5]) for account in accounts if account[5]] account_types_labels = sorted( @@ -124,27 +124,27 @@ def setup_mappers(mappers): ) for label in account_types_labels: - names = [d['name'] for d in account_types if d['label'] == label[0]] + names = [d['account_name'] for d in account_types if d['label'] == label[0]] m = dict(label=label[0], names=names, is_working_capital=label[1]) mapping['account_types'].append(m) for label in fc_adjustment_labels: - names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]] + names = [d['account_name'] for d in finance_costs_adjustments if d['label'] == label[0]] m = dict(label=label[0], names=names) mapping['finance_costs_adjustments'].append(m) for label in unique_liability_labels: - names = [d['name'] for d in tax_liabilities if d['label'] == label[0]] + names = [d['account_name'] for d in tax_liabilities if d['label'] == label[0]] m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) mapping['tax_liabilities'].append(m) for label in unique_expense_labels: - names = [d['name'] for d in tax_expenses if d['label'] == label[0]] + names = [d['account_name'] for d in tax_expenses if d['label'] == label[0]] m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) mapping['tax_expenses'].append(m) for label in unique_finance_costs_labels: - names = [d['name'] for d in finance_costs if d['label'] == label[0]] + names = [d['account_name'] for d in finance_costs if d['label'] == label[0]] m = dict(label=label[0], names=names, is_finance_cost=label[1]) mapping['finance_costs'].append(m) @@ -378,7 +378,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) - accounts = ', '.join('"%s"' % d for d in account_names) + accounts = ', '.join('%s' % d for d in account_names) if opening_balances: date_info = dict(date=start_date) From 2dd0e20003f9da13d51ced0da5ee5c492e621230 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Mar 2022 12:56:43 +0530 Subject: [PATCH 12/61] fix: Clean and fixes in Dimension-wise Accounts Balance Report (cherry picked from commit 08a06ce5c68023c41f53b584082b5e1c4ddb1f59) --- .../dimension_wise_accounts_balance_report.js | 5 +- .../dimension_wise_accounts_balance_report.py | 154 ++++++++---------- 2 files changed, 76 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js index 6a0394861b8..ea05a35b259 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js @@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("From Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname": "finance_book", @@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldname": "dimension", "label": __("Select Dimension"), "fieldtype": "Select", + "default": "Cost Center", "options": get_accounting_dimension_options(), "reqd": 1, }, @@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }); function get_accounting_dimension_options() { - let options =["", "Cost Center", "Project"]; + let options =["Cost Center", "Project"]; frappe.db.get_list('Accounting Dimension', {fields:['document_type']}).then((res) => { res.forEach((dimension) => { diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index d547470bf3c..d39e320f3ce 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -15,20 +15,22 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters def execute(filters=None): - validate_filters(filters) - dimension_items_list = get_dimension_items_list(filters.dimension, filters.company) - if not dimension_items_list: + validate_filters(filters) + dimension_list = get_dimensions(filters) + + if not dimension_list: return [], [] - dimension_items_list = [''.join(d) for d in dimension_items_list] - columns = get_columns(dimension_items_list) - data = get_data(filters, dimension_items_list) + # dimension_items_list = [''.join(d) for d in dimension_items_list] + columns = get_columns(dimension_list) + data = get_data(filters, dimension_list) return columns, data -def get_data(filters, dimension_items_list): +def get_data(filters, dimension_list): company_currency = erpnext.get_company_currency(filters.company) + acc = frappe.db.sql(""" select name, account_number, parent_account, lft, rgt, root_type, @@ -51,60 +53,54 @@ def get_data(filters, dimension_items_list): where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company)) gl_entries_by_account = {} - set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account) - format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list) - accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list) - out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list) + set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account) + format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, + frappe.scrub(filters.get('dimension'))) + accumulate_values_into_parents(accounts, accounts_by_name, dimension_list) + out = prepare_data(accounts, filters, company_currency, dimension_list) out = filter_out_zero_value_rows(out, parent_children_map) return out -def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account): - for item in dimension_items_list: - condition = get_condition(filters.from_date, item, filters.dimension) - if account: - condition += " and account in ({})"\ - .format(", ".join([frappe.db.escape(d) for d in account])) +def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account): + condition = get_condition(filters.get('dimension')) - gl_filters = { - "company": filters.get("company"), - "from_date": filters.get("from_date"), - "to_date": filters.get("to_date"), - "finance_book": cstr(filters.get("finance_book")) - } + if account: + condition += " and account in ({})"\ + .format(", ".join([frappe.db.escape(d) for d in account])) - gl_filters['item'] = ''.join(item) + gl_filters = { + "company": filters.get("company"), + "from_date": filters.get("from_date"), + "to_date": filters.get("to_date"), + "finance_book": cstr(filters.get("finance_book")) + } - if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", - filters.company, 'default_finance_book') + gl_filters['dimensions'] = set(dimension_list) - for key, value in filters.items(): - if value: - gl_filters.update({ - key: value - }) + if filters.get("include_default_book_entries"): + gl_filters["company_fb"] = frappe.db.get_value("Company", + filters.company, 'default_finance_book') - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql(""" select - posting_date, account, debit, credit, is_opening, fiscal_year, + posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {condition} + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and is_cancelled = 0 order by account, posting_date""".format( - condition=condition), - gl_filters, as_dict=True) #nosec + dimension = frappe.scrub(filters.get('dimension')), condition=condition), gl_filters, as_dict=True) #nosec - for entry in gl_entries: - entry['dimension_item'] = ''.join(item) - gl_entries_by_account.setdefault(entry.account, []).append(entry) + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) -def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list): +def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type): for entries in gl_entries_by_account.values(): for entry in entries: @@ -114,11 +110,12 @@ def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_l _("Could not retrieve information for {0}.").format(entry.account), title="Error", raise_exception=1 ) - for item in dimension_items_list: - if item == entry.dimension_item: - d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit) -def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list): + for dimension in dimension_list: + if dimension == entry.get(dimension_type): + d[frappe.scrub(dimension)] = d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit) + +def prepare_data(accounts, filters, company_currency, dimension_list): data = [] for d in accounts: @@ -135,13 +132,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen if d.account_number else d.account_name) } - for item in dimension_items_list: - row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3) + for dimension in dimension_list: + row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3) - if abs(row[frappe.scrub(item)]) >= 0.005: + if abs(row[frappe.scrub(dimension)]) >= 0.005: # ignore zero values has_value = True - total += flt(d.get(frappe.scrub(item), 0.0), 3) + total += flt(d.get(frappe.scrub(dimension), 0.0), 3) row["has_value"] = has_value row["total"] = total @@ -149,62 +146,55 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen return data -def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list): +def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - for item in dimension_items_list: - accounts_by_name[d.parent_account][frappe.scrub(item)] = \ - accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0) + for dimension in dimension_list: + accounts_by_name[d.parent_account][frappe.scrub(dimension)] = \ + accounts_by_name[d.parent_account].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0) -def get_condition(from_date, item, dimension): +def get_condition(dimension): conditions = [] - if from_date: - conditions.append("posting_date >= %(from_date)s") - if dimension: - if dimension not in ['Cost Center', 'Project']: - if dimension in ['Customer', 'Supplier']: - dimension = 'Party' - else: - dimension = 'Voucher No' - txt = "{0} = %(item)s".format(frappe.scrub(dimension)) - conditions.append(txt) + conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension))) return " and {}".format(" and ".join(conditions)) if conditions else "" -def get_dimension_items_list(dimension, company): - meta = frappe.get_meta(dimension, cached=False) - fieldnames = [d.fieldname for d in meta.get("fields")] - filters = {} - if 'company' in fieldnames: - filters['company'] = company - return frappe.get_all(dimension, filters, as_list=True) +def get_dimensions(filters): + meta = frappe.get_meta(filters.get('dimension'), cached=False) + query_filters = {} -def get_columns(dimension_items_list, accumulated_values=1, company=None): + if meta.has_field('company'): + query_filters = {'company': filters.get('company')} + + return frappe.get_all(filters.get('dimension'), filters=query_filters, pluck='name') + +def get_columns(dimension_list): columns = [{ "fieldname": "account", "label": _("Account"), "fieldtype": "Link", "options": "Account", "width": 300 + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1 }] - if company: + + for dimension in dimension_list: columns.append({ - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) - for item in dimension_items_list: - columns.append({ - "fieldname": frappe.scrub(item), - "label": item, + "fieldname": frappe.scrub(dimension), + "label": dimension, "fieldtype": "Currency", "options": "currency", "width": 150 }) + columns.append({ "fieldname": "total", "label": "Total", From 5f89cb2ccaf7656233ff6298422e333817dac82c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Mar 2022 12:57:12 +0530 Subject: [PATCH 13/61] fix: Remove comments (cherry picked from commit cab69fe1f298b44613be21c06591e868c24ee249) --- .../dimension_wise_accounts_balance_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index d39e320f3ce..0c2b6cb4cb1 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -22,7 +22,6 @@ def execute(filters=None): if not dimension_list: return [], [] - # dimension_items_list = [''.join(d) for d in dimension_items_list] columns = get_columns(dimension_list) data = get_data(filters, dimension_list) From 4237e5d9283023bc41e6efb147140750d1075b17 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Mar 2022 18:00:39 +0530 Subject: [PATCH 14/61] fix: Test case --- .../test_deferred_revenue_and_expense.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 86eb2134fe8..17475a77dbf 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -88,10 +88,12 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): posting_date="2021-05-01", parent_cost_center="Main - _CD", cost_center="Main - _CD", - do_not_submit=True, + do_not_save=True, rate=300, price_list_rate=300, ) + + si.items[0].income_account = "Sales - _CD" si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2021-05-01" si.items[0].service_end_date = "2021-08-01" @@ -269,11 +271,13 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): posting_date="2021-05-01", parent_cost_center="Main - _CD", cost_center="Main - _CD", - do_not_submit=True, + do_not_save=True, rate=300, price_list_rate=300, ) + si.items[0].enable_deferred_revenue = 1 + si.items[0].income_account = "Sales - _CD" si.items[0].deferred_revenue_account = deferred_revenue_account si.items[0].income_account = "Sales - _CD" si.save() From 00bfee97c766e771a1ab0b57d223ba9e87b70e9a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 18 Mar 2022 14:26:33 +0530 Subject: [PATCH 15/61] refactor: convert raw sql to frappe.qb --- .../report/cash_flow/custom_cash_flow.py | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index ec0c9a7c7ea..20f7fcfb1b3 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.utils import add_to_date +from frappe.query_builder.functions import Sum +from frappe.utils import add_to_date, get_date_str from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( @@ -28,15 +29,22 @@ def get_mappers_from_db(): def get_accounts_in_mappers(mapping_names): - return frappe.db.sql(''' - select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability, - cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account - from `tabCash Flow Mapping Accounts` cfma - join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name - where cfma.parent in (%s) - order by cfm.is_working_capital - ''', (', '.join('%s' % d for d in mapping_names))) + cfm = frappe.qb.DocType('Cash Flow Mapping') + cfma = frappe.qb.DocType('Cash Flow Mapping Accounts') + result = ( + frappe.qb + .select( + cfma.name, cfm.label, cfm.is_working_capital, + cfm.is_income_tax_liability, cfm.is_income_tax_expense, + cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account + ) + .from_(cfm) + .join(cfma) + .on(cfm.name == cfma.parent) + .where(cfma.parent.isin(mapping_names)) + ).run() + return result def setup_mappers(mappers): cash_flow_accounts = [] @@ -371,14 +379,30 @@ def execute(filters=None): def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0): + if not account_names or not account_names[0] or not type(account_names[0]) == str: + # only proceed if account_names is a list of account names + return {} + from erpnext.accounts.report.cash_flow.cash_flow import get_start_date company = filters.company data = {} total = 0 + GLEntry = frappe.qb.DocType('GL Entry') + Account = frappe.qb.DocType('Account') + for period in period_list: start_date = get_start_date(period, accumulated_values, company) - accounts = ', '.join('%s' % d for d in account_names) + + account_subquery = ( + frappe.qb.from_(Account) + .where( + (Account.name.isin(account_names)) | + (Account.parent_account.isin(account_names)) + ) + .select(Account.name) + .as_("account_subquery") + ) if opening_balances: date_info = dict(date=start_date) @@ -395,32 +419,31 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate else: start, end = add_to_date(**date_info), add_to_date(**date_info) - gl_sum = frappe.db.sql_list(""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE name IN (%s) - OR parent_account IN (%s)) - """, (company, start, end, accounts, accounts)) - else: - gl_sum = frappe.db.sql_list(""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE name IN (%s) - OR parent_account IN (%s)) - """, (company, start_date if accumulated_values else period['from_date'], - period['to_date'], accounts, accounts)) + start, end = get_date_str(start), get_date_str(end) - if gl_sum and gl_sum[0]: - amount = gl_sum[0] else: - amount = 0 + start, end = start_date if accumulated_values else period['from_date'], period['to_date'] + start, end = get_date_str(start), get_date_str(end) - total += amount - data.setdefault(period["key"], amount) + result = ( + frappe.qb.from_(GLEntry) + .select(Sum(GLEntry.credit) - Sum(GLEntry.debit)) + .where( + (GLEntry.company == company) & + (GLEntry.posting_date >= start) & + (GLEntry.posting_date <= end) & + (GLEntry.voucher_type != 'Period Closing Voucher') & + (GLEntry.account.isin(account_subquery)) + ) + ).run() + + if result and result[0]: + gl_sum = result[0][0] + else: + gl_sum = 0 + + total += gl_sum + data.setdefault(period["key"], gl_sum) data["total"] = total return data From 5a9bf9ffd64ffba14cb4bff63431fff319a82ed7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 18 Mar 2022 17:29:09 +0530 Subject: [PATCH 16/61] fix: respect db multi_tenancy while fetching precision (#30301) [skip ci] --- erpnext/stock/report/stock_ageing/stock_ageing.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 97a740e1844..7ca40033edb 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -12,7 +12,7 @@ from frappe.utils import cint, date_diff, flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict -precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] @@ -30,6 +30,8 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li _func = itemgetter(1) data = [] + precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 details = item_dict["details"] @@ -76,6 +78,9 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: + + precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) + range1 = range2 = range3 = above_range3 = 0.0 for item in fifo_queue: From 972d06555aea8a8e4aec3186f3844b942fa54d38 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Mar 2022 17:32:46 +0530 Subject: [PATCH 17/61] fix: Add permission for KSA Vat documents --- erpnext/patches.txt | 1 + erpnext/patches/v13_0/enable_ksa_vat_docs.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 erpnext/patches/v13_0/enable_ksa_vat_docs.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 16d8c730a14..31999126214 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -333,6 +333,7 @@ erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.non_profit_deprecation_warning +erpnext.patches.v13_0.enable_ksa_vat_docs #1 [post_model_sync] erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py new file mode 100644 index 00000000000..3f482620e16 --- /dev/null +++ b/erpnext/patches/v13_0/enable_ksa_vat_docs.py @@ -0,0 +1,12 @@ +import frappe + +from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if not company: + return + + add_print_formats() + add_permissions() \ No newline at end of file From c2cbd407c14d0d9763627adefc5f81ab264eccac Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 19 Mar 2022 20:31:06 +0530 Subject: [PATCH 18/61] fix: Allow on Submit for Material Request Item Required Date (#30174) (#30308) * fix: Allow on Submit for Material Request Item Required Date * chore: whitespace (cherry picked from commit cca9668309711bf3ca517726a94df547c5a16bbb) Co-authored-by: Devarsh Bhatt <58166671+bhattdevarsh@users.noreply.github.com> Co-authored-by: Ankush Menat --- erpnext/stock/doctype/material_request/material_request.py | 3 +++ .../doctype/material_request_item/material_request_item.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 51209acb275..49fefae550c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -82,6 +82,9 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def before_update_after_submit(self): + self.validate_schedule_date() + def validate_material_request_type(self): """ Validate fields in accordance with selected type """ diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 2bad42a0ebb..dd66cfff8be 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -177,6 +177,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "bold": 1, "columns": 2, "fieldname": "schedule_date", @@ -459,7 +460,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-03 14:40:24.409826", + "modified": "2022-03-10 18:42:42.705190", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", From f4c213379e03f09c59dac1c16efbafe00ae37e3f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 17:47:18 +0530 Subject: [PATCH 19/61] fix: set current qty as default qty for stock entry --- erpnext/public/js/utils/serial_no_batch_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 08270bdea1a..79864b717c7 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { fieldtype:'Float', read_only: me.has_batch && !me.has_serial_no, label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'), - default: flt(me.item.stock_qty), + default: flt(me.item.stock_qty) || flt(me.item.transfer_qty), }, ...get_pending_qty_fields(me), { From 5ec27c90552ab5c95ef734bb620a0e1bdd5256ee Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 17:48:43 +0530 Subject: [PATCH 20/61] fix: filter serial nos by selected batch number --- erpnext/public/js/utils/serial_no_batch_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 79864b717c7..ae8d3d455cb 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -100,7 +100,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { qty: qty, item_code: me.item_code, warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', - batch_no: me.item.batch_no || null, + batch_nos: me.item.batch_no || null, posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date } }); From 0a533d6ccc7356ef8b7c10e1c581cf630f7c3121 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 17:49:19 +0530 Subject: [PATCH 21/61] fix: skip already selected serials in sr selector --- .../js/utils/serial_no_batch_selector.js | 28 +++++++++++++++---- erpnext/stock/doctype/serial_no/serial_no.py | 15 ++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index ae8d3d455cb..81ff351d373 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -94,6 +94,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { description: __('Fetch Serial Numbers based on FIFO'), click: () => { let qty = this.dialog.fields_dict.qty.get_value(); + let already_selected_serial_nos = get_selected_serial_nos(me); let numbers = frappe.call({ method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", args: { @@ -101,7 +102,8 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { item_code: me.item_code, warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', batch_nos: me.item.batch_no || null, - posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date + posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date, + exclude_sr_nos: already_selected_serial_nos } }); @@ -577,15 +579,29 @@ function get_pending_qty_fields(me) { return pending_qty_fields; } -function calc_total_selected_qty(me) { +// get all items with same item code except row for which selector is open. +function get_rows_with_same_item_code(me) { const { frm: { doc: { items }}, item: { name, item_code }} = me; - const totalSelectedQty = items - .filter( item => ( item.name !== name ) && ( item.item_code === item_code ) ) - .map( item => flt(item.qty) ) - .reduce( (i, j) => i + j, 0); + return items.filter(item => (item.name !== name) && (item.item_code === item_code)) +} + +function calc_total_selected_qty(me) { + const totalSelectedQty = get_rows_with_same_item_code(me) + .map(item => flt(item.qty)) + .reduce((i, j) => i + j, 0); return totalSelectedQty; } +function get_selected_serial_nos(me) { + const selected_serial_nos = get_rows_with_same_item_code(me) + .map(item => item.serial_no) + .filter(serial => serial) + .map(sr_no_string => sr_no_string.split('\n')) + .reduce((acc, arr) => acc.concat(arr), []) + .filter(serial => serial); + return selected_serial_nos; +}; + function check_can_calculate_pending_qty(me) { const { frm: { doc }, item } = me; const docChecks = doc.bom_no diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bf62f50c971..3cb97558c2f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -564,9 +564,15 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): return serial_nos @frappe.whitelist() -def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None): +def auto_fetch_serial_number(qty, item_code, warehouse, + posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None): filters = { "item_code": item_code, "warehouse": warehouse } + if exclude_sr_nos is None: + exclude_sr_nos = [] + else: + exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos))) + if batch_nos: try: filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)] @@ -578,10 +584,9 @@ def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch serial_numbers = [] if for_doctype == 'POS Invoice': - reserved_sr_nos = get_pos_reserved_serial_nos(filters) - serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos) - else: - serial_numbers = fetch_serial_numbers(filters, qty) + exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters)) + + serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos) return [d.get('name') for d in serial_numbers] From 4f8bb91eae3ac6d3a3837a21be7f635a4044ccd0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 18:39:42 +0530 Subject: [PATCH 22/61] fix: sort serial nos before sending --- erpnext/stock/doctype/serial_no/serial_no.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 3cb97558c2f..884cb1e9969 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -588,7 +588,7 @@ def auto_fetch_serial_number(qty, item_code, warehouse, serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos) - return [d.get('name') for d in serial_numbers] + return sorted([d.get('name') for d in serial_numbers]) @frappe.whitelist() def get_pos_reserved_serial_nos(filters): From b9eec331e3b884a2da5ee6995cca1a7135b7c3b0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 18:40:14 +0530 Subject: [PATCH 23/61] test: auto serial fetching --- .../stock/doctype/serial_no/test_serial_no.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 057a7d4c01f..0d362d9024e 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -6,10 +6,12 @@ import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -18,9 +20,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse test_dependencies = ["Item"] test_records = frappe.get_test_records('Serial No') -from frappe.tests.utils import FrappeTestCase - -from erpnext.stock.doctype.serial_no.serial_no import * class TestSerialNo(FrappeTestCase): @@ -242,3 +241,48 @@ class TestSerialNo(FrappeTestCase): ) self.assertEqual(value_diff, -113) + def test_auto_fetch(self): + item_code = make_item(properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "serial_no_series": "TEST.#######" + }).name + warehouse = "_Test Warehouse - _TC" + + in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5) + in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5) + + in1.reload() + in2.reload() + + batch_wise_serials = { + in1.items[0].batch_no: get_serial_nos(in1.items[0].serial_no), + in2.items[0].batch_no: get_serial_nos(in2.items[0].serial_no) + } + + # Test FIFO + first_fetch = auto_fetch_serial_number(5, item_code, warehouse) + self.assertEqual(first_fetch, batch_wise_serials[in1.items[0].batch_no]) + + # partial FIFO + partial_fetch = auto_fetch_serial_number(2, item_code, warehouse) + self.assertTrue(set(partial_fetch).issubset(set(first_fetch)), + msg=f"{partial_fetch} should be subset of {first_fetch}") + + # exclusion + remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch) + self.assertEqual(sorted(remaining + partial_fetch), first_fetch) + + # batchwise + for batch, expected_serials in batch_wise_serials.items(): + fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch) + self.assertEqual(fetched_sr, sorted(expected_serials)) + + # non existing warehouse + self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), []) + + # multi batch + all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list] + fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys())) + self.assertEqual(sorted(all_serials), fetched_serials) From a585dff6fd05c885c6b4c35892f48d1fdf644487 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 18:50:05 +0530 Subject: [PATCH 24/61] refactor: batch no filter handling --- erpnext/stock/doctype/serial_no/serial_no.py | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 884cb1e9969..244f3e2f02d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -7,7 +7,16 @@ import json import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate +from frappe.utils import ( + add_days, + cint, + cstr, + flt, + get_link_to_form, + getdate, + nowdate, + safe_json_loads, +) from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so @@ -566,7 +575,8 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): @frappe.whitelist() def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None): - filters = { "item_code": item_code, "warehouse": warehouse } + + filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) if exclude_sr_nos is None: exclude_sr_nos = [] @@ -574,13 +584,14 @@ def auto_fetch_serial_number(qty, item_code, warehouse, exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos))) if batch_nos: - try: - filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)] - except Exception: - filters["batch_no"] = [batch_nos] + batch_nos = safe_json_loads(batch_nos) + if isinstance(batch_nos, list): + filters.batch_no = batch_nos + elif isinstance(batch_nos, str): + filters.batch_no = [batch_nos] if posting_date: - filters["expiry_date"] = posting_date + filters.expiry_date = posting_date serial_numbers = [] if for_doctype == 'POS Invoice': From 4b695915f458a7acb5257d1208d91f23998727eb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 19:10:57 +0530 Subject: [PATCH 25/61] refactor: Use QB for serial fetching query --- erpnext/stock/doctype/serial_no/serial_no.py | 55 ++++++++++--------- .../stock/doctype/serial_no/test_serial_no.py | 14 ++++- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 244f3e2f02d..c77c6c3ba95 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -7,6 +7,7 @@ import json import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname +from frappe.query_builder.functions import Coalesce from frappe.utils import ( add_days, cint, @@ -626,37 +627,37 @@ def get_pos_reserved_serial_nos(filters): def fetch_serial_numbers(filters, qty, do_not_include=None): if do_not_include is None: do_not_include = [] - batch_join_selection = "" - batch_no_condition = "" + batch_nos = filters.get("batch_no") expiry_date = filters.get("expiry_date") + serial_no = frappe.qb.DocType("Serial No") + + query = ( + frappe.qb + .from_(serial_no) + .select(serial_no.name) + .where( + (serial_no.item_code == filters["item_code"]) + & (serial_no.warehouse == filters["warehouse"]) + & (Coalesce(serial_no.sales_invoice, "") == "") + & (Coalesce(serial_no.delivery_document_no, "") == "") + ) + .orderby(serial_no.creation) + .limit(qty or 1) + ) + + if do_not_include: + query = query.where(serial_no.name.notin(do_not_include)) + if batch_nos: - batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos)) + query = query.where(serial_no.batch_no.isin(batch_nos)) if expiry_date: - batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name " - expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s " - batch_no_condition += expiry_date_cond - - excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''" - serial_numbers = frappe.db.sql(""" - SELECT sr.name FROM `tabSerial No` sr {batch_join_selection} - WHERE - sr.name not in ({excluded_sr_nos}) AND - sr.item_code = %(item_code)s AND - sr.warehouse = %(warehouse)s AND - ifnull(sr.sales_invoice,'') = '' AND - ifnull(sr.delivery_document_no, '') = '' - {batch_no_condition} - ORDER BY - sr.creation - LIMIT - {qty} - """.format( - excluded_sr_nos=excluded_sr_nos, - qty=qty or 1, - batch_join_selection=batch_join_selection, - batch_no_condition=batch_no_condition - ), filters, as_dict=1) + batch = frappe.qb.DocType("Batch") + query = (query + .left_join(batch).on(serial_no.batch_no == batch.name) + .where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date) + ) + serial_numbers = query.run(as_dict=True) return serial_numbers diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 0d362d9024e..cca63078402 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -256,14 +256,17 @@ class TestSerialNo(FrappeTestCase): in1.reload() in2.reload() + batch1 = in1.items[0].batch_no + batch2 = in2.items[0].batch_no + batch_wise_serials = { - in1.items[0].batch_no: get_serial_nos(in1.items[0].serial_no), - in2.items[0].batch_no: get_serial_nos(in2.items[0].serial_no) + batch1 : get_serial_nos(in1.items[0].serial_no), + batch2: get_serial_nos(in2.items[0].serial_no) } # Test FIFO first_fetch = auto_fetch_serial_number(5, item_code, warehouse) - self.assertEqual(first_fetch, batch_wise_serials[in1.items[0].batch_no]) + self.assertEqual(first_fetch, batch_wise_serials[batch1]) # partial FIFO partial_fetch = auto_fetch_serial_number(2, item_code, warehouse) @@ -286,3 +289,8 @@ class TestSerialNo(FrappeTestCase): all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list] fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys())) self.assertEqual(sorted(all_serials), fetched_serials) + + # expiry date + frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01") + non_expired_serials = auto_fetch_serial_number(5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1) + self.assertEqual(non_expired_serials, []) From 953afda01bc74f930d844d426d00f9f8cadea897 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 19:35:22 +0530 Subject: [PATCH 26/61] refactor(warehouse): raw query to ORM --- erpnext/stock/doctype/warehouse/warehouse.py | 22 +++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 9cfad86f142..fbca7a03943 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -41,14 +41,13 @@ class Warehouse(NestedSet): def on_trash(self): # delete bin - bins = frappe.db.sql("select * from `tabBin` where warehouse = %s", - self.name, as_dict=1) + bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name}) for d in bins: if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \ d['indented_qty'] or d['projected_qty'] or d['planned_qty']: throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code'])) else: - frappe.db.sql("delete from `tabBin` where name = %s", d['name']) + frappe.db.delete("Bin", filters={"name": d["name"]}) if self.check_if_sle_exists(): throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse.")) @@ -60,12 +59,10 @@ class Warehouse(NestedSet): self.unlink_from_items() def check_if_sle_exists(self): - return frappe.db.sql("""select name from `tabStock Ledger Entry` - where warehouse = %s limit 1""", self.name) + return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name}) def check_if_child_exists(self): - return frappe.db.sql("""select name from `tabWarehouse` - where parent_warehouse = %s limit 1""", self.name) + return frappe.db.exists("Warehouse", {"parent_warehouse": self.name}) def convert_to_group_or_ledger(self): if self.is_group: @@ -92,10 +89,7 @@ class Warehouse(NestedSet): return 1 def unlink_from_items(self): - frappe.db.sql(""" - update `tabItem Default` - set default_warehouse=NULL - where default_warehouse=%s""", self.name) + frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None) @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): @@ -169,10 +163,10 @@ def convert_to_group_or_ledger(): return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger() def get_child_warehouses(warehouse): - lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"]) + from frappe.utils.nestedset import get_descendants_of - return frappe.db.sql_list("""select name from `tabWarehouse` - where lft >= %s and rgt <= %s""", (lft, rgt)) + children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") + return children + [warehouse] # append self for backward compatibility def get_warehouses_based_on_account(account, company=None): warehouses = [] From 684d9d66d1bf98a010516f125507712a3d59b2c8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 19:53:10 +0530 Subject: [PATCH 27/61] test: warehouse conversion and treeview test --- .../stock/doctype/warehouse/test_warehouse.py | 31 +++++++++++++++++-- erpnext/stock/doctype/warehouse/warehouse.py | 7 +++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index cdb771935b0..08d7c993521 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -4,12 +4,12 @@ import frappe from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase -from frappe.utils import cint import erpnext -from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account +from erpnext.accounts.doctype.account.test_account import create_account from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children test_records = frappe.get_test_records('Warehouse') @@ -65,6 +65,33 @@ class TestWarehouse(FrappeTestCase): f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." ) + def test_group_non_group_conversion(self): + + warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion")) + + convert_to_group_or_ledger(warehouse.name) + warehouse.reload() + self.assertEqual(warehouse.is_group, 1) + + child = create_warehouse("GroupWHChild", {"parent_warehouse": warehouse.name}) + # chid exists + self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name) + frappe.delete_doc("Warehouse", child) + + convert_to_group_or_ledger(warehouse.name) + warehouse.reload() + self.assertEqual(warehouse.is_group, 0) + + make_stock_entry(item_code="_Test Item", target=warehouse.name, qty=1) + # SLE exists + self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name) + + def test_get_children(self): + company = "_Test Company" + + children = get_children("Warehouse", parent=company, company=company, is_root=True) + self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children)) + def create_warehouse(warehouse_name, properties=None, company=None): if not company: diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index fbca7a03943..e2a5b4bc4a3 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -158,9 +158,10 @@ def add_node(): frappe.get_doc(args).insert() @frappe.whitelist() -def convert_to_group_or_ledger(): - args = frappe.form_dict - return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger() +def convert_to_group_or_ledger(docname=None): + if not docname: + docname = frappe.form_dict.docname + return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger() def get_child_warehouses(warehouse): from frappe.utils.nestedset import get_descendants_of From 48595742338a5ca2cd429164a643ae70a8c63caf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 20:05:19 +0530 Subject: [PATCH 28/61] perf: Single query to delete bins instead of `N` --- erpnext/stock/doctype/warehouse/warehouse.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index e2a5b4bc4a3..4c7f41dcb5e 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -46,8 +46,6 @@ class Warehouse(NestedSet): if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \ d['indented_qty'] or d['projected_qty'] or d['planned_qty']: throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code'])) - else: - frappe.db.delete("Bin", filters={"name": d["name"]}) if self.check_if_sle_exists(): throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse.")) @@ -55,6 +53,7 @@ class Warehouse(NestedSet): if self.check_if_child_exists(): throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse.")) + frappe.db.delete("Bin", filters={"warehouse": self.name}) self.update_nsm_model() self.unlink_from_items() From 36df21067cd13eb7ffd8bc88b6eb144d016f0934 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 03:58:24 +0100 Subject: [PATCH 29/61] refactor: sales analytics --- .../report/sales_analytics/sales_analytics.js | 66 +++++++------------ 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index 6b03c7d92fe..d527e42ea4a 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -82,62 +82,42 @@ frappe.query_reports["Sales Analytics"] = { const tree_type = frappe.query_report.filters[0].value; if (data_doctype != tree_type) return; - row_name = data[2].content; - length = data.length; - - if (tree_type == "Customer") { - row_values = data - .slice(4, length - 1) - .map(function (column) { - return column.content; - }); - } else if (tree_type == "Item") { - row_values = data - .slice(5, length - 1) - .map(function (column) { - return column.content; - }); - } else { - row_values = data - .slice(3, length - 1) - .map(function (column) { - return column.content; - }); - } - - entry = { - name: row_name, - values: row_values, - }; - - let raw_data = frappe.query_report.chart.data; - let new_datasets = raw_data.datasets; - - let element_found = new_datasets.some((element, index, array)=>{ - if(element.name == row_name){ - array.splice(index, 1) - return true + const row_name = data[2].content; + const raw_data = frappe.query_report.chart.data; + const new_datasets = raw_data.datasets; + const element_found = new_datasets.some( + (element, index, array) => { + if (element.name == row_name) { + array.splice(index, 1); + return true; + } + return false; } - return false - }) + ); + const slice_at = { Customer: 4, Item: 5 }[tree_type] || 3; if (!element_found) { - new_datasets.push(entry); + new_datasets.push({ + name: row_name, + values: data + .slice(slice_at, data.length - 1) + .map(column => column.content), + }); } - let new_data = { + const new_data = { labels: raw_data.labels, datasets: new_datasets, }; - chart_options = { + + frappe.query_report.render_chart({ data: new_data, type: "line", - }; - frappe.query_report.render_chart(chart_options); + }); frappe.query_report.raw_chart_data = new_data; }, }, }); }, -} +}; From ca8d7576911c49139c556a00352aa6b5deda1d5b Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Sun, 20 Mar 2022 10:57:00 +0100 Subject: [PATCH 30/61] fix: clear "Retain Sample" and "Max Sample Quantity" in Item card if Has Batch No is uncheck (#30307) --- erpnext/stock/doctype/item/item.py | 8 ++++++++ erpnext/stock/doctype/item/test_item.py | 14 ++++++++++++++ erpnext/stock/doctype/warehouse/test_records.json | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8ede95539b3..3abeecd7424 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -107,6 +107,7 @@ class Item(Document): self.validate_variant_attributes() self.validate_variant_based_on_change() self.validate_fixed_asset() + self.clear_retain_sample() self.validate_retain_sample() self.validate_uom_conversion_factor() self.validate_customer_provided_part() @@ -209,6 +210,13 @@ class Item(Document): frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( self.item_code)) + def clear_retain_sample(self): + if not self.has_batch_no: + self.retain_sample = None + + if not self.retain_sample: + self.sample_quantity = None + def add_default_uom_in_conversion_factor_table(self): if not self.is_new() and self.has_value_changed("stock_uom"): self.uoms = [] diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 669cabc9013..c12200b750b 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -656,6 +656,20 @@ class TestItem(FrappeTestCase): make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5)) self.consume_item_code_with_differet_stock_transactions(item_code=item.name) + @change_settings("Stock Settings", {"sample_retention_warehouse": 0}) + def test_retain_sample(self): + frappe.db.set_single_value('Stock Settings', 'sample_retention_warehouse', '_Test Retain Sample Warehouse') + item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1}) + + self.assertEqual(item.has_batch_no, 1) + self.assertEqual(item.retain_sample, 1) + self.assertEqual(item.sample_quantity, 1) + + item.has_batch_no = None + item.save() + self.assertEqual(item.retain_sample, None) + self.assertEqual(item.sample_quantity, None) + item.delete() def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice diff --git a/erpnext/stock/doctype/warehouse/test_records.json b/erpnext/stock/doctype/warehouse/test_records.json index e128558ed3b..1b1e9058508 100644 --- a/erpnext/stock/doctype/warehouse/test_records.json +++ b/erpnext/stock/doctype/warehouse/test_records.json @@ -60,5 +60,12 @@ "warehouse_name": "_Test Warehouse Group-C2", "is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC" + }, + { + "company": "_Test Company", + "doctype": "Warehouse", + "warehouse_name": "_Test Retain Sample Warehouse", + "parent_warehouse": "_Test Warehouse Group - _TC", + "is_group": 0 } ] From c2aad115c19338998be81b4c47f1cd2f695b96cf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 20 Mar 2022 19:26:34 +0530 Subject: [PATCH 31/61] fix: disable deferred naming on SLE/GLE if hash method is used. (#30286) * fix: dont rename GLE/SLE that dont have naming series * test: tests for deferred naming of ledgers --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 + .../stock_ledger_entry/stock_ledger_entry.py | 2 + .../test_stock_ledger_entry.py | 61 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 9d1452b1b38..192099b0d19 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -32,6 +32,8 @@ class GLEntry(Document): name will be changed using autoname options (in a scheduled job) """ self.name = frappe.generate_hash(txt="", length=10) + if self.meta.autoname == "hash": + self.to_rename = 0 def validate(self): self.flags.ignore_submit_comment = True diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index c53830799d4..2f593041bf2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -26,6 +26,8 @@ class StockLedgerEntry(Document): name will be changed using autoname options (in a scheduled job) """ self.name = frappe.generate_hash(txt="", length=10) + if self.meta.autoname == "hash": + self.to_rename = 0 def validate(self): self.flags.ignore_submit_comment = True diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 2352235cb3d..fc579958be3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -7,9 +7,11 @@ from uuid import uuid4 import frappe from frappe.core.page.permission_manager.permission_manager import reset +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, today +from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( @@ -939,3 +941,62 @@ def get_unique_suffix(): # Used to isolate valuation sensitive # tests to prevent future tests from failing. return str(uuid4())[:8].upper() + + +class TestDeferredNaming(FrappeTestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.gle_autoname = frappe.get_meta("GL Entry").autoname + cls.sle_autoname = frappe.get_meta("Stock Ledger Entry").autoname + + def setUp(self) -> None: + self.item = make_item().name + self.warehouse = "Stores - TCP1" + self.company = "_Test Company with perpetual inventory" + + def tearDown(self) -> None: + make_property_setter(doctype="GL Entry", for_doctype=True, + property="autoname", value=self.gle_autoname, property_type="Data", fieldname=None) + make_property_setter(doctype="Stock Ledger Entry", for_doctype=True, + property="autoname", value=self.sle_autoname, property_type="Data", fieldname=None) + + # since deferred naming autocommits, commit all changes to avoid flake + frappe.db.commit() # nosemgrep + + @staticmethod + def get_gle_sles(se): + filters = {"voucher_type": se.doctype, "voucher_no": se.name} + gle = set(frappe.get_list("GL Entry", filters, pluck="name")) + sle = set(frappe.get_list("Stock Ledger Entry", filters, pluck="name")) + return gle, sle + + def test_deferred_naming(self): + se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse, + qty=10, rate=100, company=self.company) + + gle, sle = self.get_gle_sles(se) + rename_gle_sle_docs() + renamed_gle, renamed_sle = self.get_gle_sles(se) + + self.assertFalse(gle & renamed_gle, msg="GLEs not renamed") + self.assertFalse(sle & renamed_sle, msg="SLEs not renamed") + se.cancel() + + def test_hash_naming(self): + # disable naming series + for doctype in ("GL Entry", "Stock Ledger Entry"): + make_property_setter(doctype=doctype, for_doctype=True, + property="autoname", value="hash", property_type="Data", fieldname=None) + + se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse, + qty=10, rate=100, company=self.company) + + gle, sle = self.get_gle_sles(se) + rename_gle_sle_docs() + renamed_gle, renamed_sle = self.get_gle_sles(se) + + self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming") + self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming") + se.cancel() From 0a015b7f70039d8f0aeeb5aad2473d0d8846b0db Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 14:59:41 +0100 Subject: [PATCH 32/61] refactor: remove redundant if-statement (#30311) --- .../production_plan/production_plan.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 48cd753d751..2b6e6968bd3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1018,21 +1018,21 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): required_qty = item.get("quantity") # get available material by transferring to production warehouse for d in locations: - if required_qty <=0: return + if required_qty <= 0: + return new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") - if required_qty > 0: - new_dict.update({ - "quantity": quantity, - "material_request_type": "Material Transfer", - "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM - "from_warehouse": d.get("warehouse") - }) + new_dict.update({ + "quantity": quantity, + "material_request_type": "Material Transfer", + "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM + "from_warehouse": d.get("warehouse") + }) - required_qty -= quantity - new_mr_items.append(new_dict) + required_qty -= quantity + new_mr_items.append(new_dict) # raise purchase request for remaining qty if required_qty: From 2ff67902833283e120237435be3d53f8da8df2c4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Mar 2022 09:42:28 +0530 Subject: [PATCH 33/61] fix: P&L account validation on cancellation --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 9d1452b1b38..b2ba485ab89 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -134,7 +134,7 @@ class GLEntry(Document): def check_pl_account(self): if self.is_opening=='Yes' and \ - frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss": + frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and not self.is_cancelled: frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") .format(self.voucher_type, self.voucher_no, self.account)) From 3a7776ea7f01e36f3380dae1abab3b3a5ef4ea83 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Mar 2022 12:04:18 +0530 Subject: [PATCH 34/61] fix: Contribution amount against invoices in Sales Person Dashboard --- .../setup/doctype/sales_person/sales_person.js | 8 ++++++-- .../setup/doctype/sales_person/sales_person.py | 15 +++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index b71a92f8a98..d86a8f3d984 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -4,8 +4,12 @@ frappe.ui.form.on('Sales Person', { refresh: function(frm) { if(frm.doc.__onload && frm.doc.__onload.dashboard_info) { - var info = frm.doc.__onload.dashboard_info; - frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue'); + let info = frm.doc.__onload.dashboard_info; + frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}', + [format_currency(info.allocated_amount_against_order, info.currency)]), 'blue'); + + frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}', + [format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue'); } }, diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index b79a566578d..6af1b312bdd 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -28,14 +28,17 @@ class SalesPerson(NestedSet): def load_dashboard_info(self): company_default_currency = get_default_currency() - allocated_amount = frappe.db.sql(""" - select sum(allocated_amount) - from `tabSales Team` - where sales_person = %s and docstatus=1 and parenttype = 'Sales Order' - """,(self.sales_person_name)) + allocated_amount_against_order = flt(frappe.db.get_value('Sales Team', + {'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name}, + 'sum(allocated_amount)')) + + allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team', + {'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name}, + 'sum(allocated_amount)')) info = {} - info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0 + info["allocated_amount_against_order"] = allocated_amount_against_order + info["allocated_amount_against_invoice"] = allocated_amount_against_invoice info["currency"] = company_default_currency self.set_onload('dashboard_info', info) From 95568210b27d21a36d29e226fa8014cf40542d26 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 21 Mar 2022 15:30:04 +0530 Subject: [PATCH 36/61] fix(ux): warning for disabled carry forwarding in Policy Assignment (#30331) --- .../leave_policy_assignment.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index c11a821738f..ae5ac7b689e 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,13 +8,14 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document -from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate +from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate class LeavePolicyAssignment(Document): def validate(self): - self.validate_policy_assignment_overlap() self.set_dates() + self.validate_policy_assignment_overlap() + self.warn_about_carry_forwarding() def on_submit(self): self.grant_leave_alloc_for_employee() @@ -38,6 +39,20 @@ class LeavePolicyAssignment(Document): frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + def warn_about_carry_forwarding(self): + if not self.carry_forward: + return + + leave_types = get_leave_type_details() + leave_policy = frappe.get_doc("Leave Policy", self.leave_policy) + + for policy in leave_policy.leave_policy_details: + leave_type = leave_types.get(policy.leave_type) + if not leave_type.is_carry_forward: + msg = _("Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled.").format( + frappe.bold(get_link_to_form("Leave Type", leave_type.name))) + frappe.msgprint(msg, indicator="orange", alert=True) + @frappe.whitelist() def grant_leave_alloc_for_employee(self): if self.leaves_allocated: From d3be84bfe883a53eb9ca571aed862a650bf865e3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Mar 2022 15:40:34 +0530 Subject: [PATCH 37/61] test: correct use of test decorator --- erpnext/stock/doctype/item/test_item.py | 3 +-- erpnext/stock/doctype/warehouse/test_records.json | 7 ------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index c12200b750b..112420ff7b7 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -656,9 +656,8 @@ class TestItem(FrappeTestCase): make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5)) self.consume_item_code_with_differet_stock_transactions(item_code=item.name) - @change_settings("Stock Settings", {"sample_retention_warehouse": 0}) + @change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"}) def test_retain_sample(self): - frappe.db.set_single_value('Stock Settings', 'sample_retention_warehouse', '_Test Retain Sample Warehouse') item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1}) self.assertEqual(item.has_batch_no, 1) diff --git a/erpnext/stock/doctype/warehouse/test_records.json b/erpnext/stock/doctype/warehouse/test_records.json index 1b1e9058508..e128558ed3b 100644 --- a/erpnext/stock/doctype/warehouse/test_records.json +++ b/erpnext/stock/doctype/warehouse/test_records.json @@ -60,12 +60,5 @@ "warehouse_name": "_Test Warehouse Group-C2", "is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC" - }, - { - "company": "_Test Company", - "doctype": "Warehouse", - "warehouse_name": "_Test Retain Sample Warehouse", - "parent_warehouse": "_Test Warehouse Group - _TC", - "is_group": 0 } ] From 363e676a35da335f5c764b510b18d48280000824 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Mar 2022 16:28:17 +0530 Subject: [PATCH 38/61] fix: Reset GST State number --- erpnext/regional/india/utils.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index d443f9c15c0..55b563e1cd9 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -19,8 +19,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") def validate_gstin_for_india(doc, method): - if hasattr(doc, 'gst_state') and doc.gst_state: - doc.gst_state_number = state_numbers[doc.gst_state] + if hasattr(doc, 'gst_state'): + set_gst_state_and_state_number(doc) + if not hasattr(doc, 'gstin') or not doc.gstin: return @@ -50,7 +51,6 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN")) validate_gstin_check_digit(doc.gstin) - set_gst_state_and_state_number(doc) if not doc.gst_state: frappe.throw(_("Please enter GST state"), title=_("Invalid State")) @@ -82,17 +82,14 @@ def update_gst_category(doc, method): frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') def set_gst_state_and_state_number(doc): - if not doc.gst_state: - if not doc.state: - return + if not doc.gst_state and doc.state: state = doc.state.lower() states_lowercase = {s.lower():s for s in states} if state in states_lowercase: doc.gst_state = states_lowercase[state] else: return - - doc.gst_state_number = state_numbers[doc.gst_state] + doc.gst_state_number = state_numbers.get(doc.gst_state) def validate_gstin_check_digit(gstin, label='GSTIN'): ''' Function to validate the check digit of the GSTIN.''' From 01fd3adedf289aa18947310f06a1b6b38ba7ecd6 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Mon, 21 Mar 2022 08:27:27 -0300 Subject: [PATCH 39/61] fix: report sales payments summary (#30264) --- .../sales_payment_summary.py | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py index 3b736282cf9..d5483b5f852 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py @@ -199,31 +199,39 @@ def get_mode_of_payment_details(filters): invoice_list = get_invoices(filters) invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) if invoice_list: - inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date, - ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount - from `tabSales Invoice` a, `tabSales Invoice Payment` b - where a.name = b.parent - and a.docstatus = 1 - and a.name in ({invoice_list_names}) - group by a.owner, a.posting_date, mode_of_payment - union - select a.owner,a.posting_date, - ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_paid_amount) as paid_amount - from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c - where a.name = c.reference_name - and b.name = c.parent - and b.docstatus = 1 - and a.name in ({invoice_list_names}) - group by a.owner, a.posting_date, mode_of_payment - union - select a.owner, a.posting_date, - ifnull(a.voucher_type,'') as mode_of_payment, sum(b.credit) - from `tabJournal Entry` a, `tabJournal Entry Account` b - where a.name = b.parent - and a.docstatus = 1 - and b.reference_type = "Sales Invoice" - and b.reference_name in ({invoice_list_names}) - group by a.owner, a.posting_date, mode_of_payment + inv_mop_detail = frappe.db.sql(""" + select t.owner, + t.posting_date, + t.mode_of_payment, + sum(t.paid_amount) as paid_amount + from ( + select a.owner, a.posting_date, + ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount + from `tabSales Invoice` a, `tabSales Invoice Payment` b + where a.name = b.parent + and a.docstatus = 1 + and a.name in ({invoice_list_names}) + group by a.owner, a.posting_date, mode_of_payment + union + select a.owner,a.posting_date, + ifnull(b.mode_of_payment, '') as mode_of_payment, sum(c.allocated_amount) as paid_amount + from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c + where a.name = c.reference_name + and b.name = c.parent + and b.docstatus = 1 + and a.name in ({invoice_list_names}) + group by a.owner, a.posting_date, mode_of_payment + union + select a.owner, a.posting_date, + ifnull(a.voucher_type,'') as mode_of_payment, sum(b.credit) + from `tabJournal Entry` a, `tabJournal Entry Account` b + where a.name = b.parent + and a.docstatus = 1 + and b.reference_type = "Sales Invoice" + and b.reference_name in ({invoice_list_names}) + group by a.owner, a.posting_date, mode_of_payment + ) t + group by t.owner, t.posting_date, t.mode_of_payment """.format(invoice_list_names=invoice_list_names), as_dict=1) inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date, @@ -231,7 +239,7 @@ def get_mode_of_payment_details(filters): from `tabSales Invoice` a, `tabSales Invoice Payment` b where a.name = b.parent and a.name in ({invoice_list_names}) - and b.mode_of_payment = 'Cash' + and b.type = 'Cash' and a.base_change_amount > 0 group by a.owner, a.posting_date, mode_of_payment""".format(invoice_list_names=invoice_list_names), as_dict=1) @@ -244,4 +252,4 @@ def get_mode_of_payment_details(filters): for d in inv_mop_detail: mode_of_payment_details.setdefault(d["owner"]+cstr(d["posting_date"]), []).append((d.mode_of_payment,d.paid_amount)) - return mode_of_payment_details + return mode_of_payment_details \ No newline at end of file From f6e64c2cacf097db4174e4a3e0d3728af8082b94 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Mar 2022 17:53:18 +0530 Subject: [PATCH 40/61] fix: Product Filters Lookup - bind the right classes to the filter lookup field - make class names more descriptive - make filter lookup field more visible with white bg and border - bind lookup input field js in `views.js` - make filter lookup field functioning for atribute filters too - added placeholder to lookup field --- erpnext/e_commerce/product_ui/views.js | 16 ++++++++++++++++ erpnext/public/scss/shopping_cart.scss | 9 +++++++++ erpnext/templates/generators/item_group.html | 18 ------------------ erpnext/templates/includes/macros.html | 12 ++++++------ erpnext/www/all-products/index.html | 18 ------------------ 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js index 6dce79dd72b..fb63b21a083 100644 --- a/erpnext/e_commerce/product_ui/views.js +++ b/erpnext/e_commerce/product_ui/views.js @@ -418,6 +418,22 @@ erpnext.ProductView = class { me.change_route_with_filters(); }); + + // bind filter lookup input box + $('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => { + const $input = $(e.target); + const keyword = ($input.val() || '').toLowerCase(); + const $filter_options = $input.next('.filter-options'); + + $filter_options.find('.filter-lookup-wrapper').show(); + $filter_options.find('.filter-lookup-wrapper').each((i, el) => { + const $el = $(el); + const value = $el.data('value').toLowerCase(); + if (!value.includes(keyword)) { + $el.hide(); + } + }); + }, 300)); } change_route_with_filters() { diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 019496d295a..6ae464d2c21 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -264,6 +264,15 @@ body.product-page { font-size: 13px; } + .filter-lookup-input { + background-color: white; + border: 1px solid var(--gray-300); + + &:focus { + border: 1px solid var(--primary); + } + } + .filter-label { font-size: 11px; font-weight: 600; diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index e099cdde6aa..956c3c51e6e 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -52,24 +52,6 @@ - diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 47413077371..fb4cecf8266 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -300,13 +300,13 @@ {% if values | len > 20 %} - + {% endif %} {% if values %}
{% for value in values %} -
+