diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ee12fce7a94..56c9ff07e26 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -3,8 +3,10 @@ import frappe -from frappe import _, msgprint +from frappe import _, msgprint, qb from frappe.model.document import Document +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Sum from frappe.utils import flt, getdate, nowdate, today import erpnext @@ -120,58 +122,77 @@ class PaymentReconciliation(Document): return list(journal_entries) def get_dr_or_cr_notes(self): - condition = self.get_conditions(get_return_invoices=True) + gl = qb.DocType("GL Entry") + + voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" + doc = qb.DocType(voucher_type) + + # build conditions + sub_query_conditions = [] + conditions = [] + sub_query_conditions.append(doc.company == self.company) + + if self.get("from_payment_date"): + sub_query_conditions.append(doc.posting_date.gte(self.from_payment_date)) + + if self.get("to_payment_date"): + sub_query_conditions.append(doc.posting_date.lte(self.to_payment_date)) if self.get("cost_center"): - condition += " and doc.cost_center = '{0}' ".format(self.cost_center) + sub_query_conditions.append(doc.cost_center == self.cost_center) dr_or_cr = ( - "credit_in_account_currency" + gl["credit_in_account_currency"] if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "debit_in_account_currency" + else gl["debit_in_account_currency"] ) reconciled_dr_or_cr = ( - "debit_in_account_currency" - if dr_or_cr == "credit_in_account_currency" - else "credit_in_account_currency" + gl["debit_in_account_currency"] + if dr_or_cr == gl["credit_in_account_currency"] + else gl["credit_in_account_currency"] ) - voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" + if self.minimum_payment_amount: + conditions.append(dr_or_cr.gte(self.minimum_payment_amount)) + if self.maximum_payment_amount: + conditions.append(dr_or_cr.lte(self.maximum_payment_amount)) - return frappe.db.sql( - """ SELECT doc.name as reference_name, %(voucher_type)s as reference_type, - (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date, - account_currency as currency - FROM `tab{doc}` doc, `tabGL Entry` gl - WHERE - (doc.name = gl.against_voucher or doc.name = gl.voucher_no) - and doc.{party_type_field} = %(party)s - and doc.is_return = 1 and ifnull(doc.return_against, "") = "" - and gl.against_voucher_type = %(voucher_type)s - and doc.docstatus = 1 and gl.party = %(party)s - and gl.party_type = %(party_type)s and gl.account = %(account)s - and gl.is_cancelled = 0 {condition} - GROUP BY doc.name - Having - amount > 0 - ORDER BY doc.posting_date - """.format( - doc=voucher_type, - dr_or_cr=dr_or_cr, - reconciled_dr_or_cr=reconciled_dr_or_cr, - party_type_field=frappe.scrub(self.party_type), - condition=condition or "", - ), - { - "party": self.party, - "party_type": self.party_type, - "voucher_type": voucher_type, - "account": self.receivable_payable_account, - }, - as_dict=1, + sub_query = ( + qb.from_(doc) + .select(doc.name) + .where(Criterion.all(sub_query_conditions)) + .where( + (doc.docstatus == 1) + & (doc.is_return == 1) + & ((doc.return_against == "") | (doc.return_against.isnull())) + ) ) + query = ( + qb.from_(gl) + .select( + gl.voucher_type.as_("reference_type"), + gl.voucher_no.as_("reference_name"), + (Sum(dr_or_cr) - Sum(reconciled_dr_or_cr)).as_("amount"), + gl.posting_date, + gl.account_currency.as_("currency"), + ) + .where( + (gl.voucher_type == voucher_type) + & (gl.voucher_no.isin(sub_query)) + & (gl.is_cancelled == 0) + & (gl.account == self.receivable_payable_account) + & (gl.party_type == self.party_type) + & (gl.party == self.party) + ) + .where(Criterion.all(conditions)) + .groupby(gl.voucher_no) + .having(qb.Field("amount") > 0) + ) + dr_cr_notes = query.run(as_dict=True) + return dr_cr_notes + def add_payment_entries(self, non_reconciled_payments): self.set("payments", []) @@ -369,7 +390,7 @@ class PaymentReconciliation(Document): if not invoices_to_reconcile: frappe.throw(_("No records found in Allocation table")) - def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): + def get_conditions(self, get_invoices=False, get_payments=False): condition = " and company = '{0}' ".format(self.company) if get_invoices: @@ -397,35 +418,7 @@ class PaymentReconciliation(Document): condition += " and {dr_or_cr} <= {amount}".format( dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount) ) - - elif get_return_invoices: - condition = " and doc.company = '{0}' ".format(self.company) - condition += ( - " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) - if self.from_payment_date - else "" - ) - condition += ( - " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) - if self.to_payment_date - else "" - ) - dr_or_cr = ( - "debit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "credit_in_account_currency" - ) - - if self.minimum_invoice_amount: - condition += " and gl.{dr_or_cr} >= {amount}".format( - dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount) - ) - if self.maximum_invoice_amount: - condition += " and gl.{dr_or_cr} <= {amount}".format( - dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount) - ) - - else: + elif get_payments: condition += ( " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index ee924f86a6a..97637c7e092 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -9,6 +9,7 @@ from six import iteritems from erpnext.accounts.report.financial_statements import ( get_columns, + get_cost_centers_with_children, get_data, get_filtered_list_for_consolidated_report, get_period_list, @@ -161,10 +162,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) + filters.start_date = start_date + filters.end_date = period["to_date"] + filters.account_type = account_type - amount = get_account_type_based_gl_data( - company, start_date, period["to_date"], account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 @@ -176,7 +178,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ return data -def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): +def get_account_type_based_gl_data(company, filters=None): cond = "" filters = frappe._dict(filters or {}) @@ -192,17 +194,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type, frappe.db.escape(cstr(filters.finance_book)) ) + if filters.get("cost_center"): + filters.cost_center = get_cost_centers_with_children(filters.cost_center) + cond += " and cost_center in %(cost_center)s" + 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 + where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} + and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond} """.format( cond=cond ), - (company, start_date, end_date, account_type), + filters, ) return gl_sum[0] if gl_sum and gl_sum[0] else 0 diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 6c8f4bb6fe9..560b79243d7 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters): def get_account_type_based_data(account_type, companies, fiscal_year, filters): data = {} total = 0 + filters.account_type = account_type + filters.start_date = fiscal_year.year_start_date + filters.end_date = fiscal_year.year_end_date + for company in companies: - amount = get_account_type_based_gl_data( - company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 517e080c972..62e90ae747d 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -333,16 +333,21 @@ class StatusUpdater(Document): ) def warn_about_bypassing_with_role(self, item, qty_or_amount, role): - action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling") + if qty_or_amount == "qty": + msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.") + else: + msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.") - msg = _("{} of {} {} ignored for item {} because you have {} role.").format( - action, - _(item["target_ref_field"].title()), - frappe.bold(item["reduce_by"]), - frappe.bold(item.get("item_code")), - role, + frappe.msgprint( + msg.format( + _(item["target_ref_field"].title()), + frappe.bold(item["reduce_by"]), + frappe.bold(item.get("item_code")), + role, + ), + indicator="orange", + alert=True, ) - frappe.msgprint(msg, indicator="orange", alert=True) def update_qty(self, update_modified=True): """Updates qty or amount at row level diff --git a/erpnext/hooks.py b/erpnext/hooks.py index fb56860dae5..850daf9efed 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -526,6 +526,7 @@ scheduler_events = { "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", "erpnext.crm.doctype.lead.lead.daily_open_lead", + "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_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/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b349c50794b..c5c9b5f0d31 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1145,6 +1145,37 @@ class TestWorkOrder(FrappeTestCase): except frappe.MandatoryError: self.fail("Batch generation causing failing in Work Order") + @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1}) + def test_auto_serial_no_creation(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + fg_item = frappe.generate_hash(length=20) + child_item = frappe.generate_hash(length=20) + + bom_tree = {fg_item: {child_item: {}}} + + create_nested_bom(bom_tree, prefix="") + + item = frappe.get_doc("Item", fg_item) + item.has_serial_no = 1 + item.serial_no_series = f"{item.name}.#####" + item.save() + + try: + wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True) + serial_nos = wo_order.serial_no + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + stock_entry.set_work_order_details() + stock_entry.set_serial_no_batch_for_finished_good() + for row in stock_entry.items: + if row.item_code == fg_item: + self.assertTrue(row.serial_no) + self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos))) + + except frappe.MandatoryError: + self.fail("Batch generation causing failing in Work Order") + @change_settings( "Manufacturing Settings", {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f09cc1716f7..4c02d94fe83 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -181,13 +181,13 @@ class PickList(Document): if item_map.get(key): item_map[key].qty += item.qty - item_map[key].stock_qty += item.stock_qty + item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty")) else: item_map[key] = item # maintain count of each item (useful to limit get query) self.item_count_map.setdefault(item_code, 0) - self.item_count_map[item_code] += item.stock_qty + self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty")) return item_map.values() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 34c60d1eea5..d53f4cdbac9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,13 +4,25 @@ 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 -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, +) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -2192,16 +2204,16 @@ class StockEntry(StockController): d.qty -= process_loss_dict[d.item_code][1] def set_serial_no_batch_for_finished_good(self): - serial_nos = "" + serial_nos = [] if self.pro_doc.serial_no: - serial_nos = self.get_serial_nos_for_fg() + serial_nos = self.get_serial_nos_for_fg() or [] for row in self.items: if row.is_finished_item and row.item_code == self.pro_doc.production_item: if serial_nos: row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)]) - def get_serial_nos_for_fg(self, args): + def get_serial_nos_for_fg(self): fields = [ "`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", @@ -2217,9 +2229,7 @@ class StockEntry(StockController): ] stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) - - if self.pro_doc.serial_no: - return self.get_available_serial_nos(stock_entries) + return self.get_available_serial_nos(stock_entries) def get_available_serial_nos(self, stock_entries): used_serial_nos = [] @@ -2556,3 +2566,63 @@ def get_supplied_items(purchase_order): ) return supplied_item_details + + +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 + diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 64ea0435e16..c38b3d997a3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate, nowtime, today +from frappe.utils import add_days, flt, now, nowdate, nowtime, today from six import iteritems from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -18,6 +18,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 +1573,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) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d44b6ed93c9..47911b46146 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -813,9 +813,9 @@ def insert_item_price(args): ): if frappe.has_permission("Item Price", "write"): price_list_rate = ( - (args.rate + args.discount_amount) / args.get("conversion_factor") + (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor") if args.get("conversion_factor") - else (args.rate + args.discount_amount) + else (flt(args.rate) + flt(args.discount_amount)) ) item_price = frappe.db.get_value( diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index f308e9e41f1..1c54d775a09 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -75,6 +75,7 @@ def get_item_info(filters): if filters.get("brand"): conditions.append("item.brand=%(brand)s") conditions.append("is_stock_item = 1") + conditions.append("disabled = 0") return frappe.db.sql( """select name, item_name, description, brand, item_group, diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 69780276ce2..0b173b65a9e 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1844,6 +1844,8 @@ Outstanding Amt,Offener Betrag, Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen, Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}), Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet), +Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben." +Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben." Overdue,Überfällig, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,