From b82154cb9e1d9376519c62c864fc53b265c64268 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 19 Dec 2022 23:24:34 +0530 Subject: [PATCH 1/5] fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation (cherry picked from commit b1721b79cebc7c4c438f00a1deaebaa5b39f7e0b) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py --- erpnext/hooks.py | 1 + .../stock/doctype/stock_entry/stock_entry.py | 171 ++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index fb56860dae5..0d85810179e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -490,6 +490,7 @@ scheduler_events = { ], "daily": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", + "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3da24974919..707ad8af794 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,13 +4,29 @@ import json from collections import defaultdict +from typing import Dict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum +<<<<<<< HEAD from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate from six import iteritems, itervalues, string_types +======= +from frappe.utils import ( + add_days, + cint, + comma_or, + cstr, + flt, + format_time, + formatdate, + getdate, + nowdate, + today, +) +>>>>>>> b1721b79ce (fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -2554,3 +2570,158 @@ def get_supplied_items(purchase_order): ) return supplied_item_details +<<<<<<< HEAD +======= + + +@frappe.whitelist() +def get_items_from_subcontract_order(source_name, target_doc=None): + from erpnext.controllers.subcontracting_controller import make_rm_stock_entry + + if isinstance(target_doc, str): + target_doc = frappe.get_doc(json.loads(target_doc)) + + order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order" + target_doc = make_rm_stock_entry( + subcontract_order=source_name, order_doctype=order_doctype, target_doc=target_doc + ) + + return target_doc + + +def get_available_materials(work_order) -> dict: + data = get_stock_entry_data(work_order) + + available_materials = {} + for row in data: + key = (row.item_code, row.warehouse) + if row.purpose != "Material Transfer for Manufacture": + key = (row.item_code, row.s_warehouse) + + if key not in available_materials: + available_materials.setdefault( + key, + frappe._dict( + {"item_details": row, "batch_details": defaultdict(float), "qty": 0, "serial_nos": []} + ), + ) + + item_data = available_materials[key] + + if row.purpose == "Material Transfer for Manufacture": + item_data.qty += row.qty + if row.batch_no: + item_data.batch_details[row.batch_no] += row.qty + + if row.serial_no: + item_data.serial_nos.extend(get_serial_nos(row.serial_no)) + item_data.serial_nos.sort() + else: + # Consume raw material qty in case of 'Manufacture' or 'Material Consumption for Manufacture' + + item_data.qty -= row.qty + if row.batch_no: + item_data.batch_details[row.batch_no] -= row.qty + + if row.serial_no: + for serial_no in get_serial_nos(row.serial_no): + item_data.serial_nos.remove(serial_no) + + return available_materials + + +def get_stock_entry_data(work_order): + stock_entry = frappe.qb.DocType("Stock Entry") + stock_entry_detail = frappe.qb.DocType("Stock Entry Detail") + + return ( + frappe.qb.from_(stock_entry) + .from_(stock_entry_detail) + .select( + stock_entry_detail.item_name, + stock_entry_detail.original_item, + stock_entry_detail.item_code, + stock_entry_detail.qty, + (stock_entry_detail.t_warehouse).as_("warehouse"), + (stock_entry_detail.s_warehouse).as_("s_warehouse"), + stock_entry_detail.description, + stock_entry_detail.stock_uom, + stock_entry_detail.expense_account, + stock_entry_detail.cost_center, + stock_entry_detail.batch_no, + stock_entry_detail.serial_no, + stock_entry.purpose, + ) + .where( + (stock_entry.name == stock_entry_detail.parent) + & (stock_entry.work_order == work_order) + & (stock_entry.docstatus == 1) + & (stock_entry_detail.s_warehouse.isnotnull()) + & ( + stock_entry.purpose.isin( + ["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"] + ) + ) + ) + .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) + ).run(as_dict=1) + + +def audit_incorrect_valuation_entries(): + # Audit of stock transfer entries having incorrect valuation + from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + + stock_entries = get_incorrect_stock_entries() + + for stock_entry, values in stock_entries.items(): + reposting_data = frappe._dict( + { + "posting_date": values.posting_date, + "posting_time": values.posting_time, + "voucher_type": "Stock Entry", + "voucher_no": stock_entry, + "company": values.company, + } + ) + + create_repost_item_valuation_entry(reposting_data) + + +def get_incorrect_stock_entries() -> Dict: + stock_entry = frappe.qb.DocType("Stock Entry") + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") + transfer_purposes = [ + "Material Transfer", + "Material Transfer for Manufacture", + "Send to Subcontractor", + ] + + query = ( + frappe.qb.from_(stock_entry) + .inner_join(stock_ledger_entry) + .on(stock_entry.name == stock_ledger_entry.voucher_no) + .select( + stock_entry.name, + stock_entry.company, + stock_entry.posting_date, + stock_entry.posting_time, + Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"), + ) + .where( + (stock_entry.docstatus == 1) + & (stock_entry.purpose.isin(transfer_purposes)) + & (stock_ledger_entry.modified > add_days(today(), -2)) + ) + .groupby(stock_ledger_entry.voucher_detail_no) + .having(Sum(stock_ledger_entry.stock_value_difference) != 0) + ) + + data = query.run(as_dict=True) + stock_entries = {} + + for row in data: + if abs(row.stock_value) > 0.1 and row.name not in stock_entries: + stock_entries.setdefault(row.name, row) + + return stock_entries +>>>>>>> b1721b79ce (fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation) From 4dbce8766096f2dfe89c366d9756b0f0a4ba667e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 20 Dec 2022 00:14:41 +0530 Subject: [PATCH 2/5] test: added test case to validate audit for incorrect entries (cherry picked from commit f31612376af60e9b7ed5bce10edba49e9dc27b0d) # Conflicts: # erpnext/hooks.py # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- erpnext/hooks.py | 6 ++- .../doctype/stock_entry/test_stock_entry.py | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 0d85810179e..52ee7a17bba 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -490,7 +490,6 @@ scheduler_events = { ], "daily": [ "erpnext.support.doctype.issue.issue.auto_close_tickets", - "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", @@ -526,7 +525,12 @@ scheduler_events = { "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", +<<<<<<< HEAD "erpnext.crm.doctype.lead.lead.daily_open_lead", +======= + "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", + "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", +>>>>>>> f31612376a (test: added test case to validate audit for incorrect entries) ], "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"], "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"], diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 64ea0435e16..5cf2a556ee2 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,8 +5,12 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings +<<<<<<< HEAD from frappe.utils import add_days, flt, nowdate, nowtime, today from six import iteritems +======= +from frappe.utils import add_days, flt, now, nowdate, nowtime, today +>>>>>>> f31612376a (test: added test case to validate audit for incorrect entries) from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -18,6 +22,8 @@ from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, + audit_incorrect_valuation_entries, + get_incorrect_stock_entries, move_sample_to_retention_warehouse, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1571,6 +1577,44 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) + def test_audit_incorrect_stock_entries(self): + item_code = "Test Incorrect Valuation Rate Item - 001" + create_item(item_code=item_code, is_stock_item=1) + + make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + posting_date=add_days(nowdate(), -10), + qty=2, + rate=500, + to_warehouse="_Test Warehouse - _TC", + ) + + transfer_entry = make_stock_entry( + item_code=item_code, + purpose="Material Transfer", + qty=2, + rate=500, + from_warehouse="_Test Warehouse - _TC", + to_warehouse="_Test Warehouse 1 - _TC", + ) + + sle_name = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name" + ) + + frappe.db.set_value( + "Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10} + ) + + stock_entries = get_incorrect_stock_entries() + self.assertTrue(transfer_entry.name in stock_entries) + + audit_incorrect_valuation_entries() + + stock_entries = get_incorrect_stock_entries() + self.assertFalse(transfer_entry.name in stock_entries) + def make_serialized_item(**args): args = frappe._dict(args) From 2a18067aadb2e03cf42b8feea12436909979b0a6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 20 Dec 2022 11:49:22 +0530 Subject: [PATCH 3/5] fix: conflict in hooks file --- erpnext/hooks.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 52ee7a17bba..850daf9efed 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -525,12 +525,8 @@ scheduler_events = { "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", -<<<<<<< HEAD "erpnext.crm.doctype.lead.lead.daily_open_lead", -======= - "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", ->>>>>>> f31612376a (test: added test case to validate audit for incorrect entries) ], "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"], "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"], From 4587bb376713405cac2a310339b3480eba45cf71 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 20 Dec 2022 11:52:33 +0530 Subject: [PATCH 4/5] fix: conflict in stock_entry --- .../stock/doctype/stock_entry/stock_entry.py | 101 +----------------- 1 file changed, 1 insertion(+), 100 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 707ad8af794..d53f4cdbac9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -10,10 +10,7 @@ import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -<<<<<<< HEAD -from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate from six import iteritems, itervalues, string_types -======= from frappe.utils import ( add_days, cint, @@ -26,7 +23,6 @@ from frappe.utils import ( nowdate, today, ) ->>>>>>> b1721b79ce (fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -2570,101 +2566,6 @@ def get_supplied_items(purchase_order): ) return supplied_item_details -<<<<<<< HEAD -======= - - -@frappe.whitelist() -def get_items_from_subcontract_order(source_name, target_doc=None): - from erpnext.controllers.subcontracting_controller import make_rm_stock_entry - - if isinstance(target_doc, str): - target_doc = frappe.get_doc(json.loads(target_doc)) - - order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order" - target_doc = make_rm_stock_entry( - subcontract_order=source_name, order_doctype=order_doctype, target_doc=target_doc - ) - - return target_doc - - -def get_available_materials(work_order) -> dict: - data = get_stock_entry_data(work_order) - - available_materials = {} - for row in data: - key = (row.item_code, row.warehouse) - if row.purpose != "Material Transfer for Manufacture": - key = (row.item_code, row.s_warehouse) - - if key not in available_materials: - available_materials.setdefault( - key, - frappe._dict( - {"item_details": row, "batch_details": defaultdict(float), "qty": 0, "serial_nos": []} - ), - ) - - item_data = available_materials[key] - - if row.purpose == "Material Transfer for Manufacture": - item_data.qty += row.qty - if row.batch_no: - item_data.batch_details[row.batch_no] += row.qty - - if row.serial_no: - item_data.serial_nos.extend(get_serial_nos(row.serial_no)) - item_data.serial_nos.sort() - else: - # Consume raw material qty in case of 'Manufacture' or 'Material Consumption for Manufacture' - - item_data.qty -= row.qty - if row.batch_no: - item_data.batch_details[row.batch_no] -= row.qty - - if row.serial_no: - for serial_no in get_serial_nos(row.serial_no): - item_data.serial_nos.remove(serial_no) - - return available_materials - - -def get_stock_entry_data(work_order): - stock_entry = frappe.qb.DocType("Stock Entry") - stock_entry_detail = frappe.qb.DocType("Stock Entry Detail") - - return ( - frappe.qb.from_(stock_entry) - .from_(stock_entry_detail) - .select( - stock_entry_detail.item_name, - stock_entry_detail.original_item, - stock_entry_detail.item_code, - stock_entry_detail.qty, - (stock_entry_detail.t_warehouse).as_("warehouse"), - (stock_entry_detail.s_warehouse).as_("s_warehouse"), - stock_entry_detail.description, - stock_entry_detail.stock_uom, - stock_entry_detail.expense_account, - stock_entry_detail.cost_center, - stock_entry_detail.batch_no, - stock_entry_detail.serial_no, - stock_entry.purpose, - ) - .where( - (stock_entry.name == stock_entry_detail.parent) - & (stock_entry.work_order == work_order) - & (stock_entry.docstatus == 1) - & (stock_entry_detail.s_warehouse.isnotnull()) - & ( - stock_entry.purpose.isin( - ["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"] - ) - ) - ) - .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) - ).run(as_dict=1) def audit_incorrect_valuation_entries(): @@ -2724,4 +2625,4 @@ def get_incorrect_stock_entries() -> Dict: stock_entries.setdefault(row.name, row) return stock_entries ->>>>>>> b1721b79ce (fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation) + From 7ef0c6bb015166163447d3c463fd04351f699fc6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 20 Dec 2022 11:55:22 +0530 Subject: [PATCH 5/5] fix: conflict --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5cf2a556ee2..c38b3d997a3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,12 +5,8 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -<<<<<<< HEAD -from frappe.utils import add_days, flt, nowdate, nowtime, today -from six import iteritems -======= from frappe.utils import add_days, flt, now, nowdate, nowtime, today ->>>>>>> f31612376a (test: added test case to validate audit for incorrect entries) +from six import iteritems from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import (