From 9f2d325e67d3517480ff1580bd28e86f4ff4fc45 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 14 Jun 2022 17:20:44 +0530 Subject: [PATCH 01/15] fix: Pick Template BOM if variant BOM absent in WO popup from SO - Use `get_default_bom` in sales_order.py (reduce duplicate utility functions) - Remove redundant if else in `get_work_order_items` - `get_default_bom`: If no BOM and template exists try to fetch template BOM - test: `get_work_order_items` via SO and if right BOM is picked --- .../doctype/sales_order/sales_order.py | 48 ++++++----------- .../doctype/sales_order/test_sales_order.py | 53 +++++++++++++++++++ erpnext/stock/get_item_details.py | 20 +++++-- 3 files changed, 83 insertions(+), 38 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 7522e92a8a..8c03cb5b41 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -25,6 +25,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import get_default_bom from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -423,8 +424,9 @@ class SalesOrder(SellingController): for table in [self.items, self.packed_items]: for i in table: - bom = get_default_bom_item(i.item_code) + bom = get_default_bom(i.item_code) stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty + if not for_raw_material_request: total_work_order_qty = flt( frappe.db.sql( @@ -438,32 +440,19 @@ class SalesOrder(SellingController): pending_qty = stock_qty if pending_qty and i.item_code not in product_bundle_parents: - if bom: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom=bom, - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - sales_order_item=i.name, - ) - ) - else: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom="", - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - sales_order_item=i.name, - ) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom or "", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, ) + ) + return items def on_recurring(self, reference_doc, auto_repeat_doc): @@ -1167,13 +1156,6 @@ def update_status(status, name): so.update_status(status) -def get_default_bom_item(item_code): - bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc") - bom = bom[0].name if bom else None - - return bom - - @frappe.whitelist() def make_raw_material_request(items, company, sales_order, project=None): if not frappe.has_permission("Sales Order", "write"): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 96308f0bee..dfb8e0b447 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1380,6 +1380,59 @@ class TestSalesOrder(FrappeTestCase): except Exception: self.fail("Can not cancel sales order with linked cancelled payment entry") + def test_work_order_pop_up_from_sales_order(self): + "Test `get_work_order_items` in Sales Order picks the right BOM for items to manufacture." + + from erpnext.controllers.item_variant import create_variant + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + make_item( # template item + "Test-WO-Tshirt", + { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [{"attribute": "Test Colour"}], + }, + ) + make_item("Test-RM-Cotton") # RM for BOM + + for colour in ( + "Red", + "Green", + ): + variant = create_variant("Test-WO-Tshirt", {"Test Colour": colour}) + variant.save() + + template_bom = make_bom(item="Test-WO-Tshirt", rate=100, raw_materials=["Test-RM-Cotton"]) + red_var_bom = make_bom(item="Test-WO-Tshirt-R", rate=100, raw_materials=["Test-RM-Cotton"]) + + so = make_sales_order( + **{ + "item_list": [ + { + "item_code": "Test-WO-Tshirt-R", + "qty": 1, + "rate": 1000, + "warehouse": "_Test Warehouse - _TC", + }, + { + "item_code": "Test-WO-Tshirt-G", + "qty": 1, + "rate": 1000, + "warehouse": "_Test Warehouse - _TC", + }, + ] + } + ) + wo_items = so.get_work_order_items() + + self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R") + self.assertEqual(wo_items[0].get("bom"), red_var_bom.name) + + # Must pick Template Item BOM for Test-WO-Tshirt-G as it has no BOM + self.assertEqual(wo_items[1].get("item_code"), "Test-WO-Tshirt-G") + self.assertEqual(wo_items[1].get("bom"), template_bom.name) + def test_request_for_raw_materials(self): item = make_item( "_Test Finished Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c8d9f5404f..3776a27b35 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1352,12 +1352,22 @@ def get_price_list_currency_and_exchange_rate(args): @frappe.whitelist() def get_default_bom(item_code=None): - if item_code: - bom = frappe.db.get_value( - "BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} + def _get_bom(item): + bom = frappe.get_all( + "BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1 ) - if bom: - return bom + return bom[0].name if bom else None + + if not item_code: + return + + bom_name = _get_bom(item_code) + + template_item = frappe.db.get_value("Item", item_code, "variant_of") + if not bom_name and template_item: + bom_name = _get_bom(template_item) + + return bom_name @frappe.whitelist() From 2a9105f26f4720a486ae54e7e777123e3fd345a8 Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 15 Jun 2022 00:54:24 -0500 Subject: [PATCH 02/15] refactor: DB independent capitalization of test cases (#31359) --- .../employee_advance/test_employee_advance.py | 2 +- .../test_landed_cost_voucher.py | 6 +++--- .../purchase_receipt/test_purchase_receipt.py | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 44d68c9483..81a0876a2b 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -216,7 +216,7 @@ def make_payment_entry(advance): def make_employee_advance(employee_name, args=None): doc = frappe.new_doc("Employee Advance") doc.employee = employee_name - doc.company = "_Test company" + doc.company = "_Test Company" doc.purpose = "For site visit" doc.currency = erpnext.get_company_currency("_Test company") doc.exchange_rate = 1 diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 1af9953451..1ba801134e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -24,7 +24,7 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", get_multiple_items=True, get_taxes_and_charges=True, ) @@ -195,7 +195,7 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", get_multiple_items=True, get_taxes_and_charges=True, do_not_submit=True, @@ -280,7 +280,7 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", do_not_save=True, ) pr.items[0].cost_center = "Main - TCP1" diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7fbfa62939..be4f27465e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -276,7 +276,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", get_multiple_items=True, get_taxes_and_charges=True, ) @@ -486,13 +486,13 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, @@ -573,13 +573,13 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, @@ -615,7 +615,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", qty=2, rejected_qty=2, rejected_warehouse=rejected_warehouse, @@ -624,7 +624,7 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, @@ -951,7 +951,7 @@ class TestPurchaseReceipt(FrappeTestCase): cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", ) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) @@ -975,7 +975,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", ) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) From 37e9622426a3996eb8e09da82bd9eb05575c22fb Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <99652762+nihantra@users.noreply.github.com> Date: Wed, 15 Jun 2022 12:02:57 +0530 Subject: [PATCH 03/15] fix: Spelling mistake in quotation depend on (#31362) Update quotation.json --- erpnext/selling/doctype/quotation/quotation.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 5dfd8f2965..bb2f95dd17 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -296,7 +296,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", + "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name", "fieldname": "col_break98", "fieldtype": "Column Break", "width": "50%" @@ -316,7 +316,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", + "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name", "fieldname": "customer_group", "fieldtype": "Link", "hidden": 1, @@ -1084,4 +1084,4 @@ "states": [], "timeline_field": "party_name", "title_field": "title" -} \ No newline at end of file +} From b8f728a40aaea8b7e86b78b2e8a8cbecb8cb8775 Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 15 Jun 2022 01:37:33 -0500 Subject: [PATCH 04/15] refactor: use CURRENT_DATE instead of CURDATE() (#31356) * refactor: use CURRENT_DATE instead of CURDATE() * style: reformat to black spec * refactor: use QB for auto_close queries Co-authored-by: Ankush Menat --- .../doctype/payment_entry/payment_entry.py | 2 +- .../inactive_sales_items.py | 2 +- .../purchase_order/test_purchase_order.py | 2 +- erpnext/controllers/queries.py | 2 +- .../crm/doctype/opportunity/opportunity.py | 19 +++++++++++-------- .../doctype/project_update/project_update.py | 4 ++-- .../doctype/sales_order/test_sales_order.py | 2 +- .../inactive_customers/inactive_customers.py | 4 ++-- .../sales_order_analysis.py | 2 +- .../doctype/email_digest/email_digest.py | 4 ++-- erpnext/stock/doctype/batch/batch.py | 2 +- erpnext/support/doctype/issue/issue.py | 18 +++++++++++------- 12 files changed, 35 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a10a810d1d..f7a57bb96e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1444,7 +1444,7 @@ def get_negative_outstanding_invoices( voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" supplier_condition = "" if voucher_type == "Purchase Invoice": - supplier_condition = "and (release_date is null or release_date <= CURDATE())" + supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)" if party_account_currency == company_currency: grand_total_field = "base_grand_total" rounded_total_field = "base_rounded_total" diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index 1a003993aa..230b18c293 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -100,7 +100,7 @@ def get_sales_details(filters): sales_data = frappe.db.sql( """ select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date, - DATEDIFF(CURDATE(), {date_field}) as days_since_last_order + DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order from `tab{doctype}` s, `tab{doctype} Item` si where s.name = si.parent and s.docstatus = 1 order by days_since_last_order """.format( # nosec diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 1a7f2dd5d9..d732b755fe 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -330,7 +330,7 @@ class TestPurchaseOrder(FrappeTestCase): else: # update valid from frappe.db.sql( - """UPDATE `tabItem Tax` set valid_from = CURDATE() + """UPDATE `tabItem Tax` set valid_from = CURRENT_DATE where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}, ) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 1497b182e5..a725f674c9 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -691,7 +691,7 @@ def get_doctype_wise_filters(filters): def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): query = """select batch_id from `tabBatch` where disabled = 0 - and (expiry_date >= CURDATE() or expiry_date IS NULL) + and (expiry_date >= CURRENT_DATE or expiry_date IS NULL) and name like {txt}""".format( txt=frappe.db.escape("%{0}%".format(txt)) ) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index b590177562..c70a4f61b8 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -8,7 +8,8 @@ import frappe from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc -from frappe.query_builder import DocType +from frappe.query_builder import DocType, Interval +from frappe.query_builder.functions import Now from frappe.utils import cint, flt, get_fullname from erpnext.crm.utils import add_link_in_communication, copy_comments @@ -398,15 +399,17 @@ def auto_close_opportunity(): frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 ) - opportunities = frappe.db.sql( - """ select name from tabOpportunity where status='Replied' and - modifiedProject Name: " diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 96308f0bee..9e5d40b5a8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -644,7 +644,7 @@ class TestSalesOrder(FrappeTestCase): else: # update valid from frappe.db.sql( - """UPDATE `tabItem Tax` set valid_from = CURDATE() + """UPDATE `tabItem Tax` set valid_from = CURRENT_DATE where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}, ) diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index 1b337fc495..a166085327 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -31,13 +31,13 @@ def execute(filters=None): def get_sales_details(doctype): cond = """sum(so.base_net_total) as 'total_order_considered', max(so.posting_date) as 'last_order_date', - DATEDIFF(CURDATE(), max(so.posting_date)) as 'days_since_last_order' """ + DATEDIFF(CURRENT_DATE, max(so.posting_date)) as 'days_since_last_order' """ if doctype == "Sales Order": cond = """sum(if(so.status = "Stopped", so.base_net_total * so.per_delivered/100, so.base_net_total)) as 'total_order_considered', max(so.transaction_date) as 'last_order_date', - DATEDIFF(CURDATE(), max(so.transaction_date)) as 'days_since_last_order'""" + DATEDIFF(CURRENT_DATE, max(so.transaction_date)) as 'days_since_last_order'""" return frappe.db.sql( """select diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index cc61594af4..720aa41982 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -64,7 +64,7 @@ def get_data(conditions, filters): soi.delivery_date as delivery_date, so.name as sales_order, so.status, so.customer, soi.item_code, - DATEDIFF(CURDATE(), soi.delivery_date) as delay_days, + DATEDIFF(CURRENT_DATE, soi.delivery_date) as delay_days, IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index cdfea7764f..42ba6ce394 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -854,7 +854,7 @@ class EmailDigest(Document): sql_po = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent - where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date + where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.parent DESC, `tabPurchase Order Item`.schedule_date DESC""".format( fields=fields_po @@ -862,7 +862,7 @@ class EmailDigest(Document): sql_poi = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent - where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date + where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.idx""".format( fields=fields_poi ) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 559883f224..52854a0f01 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -335,7 +335,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s and `tabStock Ledger Entry`.is_cancelled = 0 - and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} + and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC """.format( diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 08a06b19b4..7f3e0cf4c2 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -11,6 +11,8 @@ from frappe.core.utils import get_parent_doc from frappe.email.inbox import link_communication_to_document from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Interval +from frappe.query_builder.functions import Now from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds from frappe.utils.user import is_website_user @@ -190,15 +192,17 @@ def auto_close_tickets(): frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 ) - issues = frappe.db.sql( - """ select name from tabIssue where status='Replied' and - modified Date: Wed, 15 Jun 2022 13:17:06 +0530 Subject: [PATCH 05/15] chore: add gl to payment ledger migarion to patches --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5a984635fd..5b59161609 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -374,3 +374,4 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.set_payroll_entry_status erpnext.patches.v13_0.job_card_status_on_hold +erpnext.patches.v14_0.migrate_gl_to_payment_ledger From 94ad66e55b9741d2a793fecb7c8dfad2ac971c41 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 15 Jun 2022 13:35:42 +0530 Subject: [PATCH 06/15] chore: revert naming to default (#31364) --- .../doctype/bom_update_batch/bom_update_batch.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json index 83b54d326c..b867d2aa5d 100644 --- a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json +++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "autoincrement", + "autoname": "hash", "creation": "2022-05-31 17:34:39.825537", "doctype": "DocType", "engine": "InnoDB", @@ -46,10 +46,9 @@ "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Batch", - "naming_rule": "Autoincrement", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} From 276267d5a6dbb0a183ef8eb9612476c245bd637a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 15 Jun 2022 15:26:05 +0530 Subject: [PATCH 07/15] fix: remove agriculture module from patch (#31369) --- erpnext/patches.txt | 2 +- erpnext/patches/v14_0/delete_agriculture_doctypes.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5b59161609..318875d2a4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,7 +339,7 @@ erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_healthcare_doctypes erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 -erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v14_0.delete_agriculture_doctypes # 15-06-2022 erpnext.patches.v14_0.delete_education_doctypes erpnext.patches.v14_0.delete_datev_doctypes erpnext.patches.v14_0.rearrange_company_fields diff --git a/erpnext/patches/v14_0/delete_agriculture_doctypes.py b/erpnext/patches/v14_0/delete_agriculture_doctypes.py index e0b12a2579..8ec0c33090 100644 --- a/erpnext/patches/v14_0/delete_agriculture_doctypes.py +++ b/erpnext/patches/v14_0/delete_agriculture_doctypes.py @@ -2,6 +2,9 @@ import frappe def execute(): + if "agriculture" in frappe.get_installed_apps(): + return + frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True) frappe.delete_doc("Workspace", "Agriculture", ignore_missing=True, force=True) @@ -19,3 +22,5 @@ def execute(): doctypes = frappe.get_all("DocType", {"module": "agriculture", "custom": 0}, pluck="name") for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) + + frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True) From c0f9b34ede50c8df4c634abf2b58a37d8e2ffdbb Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 15 Jun 2022 16:08:05 +0530 Subject: [PATCH 08/15] fix(minor): move variants to separate tab (#31354) * fix(minor): move variants to separate tab * fix(minor): variants tab * fix(minor): add counts Co-authored-by: Deepesh Garg Co-authored-by: Ankush Menat --- .../module_onboarding/accounts/accounts.json | 2 +- .../setup_taxes/setup_taxes.json | 4 +- .../manufacturing/manufacturing.json | 64 +++++++------------ erpnext/stock/doctype/item/item.json | 13 ++-- 4 files changed, 34 insertions(+), 49 deletions(-) diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json index b9040e3309..9916d1622d 100644 --- a/erpnext/accounts/module_onboarding/accounts/accounts.json +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -13,7 +13,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", "idx": 0, "is_complete": 0, - "modified": "2022-06-07 14:29:21.352132", + "modified": "2022-06-14 17:38:24.967834", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts", diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json index b6e9f5cd87..e323f6cb1a 100644 --- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -2,14 +2,14 @@ "action": "Create Entry", "action_label": "Manage Sales Tax Templates", "creation": "2020-05-13 19:29:43.844463", - "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n", + "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2022-06-07 14:27:15.906286", + "modified": "2022-06-14 17:37:56.694261", "modified_by": "Administrator", "name": "Setup Taxes", "owner": "Administrator", diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 9829a96e09..549f5afc70 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "docstatus": 0, "doctype": "Workspace", @@ -402,7 +402,7 @@ "type": "Link" } ], - "modified": "2022-05-31 22:08:19.408223", + "modified": "2022-06-15 15:18:57.062935", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -415,39 +415,35 @@ "sequence_id": 17.0, "shortcuts": [ { - "color": "Green", - "format": "{} Active", - "label": "Item", - "link_to": "Item", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{\n \"disabled\": 0\n}", - "type": "DocType" - }, - { - "color": "Green", - "format": "{} Active", + "color": "Grey", + "doc_view": "List", "label": "BOM", "link_to": "BOM", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{\n \"is_active\": 1\n}", + "stats_filter": "{\"is_active\":[\"=\",1]}", "type": "DocType" }, { - "color": "Yellow", - "format": "{} Open", - "label": "Work Order", - "link_to": "Work Order", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}", - "type": "DocType" - }, - { - "color": "Yellow", - "format": "{} Open", + "color": "Grey", + "doc_view": "List", "label": "Production Plan", "link_to": "Production Plan", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}", + "stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}", + "type": "DocType" + }, + { + "color": "Grey", + "doc_view": "List", + "label": "Work Order", + "link_to": "Work Order", + "stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}", + "type": "DocType" + }, + { + "color": "Grey", + "doc_view": "List", + "label": "Job Card", + "link_to": "Job Card", + "stats_filter": "{\"status\":[\"not in\",[\"Cancelled\",\"Completed\",null]]}", "type": "DocType" }, { @@ -455,12 +451,6 @@ "link_to": "Exponential Smoothing Forecasting", "type": "Report" }, - { - "label": "Work Order Summary", - "link_to": "Work Order Summary", - "restrict_to_domain": "Manufacturing", - "type": "Report" - }, { "label": "BOM Stock Report", "link_to": "BOM Stock Report", @@ -470,12 +460,6 @@ "label": "Production Planning Report", "link_to": "Production Planning Report", "type": "Report" - }, - { - "label": "Dashboard", - "link_to": "Manufacturing", - "restrict_to_domain": "Manufacturing", - "type": "Dashboard" } ], "title": "Manufacturing" diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 2f6d4fb783..76cb31dc42 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -14,7 +14,6 @@ "details", "naming_series", "item_code", - "variant_of", "item_name", "item_group", "stock_uom", @@ -22,6 +21,7 @@ "disabled", "allow_alternative_item", "is_stock_item", + "has_variants", "include_item_in_manufacturing", "opening_stock", "valuation_rate", @@ -66,7 +66,7 @@ "has_serial_no", "serial_no_series", "variants_section", - "has_variants", + "variant_of", "variant_based_on", "attributes", "accounting", @@ -112,8 +112,8 @@ "quality_inspection_template", "inspection_required_before_delivery", "manufacturing", - "default_bom", "is_sub_contracted_item", + "default_bom", "column_break_74", "customer_code", "default_item_manufacturer", @@ -479,7 +479,7 @@ "collapsible_depends_on": "attributes", "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "variants_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Variants" }, { @@ -504,7 +504,8 @@ "fieldname": "attributes", "fieldtype": "Table", "hidden": 1, - "label": "Attributes", + "label": "Variant Attributes", + "mandatory_depends_on": "has_variants", "options": "Item Variant Attribute" }, { @@ -909,7 +910,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-08 11:35:20.094546", + "modified": "2022-06-15 09:02:06.177691", "modified_by": "Administrator", "module": "Stock", "name": "Item", From d9c6b7218a11025f9ca61c000fc8d0f9e9eaf60b Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 15 Jun 2022 18:57:39 +0530 Subject: [PATCH 09/15] chore: Sponsor credit for BOM Update Tool perf --- sponsors.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sponsors.md b/sponsors.md index 125b3588e2..57adc8dad4 100644 --- a/sponsors.md +++ b/sponsors.md @@ -61,5 +61,13 @@ Bulk edit via export-import in Bank Reconciliation #4356 + + + Sapcon Instruments Pvt Ltd + + + Level wise BOM Cost Updation and Performance Enhancement #31072 + + From 5c6f22f27553bc53950c59f64947ae5da908d667 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 15 Jun 2022 19:30:26 +0530 Subject: [PATCH 10/15] refactor: simpler batching for GLE reposting (#31374) * refactor: simpler batching for GLE reposting * test: add "actual" test for chunked GLE reposting --- erpnext/accounts/utils.py | 19 ++++--- .../repost_item_valuation.py | 1 + .../test_repost_item_valuation.py | 51 ++++++++++++++++++- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2d86dead2f..f824a00743 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt -import itertools from json import loads from typing import TYPE_CHECKING, List, Optional, Tuple @@ -11,7 +10,17 @@ import frappe.defaults from frappe import _, qb, throw from frappe.model.meta import get_field_precision from frappe.query_builder.utils import DocType -from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate +from frappe.utils import ( + cint, + create_batch, + cstr, + flt, + formatdate, + get_number_format_info, + getdate, + now, + nowdate, +) from pypika import Order from pypika.terms import ExistsCriterion @@ -1149,9 +1158,7 @@ def repost_gle_for_stock_vouchers( precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 - stock_vouchers_iterator = iter(stock_vouchers) - - while stock_vouchers_chunk := list(itertools.islice(stock_vouchers_iterator, GL_REPOSTING_CHUNK)): + for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK): gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date) for voucher_type, voucher_no in stock_vouchers_chunk: @@ -1173,7 +1180,7 @@ def repost_gle_for_stock_vouchers( if repost_doc: repost_doc.db_set( "gl_reposting_index", - cint(repost_doc.gl_reposting_index) + GL_REPOSTING_CHUNK, + cint(repost_doc.gl_reposting_index) + len(stock_vouchers_chunk), ) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ea24b47a29..b1017d2c9c 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -87,6 +87,7 @@ class RepostItemValuation(Document): self.current_index = 0 self.distinct_item_and_warehouse = None self.items_to_be_repost = None + self.gl_reposting_index = 0 self.db_update() def deduplicate_similar_repost(self): diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 3c74619b46..edd2553d5d 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, call import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate -from frappe.utils.data import today +from frappe.utils.data import add_to_date, today from erpnext.accounts.utils import repost_gle_for_stock_vouchers from erpnext.controllers.stock_controller import create_item_wise_repost_entries @@ -17,10 +17,11 @@ from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.tests.test_utils import StockTestMixin from erpnext.stock.utils import PendingRepostingError -class TestRepostItemValuation(FrappeTestCase): +class TestRepostItemValuation(FrappeTestCase, StockTestMixin): def tearDown(self): frappe.flags.dont_execute_stock_reposts = False @@ -225,3 +226,49 @@ class TestRepostItemValuation(FrappeTestCase): repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc) self.assertNotIn(call("gl_reposting_index", 1), doc.db_set.mock_calls) + + def test_gl_complete_gl_reposting(self): + from erpnext.accounts import utils + + # lower numbers to simplify test + orig_chunk_size = utils.GL_REPOSTING_CHUNK + utils.GL_REPOSTING_CHUNK = 2 + self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size) + + item = self.make_item().name + + company = "_Test Company with perpetual inventory" + + for _ in range(10): + make_stock_entry(item=item, company=company, qty=1, rate=10, target="Stores - TCP1") + + # consume + consumption = make_stock_entry(item=item, company=company, qty=1, source="Stores - TCP1") + + self.assertGLEs( + consumption, + [{"credit": 10, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # backdated receipt + backdated_receipt = make_stock_entry( + item=item, + company=company, + qty=1, + rate=50, + target="Stores - TCP1", + posting_date=add_to_date(today(), days=-1), + ) + self.assertGLEs( + backdated_receipt, + [{"credit": 0, "debit": 50}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # check that original consumption GLe is updated + self.assertGLEs( + consumption, + [{"credit": 50, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) From 86919d2a6d2fd7447a4559a45bcd5f4e27a1eccc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 15 Jun 2022 21:19:09 +0530 Subject: [PATCH 11/15] test: silent test failure in stock assertions (#31377) If actual values are not present then test is silently passing, # of actual values should be at least equal to expected values. --- erpnext/accounts/utils.py | 4 +++- erpnext/stock/tests/test_utils.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f824a00743..ccf4b40246 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1175,7 +1175,9 @@ def repost_gle_for_stock_vouchers( voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) else: _delete_gl_entries(voucher_type, voucher_no) - frappe.db.commit() + + if not frappe.flags.in_test: + frappe.db.commit() if repost_doc: repost_doc.db_set( diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index b046dbda24..4e93ac93cb 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -26,6 +26,7 @@ class StockTestMixin: filters=filters, order_by="timestamp(posting_date, posting_time), creation", ) + self.assertGreaterEqual(len(sles), len(expected_sles)) for exp_sle, act_sle in zip(expected_sles, sles): for k, v in exp_sle.items(): @@ -49,7 +50,7 @@ class StockTestMixin: filters=filters, order_by=order_by or "posting_date, creation", ) - + self.assertGreaterEqual(len(actual_gles), len(expected_gles)) for exp_gle, act_gle in zip(expected_gles, actual_gles): for k, exp_value in exp_gle.items(): act_value = act_gle[k] From b4a93da9f3d69c2f45525ba8b41e7def08c74944 Mon Sep 17 00:00:00 2001 From: Jingxuan He Date: Thu, 16 Jun 2022 08:46:59 +0200 Subject: [PATCH 12/15] chore: Fix a potential variable misuse bug (#31372) * Fix a potential variable misuse bug * chore: Separate check (separate line) for empty table in Pricing Rule * chore: Code readability & check for field in row (now row itself) Co-authored-by: marination --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 2438f4b1ab..98e0a9b215 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -36,8 +36,12 @@ class PricingRule(Document): def validate_duplicate_apply_on(self): if self.apply_on != "Transaction": - field = apply_on_dict.get(self.apply_on) - values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] + apply_on_table = apply_on_dict.get(self.apply_on) + if not apply_on_table: + return + + apply_on_field = frappe.scrub(self.apply_on) + values = [d.get(apply_on_field) for d in self.get(apply_on_table) if d.get(apply_on_field)] if len(values) != len(set(values)): frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on)) From 6f2086d770647a449eca36114d7b8fd2e97ea25d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 16 Jun 2022 22:15:06 +0530 Subject: [PATCH 13/15] test: verify that all patches exist in patches.txt (#31371) * chore: delete orphaned patches * test: orphan/missing entries in patches.txt [skip ci] --- .../update_healthcare_refactored_changes.py | 131 ------------------ .../healthcare_lab_module_rename_doctypes.py | 94 ------------- .../v13_0/print_uom_after_quantity_patch.py | 9 -- .../rename_discharge_date_in_ip_record.py | 8 -- ...et_company_field_in_healthcare_doctypes.py | 25 ---- erpnext/tests/test_init.py | 5 + 6 files changed, 5 insertions(+), 267 deletions(-) delete mode 100644 erpnext/patches/v12_0/update_healthcare_refactored_changes.py delete mode 100644 erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py delete mode 100644 erpnext/patches/v13_0/print_uom_after_quantity_patch.py delete mode 100644 erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py delete mode 100644 erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py diff --git a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py deleted file mode 100644 index 5ca0d5d47d..0000000000 --- a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py +++ /dev/null @@ -1,131 +0,0 @@ -import frappe -from frappe.model.utils.rename_field import rename_field -from frappe.modules import get_doctype_module, scrub - -field_rename_map = { - "Healthcare Settings": [ - ["patient_master_name", "patient_name_by"], - ["max_visit", "max_visits"], - ["reg_sms", "send_registration_msg"], - ["reg_msg", "registration_msg"], - ["app_con", "send_appointment_confirmation"], - ["app_con_msg", "appointment_confirmation_msg"], - ["no_con", "avoid_confirmation"], - ["app_rem", "send_appointment_reminder"], - ["app_rem_msg", "appointment_reminder_msg"], - ["rem_before", "remind_before"], - ["manage_customer", "link_customer_to_patient"], - ["create_test_on_si_submit", "create_lab_test_on_si_submit"], - ["require_sample_collection", "create_sample_collection_for_lab_test"], - ["require_test_result_approval", "lab_test_approval_required"], - ["manage_appointment_invoice_automatically", "automate_appointment_invoicing"], - ], - "Drug Prescription": [["use_interval", "usage_interval"], ["in_every", "interval_uom"]], - "Lab Test Template": [ - ["sample_quantity", "sample_qty"], - ["sample_collection_details", "sample_details"], - ], - "Sample Collection": [ - ["sample_quantity", "sample_qty"], - ["sample_collection_details", "sample_details"], - ], - "Fee Validity": [["max_visit", "max_visits"]], -} - - -def execute(): - for dn in field_rename_map: - if frappe.db.exists("DocType", dn): - if dn == "Healthcare Settings": - frappe.reload_doctype("Healthcare Settings") - else: - frappe.reload_doc(get_doctype_module(dn), "doctype", scrub(dn)) - - for dt, field_list in field_rename_map.items(): - if frappe.db.exists("DocType", dt): - for field in field_list: - if dt == "Healthcare Settings": - rename_field(dt, field[0], field[1]) - elif frappe.db.has_column(dt, field[0]): - rename_field(dt, field[0], field[1]) - - # first name mandatory in Patient - if frappe.db.exists("DocType", "Patient"): - patients = frappe.db.sql("select name, patient_name from `tabPatient`", as_dict=1) - frappe.reload_doc("healthcare", "doctype", "patient") - for entry in patients: - name = entry.patient_name.split(" ") - frappe.db.set_value("Patient", entry.name, "first_name", name[0]) - - # mark Healthcare Practitioner status as Disabled - if frappe.db.exists("DocType", "Healthcare Practitioner"): - practitioners = frappe.db.sql( - "select name from `tabHealthcare Practitioner` where 'active'= 0", as_dict=1 - ) - practitioners_lst = [p.name for p in practitioners] - frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") - if practitioners_lst: - frappe.db.sql( - "update `tabHealthcare Practitioner` set status = 'Disabled' where name IN %(practitioners)s" - "", - {"practitioners": practitioners_lst}, - ) - - # set Clinical Procedure status - if frappe.db.exists("DocType", "Clinical Procedure"): - frappe.reload_doc("healthcare", "doctype", "clinical_procedure") - frappe.db.sql( - """ - UPDATE - `tabClinical Procedure` - SET - docstatus = (CASE WHEN status = 'Cancelled' THEN 2 - WHEN status = 'Draft' THEN 0 - ELSE 1 - END) - """ - ) - - # set complaints and diagnosis in table multiselect in Patient Encounter - if frappe.db.exists("DocType", "Patient Encounter"): - field_list = [["visit_department", "medical_department"], ["type", "appointment_type"]] - encounter_details = frappe.db.sql( - """select symptoms, diagnosis, name from `tabPatient Encounter`""", as_dict=True - ) - frappe.reload_doc("healthcare", "doctype", "patient_encounter") - frappe.reload_doc("healthcare", "doctype", "patient_encounter_symptom") - frappe.reload_doc("healthcare", "doctype", "patient_encounter_diagnosis") - - for field in field_list: - if frappe.db.has_column(dt, field[0]): - rename_field(dt, field[0], field[1]) - - for entry in encounter_details: - doc = frappe.get_doc("Patient Encounter", entry.name) - symptoms = entry.symptoms.split("\n") if entry.symptoms else [] - for symptom in symptoms: - if not frappe.db.exists("Complaint", symptom): - frappe.get_doc({"doctype": "Complaint", "complaints": symptom}).insert() - row = doc.append("symptoms", {"complaint": symptom}) - row.db_update() - - diagnosis = entry.diagnosis.split("\n") if entry.diagnosis else [] - for d in diagnosis: - if not frappe.db.exists("Diagnosis", d): - frappe.get_doc({"doctype": "Diagnosis", "diagnosis": d}).insert() - row = doc.append("diagnosis", {"diagnosis": d}) - row.db_update() - doc.db_update() - - if frappe.db.exists("DocType", "Fee Validity"): - # update fee validity status - frappe.db.sql( - """ - UPDATE - `tabFee Validity` - SET - status = (CASE WHEN visited >= max_visits THEN 'Completed' - ELSE 'Pending' - END) - """ - ) diff --git a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py deleted file mode 100644 index 30b84accf3..0000000000 --- a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py +++ /dev/null @@ -1,94 +0,0 @@ -import frappe -from frappe.model.utils.rename_field import rename_field - - -def execute(): - if frappe.db.exists("DocType", "Lab Test") and frappe.db.exists("DocType", "Lab Test Template"): - # rename child doctypes - doctypes = { - "Lab Test Groups": "Lab Test Group Template", - "Normal Test Items": "Normal Test Result", - "Sensitivity Test Items": "Sensitivity Test Result", - "Special Test Items": "Descriptive Test Result", - "Special Test Template": "Descriptive Test Template", - } - - frappe.reload_doc("healthcare", "doctype", "lab_test") - frappe.reload_doc("healthcare", "doctype", "lab_test_template") - - for old_dt, new_dt in doctypes.items(): - frappe.flags.link_fields = {} - should_rename = frappe.db.table_exists(old_dt) and not frappe.db.table_exists(new_dt) - if should_rename: - frappe.reload_doc("healthcare", "doctype", frappe.scrub(old_dt)) - frappe.rename_doc("DocType", old_dt, new_dt, force=True) - frappe.reload_doc("healthcare", "doctype", frappe.scrub(new_dt)) - frappe.delete_doc_if_exists("DocType", old_dt) - - parent_fields = { - "Lab Test Group Template": "lab_test_groups", - "Descriptive Test Template": "descriptive_test_templates", - "Normal Test Result": "normal_test_items", - "Sensitivity Test Result": "sensitivity_test_items", - "Descriptive Test Result": "descriptive_test_items", - } - - for doctype, parentfield in parent_fields.items(): - frappe.db.sql( - """ - UPDATE `tab{0}` - SET parentfield = %(parentfield)s - """.format( - doctype - ), - {"parentfield": parentfield}, - ) - - # copy renamed child table fields (fields were already renamed in old doctype json, hence sql) - rename_fields = { - "lab_test_name": "test_name", - "lab_test_event": "test_event", - "lab_test_uom": "test_uom", - "lab_test_comment": "test_comment", - } - - for new, old in rename_fields.items(): - if frappe.db.has_column("Normal Test Result", old): - frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}""".format(new, old)) - - if frappe.db.has_column("Normal Test Template", "test_event"): - frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""") - - if frappe.db.has_column("Normal Test Template", "test_uom"): - frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""") - - if frappe.db.has_column("Descriptive Test Result", "test_particulars"): - frappe.db.sql( - """UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""" - ) - - rename_fields = { - "lab_test_template": "test_template", - "lab_test_description": "test_description", - "lab_test_rate": "test_rate", - } - - for new, old in rename_fields.items(): - if frappe.db.has_column("Lab Test Group Template", old): - frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}""".format(new, old)) - - # rename field - frappe.reload_doc("healthcare", "doctype", "lab_test") - if frappe.db.has_column("Lab Test", "special_toggle"): - rename_field("Lab Test", "special_toggle", "descriptive_toggle") - - if frappe.db.exists("DocType", "Lab Test Group Template"): - # fix select field option - frappe.reload_doc("healthcare", "doctype", "lab_test_group_template") - frappe.db.sql( - """ - UPDATE `tabLab Test Group Template` - SET template_or_new_line = 'Add New Line' - WHERE template_or_new_line = 'Add new line' - """ - ) diff --git a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py deleted file mode 100644 index a16f909fc3..0000000000 --- a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2019, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -from erpnext.setup.install import create_print_uom_after_qty_custom_field - - -def execute(): - create_print_uom_after_qty_custom_field() diff --git a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py deleted file mode 100644 index 3bd717d77b..0000000000 --- a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py +++ /dev/null @@ -1,8 +0,0 @@ -import frappe -from frappe.model.utils.rename_field import rename_field - - -def execute(): - frappe.reload_doc("Healthcare", "doctype", "Inpatient Record") - if frappe.db.has_column("Inpatient Record", "discharge_date"): - rename_field("Inpatient Record", "discharge_date", "discharge_datetime") diff --git a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py deleted file mode 100644 index bc2d1b94f7..0000000000 --- a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py +++ /dev/null @@ -1,25 +0,0 @@ -import frappe - - -def execute(): - company = frappe.db.get_single_value("Global Defaults", "default_company") - doctypes = [ - "Clinical Procedure", - "Inpatient Record", - "Lab Test", - "Sample Collection", - "Patient Appointment", - "Patient Encounter", - "Vital Signs", - "Therapy Session", - "Therapy Plan", - "Patient Assessment", - ] - for entry in doctypes: - if frappe.db.exists("DocType", entry): - frappe.reload_doc("Healthcare", "doctype", entry) - frappe.db.sql( - "update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format( - dt=entry, company=frappe.db.escape(company) - ) - ) diff --git a/erpnext/tests/test_init.py b/erpnext/tests/test_init.py index 4d5fced083..18ce93ab83 100644 --- a/erpnext/tests/test_init.py +++ b/erpnext/tests/test_init.py @@ -45,3 +45,8 @@ class TestInit(unittest.TestCase): from frappe.tests.test_translate import verify_translation_files verify_translation_files("erpnext") + + def test_patches(self): + from frappe.tests.test_patches import check_patch_files + + check_patch_files("erpnext") From 1a3997a5669893d76ef743ff07c15dfe63fde1ae Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 16 Jun 2022 17:03:47 +0000 Subject: [PATCH 14/15] fix: transaction date gets unset in material request (#31327) * fix: set date correctly in material request * fix: use only `transaction_date` in `get_item_details` --- erpnext/public/js/controllers/transaction.js | 1 - erpnext/stock/get_item_details.py | 21 ++++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index de93c82ef2..01f72adf34 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -453,7 +453,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe is_pos: cint(me.frm.doc.is_pos), is_return: cint(me.frm.doc.is_return), is_subcontracted: me.frm.doc.is_subcontracted, - transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, doctype: me.frm.doc.doctype, name: me.frm.doc.name, diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 3776a27b35..7cff85fb57 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -63,18 +63,16 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru item = frappe.get_cached_doc("Item", args.item_code) validate_item_details(args, item) - out = get_basic_details(args, item, overwrite_warehouse) - if isinstance(doc, str): doc = json.loads(doc) - if doc and doc.get("doctype") == "Purchase Invoice": - args["bill_date"] = doc.get("bill_date") - if doc: - args["posting_date"] = doc.get("posting_date") - args["transaction_date"] = doc.get("transaction_date") + args["transaction_date"] = doc.get("transaction_date") or doc.get("posting_date") + if doc.get("doctype") == "Purchase Invoice": + args["bill_date"] = doc.get("bill_date") + + out = get_basic_details(args, item, overwrite_warehouse) get_item_tax_template(args, item, out) out["item_tax_rate"] = get_item_tax_map( args.company, @@ -596,9 +594,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date - validation_date = ( - args.get("transaction_date") or args.get("bill_date") or args.get("posting_date") - ) + validation_date = args.get("bill_date") or args.get("transaction_date") if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax): taxes_with_validity.append(tax) @@ -891,10 +887,6 @@ def get_item_price(args, item_code, ignore_party=False): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - if args.get("posting_date"): - conditions += """ and %(posting_date)s between - ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - return frappe.db.sql( """ select name, price_list_rate, uom from `tabItem Price` {conditions} @@ -921,7 +913,6 @@ def get_price_list_rate_for(args, item_code): "supplier": args.get("supplier"), "uom": args.get("uom"), "transaction_date": args.get("transaction_date"), - "posting_date": args.get("posting_date"), "batch_no": args.get("batch_no"), } From 10583eb3cebc2491e192721be1d49dd10aa00860 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 17 Jun 2022 12:13:27 +0530 Subject: [PATCH 15/15] fix: UOM handling for transaction without item (#31389) If invoice is made without item code then UOM, Stock UOM and conversion_factor all need to be manually added, this is confusing and leads missing them out leads to errors. Simplest solution: - if either UOM exists then set both to same uom conversion factor to - also set conversion factor based on UOM conversions --- .../purchase_invoice/test_purchase_invoice.py | 20 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 3c70e24cae..6412da709f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1616,6 +1616,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): company.enable_provisional_accounting_for_non_stock_items = 0 company.save() + def test_item_less_defaults(self): + + pi = frappe.new_doc("Purchase Invoice") + pi.supplier = "_Test Supplier" + pi.company = "_Test Company" + pi.append( + "items", + { + "item_name": "Opening item", + "qty": 1, + "uom": "Tonne", + "stock_uom": "Kg", + "rate": 1000, + "expense_account": "Stock Received But Not Billed - _TC", + }, + ) + + pi.save() + self.assertEqual(pi.items[0].conversion_factor, 1000) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 854c0d00f5..f49366a956 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -46,6 +46,7 @@ from erpnext.controllers.print_settings import ( from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.exceptions import InvalidCurrency from erpnext.setup.utils import get_exchange_rate +from erpnext.stock.doctype.item.item import get_uom_conv_factor from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.get_item_details import ( _get_item_tax_template, @@ -548,6 +549,15 @@ class AccountsController(TransactionBase): if ret.get("pricing_rules"): self.apply_pricing_rule_on_items(item, ret) self.set_pricing_rule_details(item, ret) + else: + # Transactions line item without item code + + uom = item.get("uom") + stock_uom = item.get("stock_uom") + if bool(uom) != bool(stock_uom): # xor + item.stock_uom = item.uom = uom or stock_uom + + item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate)