diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 0641612c615..183049c8dfc 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -252,6 +252,10 @@ frappe.treeview_settings["Account"] = { root_company, ]); } else { + const node = treeview.tree.get_selected_node(); + if (node.is_root) { + frappe.throw(__("Cannot create root account.")); + } treeview.new_node(); } }, @@ -270,7 +274,8 @@ frappe.treeview_settings["Account"] = { ].treeview.page.fields_dict.root_company.get_value() || frappe.flags.ignore_root_company_validation) && node.expandable && - !node.hide_add + !node.hide_add && + !node.is_root ); }, click: function () { diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index ba577f2b8c9..931e05a716b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -26,9 +26,20 @@ frappe.ui.form.on("Accounts Settings", { add_taxes_from_taxes_and_charges_template(frm) { toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template"); }, + add_taxes_from_item_tax_template(frm) { toggle_tax_settings(frm, "add_taxes_from_item_tax_template"); }, + + drop_ar_procedures: function (frm) { + frm.call({ + doc: frm.doc, + method: "drop_ar_sql_procedures", + callback: function (r) { + frappe.show_alert(__("Procedures dropped"), 5); + }, + }); + }, }); function toggle_tax_settings(frm, field_name) { diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 80b7d996101..946e8d1a5cc 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -88,6 +88,8 @@ "receivable_payable_remarks_length", "accounts_receivable_payable_tuning_section", "receivable_payable_fetch_method", + "column_break_ntmi", + "drop_ar_procedures", "legacy_section", "ignore_is_opening_check_for_reporting", "payment_request_settings", @@ -552,7 +554,7 @@ "fieldname": "receivable_payable_fetch_method", "fieldtype": "Select", "label": "Data Fetch Method", - "options": "Buffered Cursor\nUnBuffered Cursor" + "options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL" }, { "fieldname": "accounts_receivable_payable_tuning_section", @@ -609,6 +611,17 @@ "fieldname": "add_taxes_from_taxes_and_charges_template", "fieldtype": "Check", "label": "Automatically Add Taxes from Taxes and Charges Template" + }, + { + "fieldname": "column_break_ntmi", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"", + "description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report", + "fieldname": "drop_ar_procedures", + "fieldtype": "Button", + "label": "Drop Procedures" } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 7959f163871..362b235b2f6 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -58,7 +58,7 @@ class AccountsSettings(Document): merge_similar_account_heads: DF.Check over_billing_allowance: DF.Currency post_change_gl_entries: DF.Check - receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"] + receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"] receivable_payable_remarks_length: DF.Int reconciliation_queue_size: DF.Int role_allowed_to_over_bill: DF.Link | None @@ -152,3 +152,11 @@ class AccountsSettings(Document): ), title=_("Auto Tax Settings Error"), ) + + @frappe.whitelist() + def drop_ar_sql_procedures(self): + from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR + + frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}") + frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}") + frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index aa0a898bd42..2cc1d5f22bb 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -1071,10 +1071,3 @@ def create_pos_invoice(**args): pos_inv.payment_schedule = [] return pos_inv - - -def make_batch_item(item_name): - from erpnext.stock.doctype.item.test_item import make_item - - if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b12f51e8a4b..829428eec86 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -461,6 +461,7 @@ class SalesInvoice(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) + self.validate_standalone_serial_nos_customer() self.update_stock_reservation_entries() self.update_stock_ledger() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index bae372250ea..70d09ab70ed 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3055,6 +3055,28 @@ class TestSalesInvoice(FrappeTestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + # cases where distributed discount amount is not set + frappe.db.set_value( + "Sales Invoice Item", + {"name": ["in", [d.name for d in si.items]]}, + "distributed_discount_amount", + 0, + ) + + si.load_from_db() + si.additional_discount_account = additional_discount_account + # Ledger reposted implicitly upon 'Update After Submit' + si.save() + + expected_gle = [ + ["Debtors - _TC", 88, 0.0, nowdate()], + ["Discount Account - _TC", 22.0, 0.0, nowdate()], + ["Service - _TC", 0.0, 100.0, nowdate()], + ["TDS Payable - _TC", 0.0, 10.0, nowdate()], + ] + + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + def test_asset_depreciation_on_sale_with_pro_rata(self): """ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index e46af2657b5..ac9d5bfbd01 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -23,6 +23,13 @@ frappe.query_reports["Accounts Payable Summary"] = { options: "Posting Date\nDue Date", default: "Due Date", }, + { + fieldname: "calculate_ageing_with", + label: __("Calculate Ageing With"), + fieldtype: "Select", + options: "Report Date\nToday Date", + default: "Report Date", + }, { fieldname: "range", label: __("Ageing Range"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a2237ea9ac2..6061be9f3a8 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -6,7 +6,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub -from frappe.desk.reportview import build_match_conditions +from frappe.database.schema import get_definition from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -16,6 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimension_with_children, ) from erpnext.accounts.utils import ( + build_qb_match_conditions, get_advance_payment_doctypes, get_currency_precision, get_party_types_from_account_type, @@ -127,6 +128,8 @@ class ReceivablePayableReport: self.fetch_ple_in_buffered_cursor() elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() + elif self.ple_fetch_method == "Raw SQL": + self.fetch_ple_in_sql_procedures() # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -134,8 +137,7 @@ class ReceivablePayableReport: self.build_data() def fetch_ple_in_buffered_cursor(self): - query, param = self.ple_query - self.ple_entries = frappe.db.sql(query, param, as_dict=True) + self.ple_entries = self.ple_query.run(as_dict=True) for ple in self.ple_entries: self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -148,9 +150,8 @@ class ReceivablePayableReport: def fetch_ple_in_unbuffered_cursor(self): self.ple_entries = [] - query, param = self.ple_query with frappe.db.unbuffered_cursor(): - for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True): + for ple in self.ple_query.run(as_dict=True, as_iterator=True): self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding self.ple_entries.append(ple) @@ -318,6 +319,79 @@ class ReceivablePayableReport: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency + def fetch_ple_in_sql_procedures(self): + self.proc = InitSQLProceduresForAR() + + build_balance = f""" + begin not atomic + declare done boolean default false; + declare rec1 row type of `{self.proc._row_def_table_name}`; + declare ple cursor for {self.ple_query.get_sql()}; + declare continue handler for not found set done = true; + + open ple; + fetch ple into rec1; + while not done do + call {self.proc.init_procedure_name}(rec1); + fetch ple into rec1; + end while; + close ple; + + set done = false; + open ple; + fetch ple into rec1; + while not done do + call {self.proc.allocate_procedure_name}(rec1); + fetch ple into rec1; + end while; + close ple; + end; + """ + frappe.db.sql(build_balance) + + balances = frappe.db.sql( + f"""select + name, + voucher_type, + voucher_no, + party, + party_account `account`, + posting_date, + account_currency, + cost_center, + sum(invoiced) `invoiced`, + sum(paid) `paid`, + sum(credit_note) `credit_note`, + sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`, + sum(invoiced_in_account_currency) `invoiced_in_account_currency`, + sum(paid_in_account_currency) `paid_in_account_currency`, + sum(credit_note_in_account_currency) `credit_note_in_account_currency`, + sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency` + from `{self.proc._voucher_balance_name}` group by name order by posting_date;""", + as_dict=True, + ) + for x in balances: + if self.filters.get("ignore_accounts"): + key = (x.voucher_type, x.voucher_no, x.party) + else: + key = (x.account, x.voucher_type, x.voucher_no, x.party) + + _d = self.build_voucher_dict(x) + for field in [ + "invoiced", + "paid", + "credit_note", + "outstanding", + "invoiced_in_account_currency", + "paid_in_account_currency", + "credit_note_in_account_currency", + "outstanding_in_account_currency", + "cost_center", + ]: + _d[field] = x.get(field) + + self.voucher_balance[key] = _d + def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -861,18 +935,15 @@ class ReceivablePayableReport: else: query = query.select(ple.remarks) - query, param = query.walk() - - match_conditions = build_match_conditions("Payment Ledger Entry") - if match_conditions: - query += " AND " + match_conditions + if match_conditions := build_qb_match_conditions("Payment Ledger Entry"): + query = query.where(Criterion.all(match_conditions)) if self.filters.get("group_by_party"): - query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`" + query = query.orderby(self.ple.party, self.ple.posting_date) else: - query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`" + query = query.orderby(self.ple.posting_date, self.ple.party) - self.ple_query = (query, param) + self.ple_query = query def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -1253,3 +1324,134 @@ def get_customer_group_with_children(customer_groups): frappe.throw(_("Customer Group: {0} does not exist").format(d)) return list(set(all_customer_groups)) + + +class InitSQLProceduresForAR: + """ + Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report + """ + + _varchar_type = get_definition("Data") + _currency_type = get_definition("Currency") + # Temporary Tables + _voucher_balance_name = "_ar_voucher_balance" + _voucher_balance_definition = f""" + create temporary table `{_voucher_balance_name}`( + name {_varchar_type}, + voucher_type {_varchar_type}, + voucher_no {_varchar_type}, + party {_varchar_type}, + party_account {_varchar_type}, + posting_date date, + account_currency {_varchar_type}, + cost_center {_varchar_type}, + invoiced {_currency_type}, + paid {_currency_type}, + credit_note {_currency_type}, + invoiced_in_account_currency {_currency_type}, + paid_in_account_currency {_currency_type}, + credit_note_in_account_currency {_currency_type}) engine=memory; + """ + + _row_def_table_name = "_ar_ple_row" + _row_def_table_definition = f""" + create temporary table `{_row_def_table_name}`( + name {_varchar_type}, + account {_varchar_type}, + voucher_type {_varchar_type}, + voucher_no {_varchar_type}, + against_voucher_type {_varchar_type}, + against_voucher_no {_varchar_type}, + party_type {_varchar_type}, + cost_center {_varchar_type}, + party {_varchar_type}, + posting_date date, + due_date date, + account_currency {_varchar_type}, + amount {_currency_type}, + amount_in_account_currency {_currency_type}) engine=memory; + """ + + # Function + genkey_function_name = "ar_genkey" + genkey_function_sql = f""" + create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40) + begin + if allocate then + return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party)); + else + return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party)); + end if; + end + """ + + # Procedures + init_procedure_name = "ar_init_tmp_table" + init_procedure_sql = f""" + create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`) + begin + if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false)) + then + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); + end if; + end; + """ + + allocate_procedure_name = "ar_allocate_to_tmp_table" + allocate_procedure_sql = f""" + create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`) + begin + declare invoiced {_currency_type} default 0; + declare invoiced_in_account_currency {_currency_type} default 0; + declare paid {_currency_type} default 0; + declare paid_in_account_currency {_currency_type} default 0; + declare credit_note {_currency_type} default 0; + declare credit_note_in_account_currency {_currency_type} default 0; + + + if ple.amount > 0 then + if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set invoiced = ple.amount; + set invoiced_in_account_currency = ple.amount_in_account_currency; + end if; + else + + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then + if (ple.voucher_no = ple.against_voucher_no) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set credit_note = -1 * ple.amount; + set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + else + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + + end if; + + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0); + end; + """ + + def __init__(self): + existing_procedures = frappe.db.get_routines() + + if self.genkey_function_name not in existing_procedures: + frappe.db.sql(self.genkey_function_sql) + + if self.init_procedure_name not in existing_procedures: + frappe.db.sql(self.init_procedure_sql) + + if self.allocate_procedure_name not in existing_procedures: + frappe.db.sql(self.allocate_procedure_sql) + + frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`") + frappe.db.sql(self._voucher_balance_definition) + + frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`") + frappe.db.sql(self._row_def_table_definition) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 17ee5e0b323..ae0bddaa766 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -23,6 +23,13 @@ frappe.query_reports["Accounts Receivable Summary"] = { options: "Posting Date\nDue Date", default: "Due Date", }, + { + fieldname: "calculate_ageing_with", + label: __("Calculate Ageing With"), + fieldtype: "Select", + options: "Report Date\nToday Date", + default: "Report Date", + }, { fieldname: "range", label: __("Ageing Range"), diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index db984e821a5..084ea9b80ea 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -179,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): def get_condition(dimension): conditions = [] - conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s") + conditions.append(f"{frappe.scrub(dimension)} in (%(dimensions)s)") return " and {}".format(" and ".join(conditions)) if conditions else "" diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c775773cd71..9132cb15fd9 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Optional import frappe import frappe.defaults from frappe import _, qb, throw +from frappe.desk.reportview import build_match_conditions from frappe.model.meta import get_field_precision from frappe.query_builder import AliasedQuery, Case, Criterion, Table from frappe.query_builder.functions import Count, Max, Sum @@ -2347,3 +2348,19 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15): "frequency": "Cron", } ).save() + + +def build_qb_match_conditions(doctype, user=None) -> list: + match_filters = build_match_conditions(doctype, user, False) + criterion = [] + if match_filters: + from frappe import qb + + _dt = qb.DocType(doctype) + + for filter in match_filters: + for d, names in filter.items(): + fieldname = d.lower().replace(" ", "_") + criterion.append(_dt[fieldname].isin(names)) + + return criterion diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 616680d07ba..019c97114fa 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1215,6 +1215,10 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): opening_accumulated_depreciation = flt( (asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity ) + value_after_depreciation = flt( + (asset.value_after_depreciation * remaining_qty) / asset.asset_quantity, + asset.precision("gross_purchase_amount"), + ) frappe.db.set_value( "Asset", @@ -1222,6 +1226,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): { "opening_accumulated_depreciation": opening_accumulated_depreciation, "gross_purchase_amount": remaining_gross_purchase_amount, + "value_after_depreciation": value_after_depreciation, "asset_quantity": remaining_qty, }, ) @@ -1283,6 +1288,10 @@ def create_new_asset_after_split(asset, split_qty): new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.asset_quantity = split_qty new_asset.split_from = asset.name + new_asset.value_after_depreciation = flt( + (asset.value_after_depreciation * split_qty) / asset.asset_quantity, + asset.precision("gross_purchase_amount"), + ) for row in new_asset.get("finance_books"): row.value_after_depreciation = flt((row.value_after_depreciation * split_qty) / asset.asset_quantity) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 33c5783861d..e0e53adeebd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1946,6 +1946,15 @@ class AccountsController(TransactionBase): and self.get("discount_amount") and self.get("additional_discount_account") ): + # cases where distributed_discount_amount is not patched + if not hasattr(self, "__has_distributed_discount_set"): + self.__has_distributed_discount_set = any( + i.distributed_discount_amount for i in self.get("items") + ) + + if not self.__has_distributed_discount_set: + return item.amount, item.base_amount + amount += item.distributed_discount_amount base_amount += flt( item.distributed_discount_amount * self.get("conversion_rate"), diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index cb359dc5a71..011f21fe388 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -36,6 +36,17 @@ def validate_return_against(doc): party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier" + if ref_doc.get(party_type) != doc.get(party_type): + frappe.throw( + _("The {0} {1} does not match with the {0} {2} in the {3} {4}").format( + doc.meta.get_label(party_type), + doc.get(party_type), + ref_doc.get(party_type), + ref_doc.doctype, + ref_doc.name, + ) + ) + if ( ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 9089b5a2829..5f7cfb165d4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -57,6 +57,35 @@ class SellingController(StockController): if self.get(table_field): self.set_serial_and_batch_bundle(table_field) + def validate_standalone_serial_nos_customer(self): + if not self.is_return or self.return_against: + return + + if self.doctype in ["Sales Invoice", "Delivery Note"]: + bundle_ids = [d.serial_and_batch_bundle for d in self.get("items") if d.serial_and_batch_bundle] + if not bundle_ids: + return + + serial_nos = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", bundle_ids)}, + pluck="serial_no", + ) + + if serial_nos := frappe.get_all( + "Serial No", + filters={"name": ("in", serial_nos), "customer": ("is", "set")}, + fields=["name", "customer"], + ): + for sn in serial_nos: + if sn.customer and sn.customer != self.customer: + frappe.throw( + _( + "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" + ).format(frappe.bold(sn.name), frappe.bold(sn.customer)), + title=_("Serial No Already Assigned"), + ) + def set_missing_values(self, for_validate=False): super().set_missing_values(for_validate) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 22a71734f3d..ab5c1b3c69d 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -464,7 +464,7 @@ class SubcontractingController(StockController): i += 1 def __remove_serial_and_batch_bundle(self, item): - if item.serial_and_batch_bundle: + if item.get("serial_and_batch_bundle"): frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True) def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): @@ -953,7 +953,7 @@ class SubcontractingController(StockController): ) sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows) - sco_doc.update_reserved_qty_for_subcontracting() + sco_doc.update_reserved_qty_for_subcontracting(sco_item_rows) def make_sl_entries_for_supplier_warehouse(self, sl_entries): if hasattr(self, "supplied_items"): @@ -1046,7 +1046,7 @@ class SubcontractingController(StockController): return supplied_items_cost - def set_subcontracting_order_status(self): + def set_subcontracting_order_status(self, update_bin=True): if self.doctype == "Subcontracting Order": self.update_status() elif self.doctype == "Subcontracting Receipt": @@ -1055,7 +1055,7 @@ class SubcontractingController(StockController): if self.subcontract_orders: for sco in set(self.subcontract_orders): sco_doc = frappe.get_doc("Subcontracting Order", sco) - sco_doc.update_status() + sco_doc.update_status(update_bin=update_bin) def calculate_additional_costs(self): self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index bf6bf2530ce..047b39df54f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1274,7 +1274,7 @@ def get_children(parent=None, is_root=False, **filters): bom_items = frappe.get_all( "BOM Item", - fields=["item_code", "bom_no as value", "stock_qty"], + fields=["item_code", "bom_no as value", "stock_qty", "qty"], filters=[["parent", "=", frappe.form_dict.parent]], order_by="idx", ) diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 534de0e654b..9b4b3c8f96c 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -16,7 +16,14 @@ frappe.treeview_settings["BOM"] = { show_expand_all: false, get_label: function (node) { if (node.data.qty) { - return node.data.qty + " x " + node.data.item_code; + const escape = frappe.utils.escape_html; + let label = escape(node.data.item_code); + if (node.data.item_name && node.data.item_code !== node.data.item_name) { + label += `: ${escape(node.data.item_name)}`; + } + return `${label} ${node.data.qty} ${escape( + __(node.data.stock_uom) + )}`; } else { return node.data.item_code || node.data.value; } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 71cb012c1b8..447e264ad75 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -412,3 +412,4 @@ erpnext.patches.v15_0.drop_sle_indexes erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.set_company_on_pos_inv_merge_log +erpnext.patches.v15_0.rename_price_list_to_buying_price_list diff --git a/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py b/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py new file mode 100644 index 00000000000..2b6dbb77b9a --- /dev/null +++ b/erpnext/patches/v15_0/rename_price_list_to_buying_price_list.py @@ -0,0 +1,11 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + if frappe.db.has_column("Material Request", "price_list"): + rename_field( + "Material Request", + "price_list", + "buying_price_list", + ) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 2162c000221..8b5c8e0fe59 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -582,7 +582,6 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { ignore_pricing_rule: frm.doc.ignore_pricing_rule, doctype: frm.doc.doctype }, - price_list: frm.doc.price_list, }, freeze: true, callback: function(r) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 76ff4e8c49d..c7260ccc722 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -42,6 +42,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate); + cur_frm.cscript.set_gross_profit(item); + cur_frm.cscript.calculate_taxes_and_totals(); + cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); + if (item.item_code && item.rate) { frappe.call({ method: "erpnext.stock.get_item_details.get_item_tax_template", @@ -63,10 +67,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } }); } - - cur_frm.cscript.set_gross_profit(item); - cur_frm.cscript.calculate_taxes_and_totals(); - cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); }); frappe.ui.form.on(this.frm.cscript.tax_table, "rate", function(frm, cdt, cdn) { @@ -989,7 +989,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } var party = me.frm.doc[frappe.model.scrub(party_type)]; - if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc.get(party_account_field))) { + if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc[party_account_field])) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", args: { diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 6ae0d675140..e930322a2c4 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -345,7 +345,10 @@ erpnext.PointOfSale.ItemSelector = class { const items = this.search_index[selling_price_list][search_term]; this.items = items; this.render_item_list(items); - this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); + this.auto_add_item && + this.search_field.$input[0].value && + this.items.length == 1 && + this.add_filtered_item_to_cart(); return; } } @@ -358,7 +361,10 @@ erpnext.PointOfSale.ItemSelector = class { } this.items = items; this.render_item_list(items); - this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); + this.auto_add_item && + this.search_field.$input[0].value && + this.items.length == 1 && + this.add_filtered_item_to_cart(); }); } diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 30de87e151a..1c80fd84965 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -600,7 +600,7 @@ "collapsible": 1, "fieldname": "exit", "fieldtype": "Tab Break", - "label": "Exit", + "label": "Employee Exit", "oldfieldtype": "Section Break" }, { @@ -822,7 +822,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2025-02-07 13:54:40.122345", + "modified": "2025-07-04 08:29:34.347269", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index 0c97b626954..b50eb381c95 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -2,8 +2,10 @@ frappe.listview_settings["Employee"] = { add_fields: ["status", "branch", "department", "designation", "image"], filters: [["status", "=", "Active"]], get_indicator: function (doc) { - var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status]; - return indicator; + return [ + __(doc.status, null, "Employee"), + { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status], + "status,=," + doc.status, + ]; }, }; diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 9fb036a48d9..eae5c9f6c74 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -104,7 +104,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if not transaction_date: transaction_date = nowdate() - currency_settings = frappe.get_doc("Accounts Settings").as_dict() + currency_settings = frappe.get_cached_doc("Accounts Settings") allow_stale_rates = currency_settings.get("allow_stale") filters = [ diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 3ef0e57c25a..1d44d19ac81 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -35,9 +35,11 @@ class TestBatch(FrappeTestCase): def make_batch_item(cls, item_name=None): from erpnext.stock.doctype.item.test_item import make_item - if not frappe.db.exists(item_name): + if not frappe.db.exists("Item", item_name): return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) + return frappe.get_doc("Item", item_name) + def test_purchase_receipt(self, batch_qty=100): """Test automated batch creation from Purchase Receipt""" self.make_batch_item("ITEM-BATCH-1") @@ -305,8 +307,18 @@ class TestBatch(FrappeTestCase): self.assertEqual( get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"), [ - {"batch_no": "batch a", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"}, - {"batch_no": "batch b", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"}, + { + "batch_no": "batch a", + "qty": 90.0, + "warehouse": "_Test Warehouse - _TC", + "expiry_date": None, + }, + { + "batch_no": "batch b", + "qty": 90.0, + "warehouse": "_Test Warehouse - _TC", + "expiry_date": None, + }, ], ) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 37e2737391a..8aeb56e5d7c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -466,6 +466,8 @@ class DeliveryNote(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) + self.validate_standalone_serial_nos_customer() + # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 8f5d08fe946..5208b947288 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -43,7 +43,7 @@ frappe.ui.form.on("Material Request", { }; }); - frm.set_query("price_list", () => { + frm.set_query("buying_price_list", () => { return { filters: { buying: 1, @@ -79,7 +79,7 @@ frappe.ui.form.on("Material Request", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - frm.doc.price_list = frappe.defaults.get_default("buying_price_list"); + frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list"); }, company: function (frm) { @@ -255,8 +255,8 @@ frappe.ui.form.on("Material Request", { from_warehouse: item.from_warehouse, warehouse: item.warehouse, doctype: frm.doc.doctype, - buying_price_list: frm.doc.price_list - ? frm.doc.price_list + buying_price_list: frm.doc.buying_price_list + ? frm.doc.buying_price_list : frappe.defaults.get_default("buying_price_list"), currency: frappe.defaults.get_default("Currency"), name: frm.doc.name, diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 4079288009a..43cf7a7e527 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -16,7 +16,7 @@ "column_break_2", "transaction_date", "schedule_date", - "price_list", + "buying_price_list", "amended_from", "warehouse_section", "scan_barcode", @@ -354,7 +354,7 @@ "fieldtype": "Column Break" }, { - "fieldname": "price_list", + "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List" @@ -364,7 +364,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-07 13:15:28.615984", + "modified": "2025-07-11 21:03:26.588307", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 8c80f907093..c37a783ca4d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -153,8 +153,8 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") - if not self.price_list: - self.price_list = frappe.defaults.get_defaults().buying_price_list + if not self.buying_price_list: + self.buying_price_list = frappe.defaults.get_defaults().buying_price_list def before_update_after_submit(self): self.validate_schedule_date() diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index b20ac15f136..87174bc7513 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -6,11 +6,11 @@ frappe.listview_settings["Material Request"] = { return [__("Stopped"), "red", "status,=,Stopped"]; } else if (doc.transfer_status && doc.docstatus != 2) { if (doc.transfer_status == "Not Started") { - return [__("Not Started"), "orange"]; + return [__("Not Started"), "orange", "transfer_status,=,Not Started"]; } else if (doc.transfer_status == "In Transit") { - return [__("In Transit"), "yellow"]; + return [__("In Transit"), "yellow", "transfer_status,=,In Transit"]; } else if (doc.transfer_status == "Completed") { - return [__("Completed"), "green"]; + return [__("Completed"), "green", "transfer_status,=,Completed"]; } } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"]; diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 3d3bfc7e010..ceb2fdb0087 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -340,7 +340,7 @@ def on_doctype_update(): @frappe.whitelist() -def get_items_from_product_bundle(row, price_list): +def get_items_from_product_bundle(row): row, items = json.loads(row), [] bundled_items = get_product_bundle_items(row["item_code"]) @@ -350,7 +350,6 @@ def get_items_from_product_bundle(row, price_list): "item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty), "conversion_rate": 1, - "price_list": price_list, "currency": frappe.defaults.get_defaults().currency, } ) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index aae1d4786d0..f554f4bd140 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -116,13 +116,15 @@ class PickList(TransactionBase): continue - bin_qty = frappe.db.get_value( - "Bin", - {"item_code": row.item_code, "warehouse": row.warehouse}, - "actual_qty", + bin_qty = flt( + frappe.db.get_value( + "Bin", + {"item_code": row.item_code, "warehouse": row.warehouse}, + "actual_qty", + ) ) - if row.picked_qty > flt(bin_qty): + if row.picked_qty > bin_qty: frappe.throw( _( "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 404abbd21bc..a250df28350 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -55,14 +55,14 @@ frappe.ui.form.on("Serial and Batch Bundle", { let fields = frm.events.get_prompt_fields(frm); - frm.add_custom_button(__("Make " + label), () => { + frm.add_custom_button(__("Make {0}", [label]), () => { frappe.prompt( fields, (data) => { frm.events.add_serial_batch(frm, data); }, - "Add " + label, - "Make " + label + __("Add {0}", [label]), + __("Make {0}", [label]) ); }); } diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 11697409820..f6162ab558b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -11,11 +11,11 @@ from frappe.model.document import Document from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import ( - add_days, cint, cstr, flt, get_link_to_form, + getdate, now, nowtime, parse_json, @@ -804,7 +804,7 @@ class SerialandBatchBundle(Document): if qty_field == "qty" and row.get("stock_qty"): qty = row.get("stock_qty") - precision = row.precision + precision = row.precision(qty_field) if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row) set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row) @@ -2148,6 +2148,9 @@ def get_auto_batch_nos(kwargs): picked_batches, ) + if kwargs.based_on == "Expiry": + available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31"))) + if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"): filter_zero_near_batches(available_batches, kwargs) @@ -2247,6 +2250,7 @@ def get_available_batches(kwargs): batch_ledger.batch_no, batch_ledger.warehouse, Sum(batch_ledger.qty).as_("qty"), + batch_table.expiry_date, ) .where(batch_table.disabled == 0) .where(stock_ledger_entry.is_cancelled == 0) @@ -2537,6 +2541,7 @@ def get_stock_ledgers_batches(kwargs): stock_ledger_entry.item_code, Sum(stock_ledger_entry.actual_qty).as_("qty"), stock_ledger_entry.batch_no, + batch_table.expiry_date, ) .where((stock_ledger_entry.is_cancelled == 0) & (stock_ledger_entry.batch_no.isnotnull())) .groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse) diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 89c77d46b1e..924277b30ea 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -15,6 +15,7 @@ "batch_no", "warehouse", "purchase_rate", + "customer", "column_break1", "status", "item_name", @@ -267,12 +268,21 @@ "label": "Creation Document No", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "no_copy": 1, + "options": "Customer", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2025-01-15 16:22:49.873889", + "modified": "2025-07-15 13:40:21.938700", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", @@ -310,6 +320,7 @@ "role": "Stock User" } ], + "row_format": "Dynamic", "search_fields": "item_code", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 928313576f1..896323d6529 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -40,6 +40,7 @@ class SerialNo(StockController): batch_no: DF.Link | None brand: DF.Link | None company: DF.Link + customer: DF.Link | None description: DF.Text | None employee: DF.Link | None item_code: DF.Link diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4c3ffc157c6..37c0340d2bf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -435,7 +435,9 @@ class StockEntry(StockController): additional_cost_amt = additional_costs[0][0] if additional_costs else 0 amount += additional_cost_amt - frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount) + project = frappe.get_doc("Project", self.project) + project.total_consumed_material_cost = amount + project.save() def validate_item(self): stock_items = self.get_stock_items() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index fee71460ee5..cf789d5bca2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -1089,17 +1089,14 @@ class StockReconciliation(StockController): } ) - if ( - add_new_sle - and not frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, - "name", - ) - and not row.current_serial_and_batch_bundle + if add_new_sle and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", ): - self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) - row.reload() + if not row.current_serial_and_batch_bundle: + self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) + row.reload() self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 1513f48eb9f..ed76d96fd2e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -120,7 +120,11 @@ class StockSettings(Document): ) def cant_change_valuation_method(self): - previous_valuation_method = self.get_doc_before_save().get("valuation_method") + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + + previous_valuation_method = doc_before_save.get("valuation_method") if previous_valuation_method and previous_valuation_method != self.valuation_method: # check if there are any stock ledger entries against items diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py index 6911b979ae4..c7fd27020c2 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.py +++ b/erpnext/stock/report/available_serial_no/available_serial_no.py @@ -19,21 +19,17 @@ def execute(filters=None): columns = get_columns(filters) items = get_items(filters) sl_entries = get_stock_ledger_entries(filters, items) + + if not sl_entries: + return columns, [] + item_details = get_item_details(items, sl_entries, False) - - opening_row = get_opening_balance_data(filters, columns, sl_entries) - + opening_row = get_opening_balance(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision) - return columns, data -def get_opening_balance_data(filters, columns, sl_entries): - opening_row = get_opening_balance(filters, columns, sl_entries) - return opening_row - - def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision): data = [] diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 9b4520064d6..3193ba3de51 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt, today +from frappe.utils.nestedset import get_descendants_of from pypika.terms import ExistsCriterion from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty @@ -21,6 +22,10 @@ def execute(filters=None): columns = get_columns() bin_list = get_bin_list(filters) item_map = get_item_map(filters.get("item_code"), include_uom) + item_groups = [] + if filters.get("item_group"): + item_groups.append(filters.item_group) + item_groups.extend(get_descendants_of("Item Group", filters.item_group)) warehouse_company = {} data = [] @@ -40,7 +45,7 @@ def execute(filters=None): if filters.brand and filters.brand != item.brand: continue - elif filters.item_group and filters.item_group != item.item_group: + elif item_groups and item.item_group not in item_groups: continue elif filters.company and filters.company != company: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 8d9634db965..0fbf8475103 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -377,6 +377,10 @@ class SerialBatchBundle: ]: status = "Consumed" + customer = None + if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0: + customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer") + sn_table = frappe.qb.DocType("Serial No") query = ( @@ -387,10 +391,11 @@ class SerialBatchBundle: "Active" if warehouse else status - if (sn_table.purchase_document_no != sle.voucher_no and sle.is_cancelled != 1) + if (sn_table.purchase_document_no != sle.voucher_no or sle.is_cancelled != 1) else "Inactive", ) .set(sn_table.company, sle.company) + .set(sn_table.customer, customer) .where(sn_table.name.isin(serial_nos)) ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f742f52daee..8e2319f7bd6 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -919,6 +919,7 @@ class update_entries_after: ) sle.doctype = "Stock Ledger Entry" + sle.modified = now() frappe.get_doc(sle).db_update() if not self.args.get("sle_id") or ( diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index e8bee897840..e9848c88952 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -236,8 +236,11 @@ class SubcontractingOrder(SubcontractingController): return flt(query[0][0]) if query else 0 - def update_reserved_qty_for_subcontracting(self): + def update_reserved_qty_for_subcontracting(self, sco_item_rows=None): for item in self.supplied_items: + if sco_item_rows and item.reference_name not in sco_item_rows: + continue + if item.rm_item_code: stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse) stock_bin.update_reserved_qty_for_sub_contracting() @@ -299,7 +302,7 @@ class SubcontractingOrder(SubcontractingController): self.set_missing_values() - def update_status(self, status=None, update_modified=True): + def update_status(self, status=None, update_modified=True, update_bin=True): if self.status == "Closed" and self.status != status: check_on_hold_or_closed_status("Purchase Order", self.purchase_order) @@ -329,8 +332,9 @@ class SubcontractingOrder(SubcontractingController): self.db_set("status", status, update_modified=update_modified) self.update_requested_qty() - self.update_ordered_qty_for_subcontracting() - self.update_reserved_qty_for_subcontracting() + if update_bin: + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() def update_subcontracted_quantity_in_po(self, cancel=False): for service_item in self.service_items: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 01f5df3b684..00c1c08cd5c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -152,7 +152,7 @@ class SubcontractingReceipt(SubcontractingController): self.validate_available_qty_for_consumption() self.update_status_updater_args() self.update_prevdoc_status() - self.set_subcontracting_order_status() + self.set_subcontracting_order_status(update_bin=False) self.set_consumed_qty_in_subcontract_order() for table_name in ["items", "supplied_items"]: @@ -179,7 +179,7 @@ class SubcontractingReceipt(SubcontractingController): self.update_status_updater_args() self.update_prevdoc_status() self.set_consumed_qty_in_subcontract_order() - self.set_subcontracting_order_status() + self.set_subcontracting_order_status(update_bin=False) self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 7afa06ff9a7..fd4a0dffd8d 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -157,6 +157,8 @@ def link_existing_conversations(doc, state): """ Called from hooks on creation of Contact or Lead to link all the existing conversations. """ + if doc.flags.ignore_auto_link_call_log: + return if doc.doctype != "Contact": return try: @@ -183,12 +185,12 @@ def link_existing_conversations(doc, state): """, dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype), ) - - for log in logs: - call_log = frappe.get_doc("Call Log", log) - call_log.add_link(link_type=doc.doctype, link_name=doc.name) - call_log.save(ignore_permissions=True) - frappe.db.commit() + if logs: + for log in logs: + call_log = frappe.get_doc("Call Log", log) + call_log.add_link(link_type=doc.doctype, link_name=doc.name) + call_log.save(ignore_permissions=True) + frappe.db.commit() except Exception: frappe.log_error(title=_("Error during caller information update"))