diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 02ff1dbf3e9..599a0604755 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -123,8 +123,7 @@ "fieldname": "transaction_id", "fieldtype": "Data", "label": "Transaction ID", - "read_only": 1, - "unique": 1 + "read_only": 1 }, { "allow_on_submit": 1, @@ -239,7 +238,7 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2025-10-14 11:53:45.908169", + "modified": "2025-10-23 17:32:58.514807", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index 25aa3f30363..f97fcf1ec34 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -99,7 +99,7 @@ class FiscalYear(Document): ) overlap = False - if not self.get("companies") or not company_for_existing: + if not self.get("companies") and not company_for_existing: overlap = True for d in self.get("companies"): diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index 8d437164015..9fb78bf41bf 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -26,6 +26,27 @@ class TestFiscalYear(unittest.TestCase): self.assertRaises(frappe.exceptions.InvalidDates, fy.insert) + def test_company_fiscal_year_overlap(self): + for name in ["_Test Global FY 2001", "_Test Company FY 2001"]: + if frappe.db.exists("Fiscal Year", name): + frappe.delete_doc("Fiscal Year", name) + + global_fy = frappe.new_doc("Fiscal Year") + global_fy.year = "_Test Global FY 2001" + global_fy.year_start_date = "2001-04-01" + global_fy.year_end_date = "2002-03-31" + global_fy.insert() + + company_fy = frappe.new_doc("Fiscal Year") + company_fy.year = "_Test Company FY 2001" + company_fy.year_start_date = "2001-01-01" + company_fy.year_end_date = "2001-12-31" + company_fy.append("companies", {"company": "_Test Company"}) + + company_fy.insert() + self.assertTrue(frappe.db.exists("Fiscal Year", global_fy.name)) + self.assertTrue(frappe.db.exists("Fiscal Year", company_fy.name)) + def test_record_generator(): test_records = [ diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index b333e103b59..0b520ecd0e4 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -106,7 +106,6 @@ "fieldname": "account_currency", "fieldtype": "Link", "label": "Account Currency", - "no_copy": 1, "options": "Currency", "print_hide": 1, "read_only": 1 @@ -288,7 +287,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-10-20 17:46:47.344089", + "modified": "2025-10-27 13:48:32.805100", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index c5b815b5e61..75a86318fbb 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -385,6 +385,16 @@ frappe.ui.form.on("Payment Reconciliation Allocation", { // filter payment let payment = frm.doc.payments.filter((x) => x.reference_name == row.reference_name); + let amount = payment[0].amount; + for (const d of frm.doc.allocation) { + if (row.reference_name == d.reference_name && amount) { + if (d.allocated_amount <= amount) { + d.amount = amount; + amount -= d.allocated_amount; + } + } + } + frm.call({ doc: frm.doc, method: "calculate_difference_on_allocation_change", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 7df3bce9b4a..32fda83943b 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -648,6 +648,7 @@ def make_reverse_gl_entries( adv_adj=False, update_outstanding="Yes", partial_cancel=False, + posting_date=None, ): """ Get original gl entries of the voucher @@ -745,6 +746,8 @@ def make_reverse_gl_entries( if immutable_ledger_enabled: new_gle["is_cancelled"] = 0 new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate() + elif posting_date: + new_gle["posting_date"] = posting_date if new_gle["debit"] or new_gle["credit"]: make_entry(new_gle, adv_adj, "Yes") diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index c196d52d744..57c81712cc1 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -273,6 +273,7 @@ def get_asset_value_adjustment_map_by_category(filters): AND a.company = %(company)s AND a.purchase_date <= %(to_date)s AND gle.account = aca.fixed_asset_account + AND gle.is_opening = 'No' GROUP BY a.asset_category """, {"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company}, @@ -543,6 +544,7 @@ def get_asset_value_adjustment_map(filters): AND a.company = %(company)s AND a.purchase_date <= %(to_date)s AND gle.account = aca.fixed_asset_account + AND gle.is_opening = 'No' GROUP BY a.name """, {"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company}, diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 367fe4f4fd2..baf2da6ceea 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -231,6 +231,15 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_ group_columns = group_wise_columns.get(scrub(filters.group_by)) + # removing customer_name from group columns + customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") + supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") + + if "customer_name" in group_columns and ( + supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name" + ): + group_columns = [col for col in group_columns if col != "customer_name"] + for src in gross_profit_data.grouped_data: total_base_amount += src.base_amount or 0.00 total_buying_amount += src.buying_amount or 0.00 diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 8e75762a92d..487f67669ff 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -518,6 +518,7 @@ "read_only": 1 }, { + "default": "0", "depends_on": "eval:doc.docstatus > 0", "fieldname": "additional_asset_cost", "fieldtype": "Currency", @@ -596,7 +597,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2025-05-20 00:44:06.229177", + "modified": "2025-10-23 22:43:33.634452", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -641,4 +642,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e35ebf6ed06..929a60d0e6d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -69,6 +69,7 @@ class Asset(AccountsController): default_finance_book: DF.Link | None department: DF.Link | None depr_entry_posting_status: DF.Literal["", "Successful", "Failed"] + depreciation_completed: DF.Check depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"] disposal_date: DF.Date | None finance_books: DF.Table[AssetFinanceBook] @@ -152,7 +153,9 @@ class Asset(AccountsController): ) self.validate_expected_value_after_useful_life() self.set_total_booked_depreciations() - self.total_asset_cost = self.gross_purchase_amount + + def before_save(self): + self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost self.status = self.get_status() def on_submit(self): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fb5cc130f0b..643fafb74ab 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -878,6 +878,7 @@ class BuyingController(SubcontractingController): "asset_category": item_data.get("asset_category"), "location": row.asset_location, "company": self.company, + "status": "Draft", "supplier": self.supplier, "purchase_date": self.posting_date, "calculate_depreciation": 0, diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a1422bcb265..b7fcb45b7ec 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -724,6 +724,9 @@ class ProductionPlan(Document): if not wo_list: frappe.msgprint(_("No Work Orders were created")) + if not po_list: + frappe.msgprint(_("No Purchase Orders were created")) + def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 97c85502c98..de6dec9ebb8 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -26,7 +26,6 @@ def get_exploded_items(bom, data, indent=0, qty=1): ) for item in exploded_items: - print(item.bom_no, indent) item["indent"] = indent data.append( { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0abf1b42ee4..4e05b974d16 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -422,3 +422,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_fo erpnext.patches.v15_0.add_company_payment_gateway_account erpnext.patches.v15_0.update_uae_zero_rated_fetch erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter +erpnext.patches.v15_0.set_asset_status_if_not_already_set diff --git a/erpnext/patches/v15_0/set_asset_status_if_not_already_set.py b/erpnext/patches/v15_0/set_asset_status_if_not_already_set.py new file mode 100644 index 00000000000..ac0af708de4 --- /dev/null +++ b/erpnext/patches/v15_0/set_asset_status_if_not_already_set.py @@ -0,0 +1,13 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + Asset = DocType("Asset") + + query = ( + frappe.qb.update(Asset) + .set(Asset.status, "Draft") + .where((Asset.docstatus == 0) & ((Asset.status.isnull()) | (Asset.status == ""))) + ) + query.run() diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index cc9832b5845..8ff7fabc23e 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -245,6 +245,7 @@ "fieldname": "act_start_date", "fieldtype": "Date", "label": "Actual Start Date (via Timesheet)", + "no_copy": 1, "oldfieldname": "act_start_date", "oldfieldtype": "Date", "read_only": 1 @@ -253,6 +254,7 @@ "fieldname": "actual_time", "fieldtype": "Float", "label": "Actual Time in Hours (via Timesheet)", + "no_copy": 1, "read_only": 1 }, { @@ -263,6 +265,7 @@ "fieldname": "act_end_date", "fieldtype": "Date", "label": "Actual End Date (via Timesheet)", + "no_copy": 1, "oldfieldname": "act_end_date", "oldfieldtype": "Date", "read_only": 1 @@ -277,6 +280,7 @@ "fieldname": "total_costing_amount", "fieldtype": "Currency", "label": "Total Costing Amount (via Timesheet)", + "no_copy": 1, "oldfieldname": "actual_budget", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -290,6 +294,7 @@ "fieldname": "total_billing_amount", "fieldtype": "Currency", "label": "Total Billable Amount (via Timesheet)", + "no_copy": 1, "read_only": 1 }, { @@ -399,7 +404,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2024-01-08 16:00:41.296203", + "modified": "2025-10-16 08:39:12.214577", "modified_by": "Administrator", "module": "Projects", "name": "Task", @@ -429,4 +434,4 @@ "timeline_field": "project", "title_field": "subject", "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js index ad90d049659..0f9427cd520 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js @@ -3,6 +3,12 @@ frappe.query_reports["Delayed Tasks Summary"] = { filters: [ + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project", + }, { fieldname: "from_date", label: __("From Date"), diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index dc3da259662..fe47cf7541e 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -23,6 +23,7 @@ def get_data(filters): fields=[ "name", "subject", + "project", "exp_start_date", "exp_end_date", "status", @@ -56,7 +57,7 @@ def get_data(filters): def get_conditions(filters): conditions = frappe._dict() - keys = ["priority", "status"] + keys = ["priority", "status", "project"] for key in keys: if filters.get(key): conditions[key] = filters.get(key) @@ -89,6 +90,13 @@ def get_columns(): columns = [ {"fieldname": "name", "fieldtype": "Link", "label": _("Task"), "options": "Task", "width": 150}, {"fieldname": "subject", "fieldtype": "Data", "label": _("Subject"), "width": 200}, + { + "fieldname": "project", + "fieldtype": "Link", + "label": _("Project"), + "options": "Project", + "width": 150, + }, {"fieldname": "status", "fieldtype": "Data", "label": _("Status"), "width": 100}, {"fieldname": "priority", "fieldtype": "Data", "label": _("Priority"), "width": 80}, {"fieldname": "progress", "fieldtype": "Data", "label": _("Progress (%)"), "width": 120}, diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index c7b08f1dc15..dec0f1c024d 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -237,10 +237,10 @@ erpnext.accounts.pos = { frappe.ui.form.on(doctype, { mode_of_payment: function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - get_payment_mode_account(frm, d.mode_of_payment, function(account){ - frappe.model.set_value(cdt, cdn, 'account', account) - }) - } + erpnext.accounts.pos.get_payment_mode_account(frm, d.mode_of_payment, function (account) { + frappe.model.set_value(cdt, cdn, "account", account); + }); + }, }); }, diff --git a/erpnext/setup/doctype/driver/driver.js b/erpnext/setup/doctype/driver/driver.js index 0bd80edb921..35f8bff5874 100644 --- a/erpnext/setup/doctype/driver/driver.js +++ b/erpnext/setup/doctype/driver/driver.js @@ -24,6 +24,7 @@ frappe.ui.form.on("Driver", { transporter: function (frm, cdt, cdn) { // this assumes that supplier's address has same title as supplier's name + if (!frm.doc.transporter) return; frappe.db .get_doc("Address", null, { address_title: frm.doc.transporter }) .then((r) => { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 4b28f6094c1..332f05fafd2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -539,7 +539,7 @@ frappe.ui.form.on("Stock Entry", { const item = locals[cdt][cdn]; item.transfer_qty = flt(item.qty) * flt(item.conversion_factor); - const args = { + let args = { item_code: item.item_code, posting_date: frm.doc.posting_date, posting_time: frm.doc.posting_time, @@ -553,6 +553,10 @@ frappe.ui.form.on("Stock Entry", { allow_zero_valuation: 1, }; + if (item.batch_no && frm.doc.purpose == "Material Receipt") { + args.qty = Math.abs(args.qty) * -1; + } + if (item.item_code || item.serial_no) { frappe.call({ method: "erpnext.stock.utils.get_incoming_rate", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index d8dd2a7560a..450bc01a67c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -7,7 +7,10 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { setup(frm) { frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; - frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm }); + frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ + frm: frm, + uom_field: "stock_uom", + }); }, onload: function (frm) { diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js index dccb543115e..eb146a7b447 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js @@ -24,24 +24,26 @@ frappe.query_reports["Incorrect Serial and Batch Bundle"] = { }, onload(report) { - report.page.add_inner_button(__("Remove SABB Entry"), () => { - let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map((i) => frappe.query_report.data[i]); + report.page + .add_inner_button(__("Fix SABB Entry"), () => { + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map((i) => frappe.query_report.data[i]); - if (!selected_rows.length) { - frappe.throw(__("Please select a row to create a Reposting Entry")); - } else { - frappe.call({ - method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.remove_sabb_entry", - freeze: true, - args: { - selected_rows: selected_rows, - }, - callback: function (r) { - frappe.query_report.refresh(); - }, - }); - } - }); + if (!selected_rows.length) { + frappe.throw(__("Please select at least one row to fix")); + } else { + frappe.call({ + method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.fix_sabb_entries", + freeze: true, + args: { + selected_rows: selected_rows, + }, + callback: function (r) { + frappe.query_report.refresh(); + }, + }); + } + }) + .addClass("btn-primary"); }, }; diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py index e65725f3c3d..0b27d697a4d 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py @@ -13,7 +13,10 @@ def execute(filters: dict | None = None): every time the report is refreshed or a filter is updated. """ columns = get_columns() - data = get_data(filters) + unlinked_bundles = get_unlinked_serial_batch_bundles(filters) or [] + linked_cancelled_bundles = get_linked_cancelled_sabb(filters) or [] + + data = unlinked_bundles + linked_cancelled_bundles return columns, data @@ -50,14 +53,17 @@ def get_columns() -> list[dict]: "fieldtype": "Data", "width": 200, }, + { + "label": _("Is Cancelled"), + "fieldname": "is_cancelled", + "fieldtype": "Check", + "width": 200, + }, ] -def get_data(filters) -> list[list]: - """Return data for the report. - - The report data is a list of rows, with each row being a list of cell values. - """ +def get_unlinked_serial_batch_bundles(filters) -> list[list]: + # SABB has not been linked to any SLE SABB = frappe.qb.DocType("Serial and Batch Bundle") SLE = frappe.qb.DocType("Stock Ledger Entry") @@ -77,6 +83,7 @@ def get_data(filters) -> list[list]: SABB.voucher_type, SABB.voucher_no, SABB.voucher_detail_no, + SABB.is_cancelled, ) .where( (SLE.serial_and_batch_bundle.isnull()) @@ -94,14 +101,63 @@ def get_data(filters) -> list[list]: return data +def get_linked_cancelled_sabb(filters): + # SABB has cancelled but voucher is not cancelled + + SABB = frappe.qb.DocType("Serial and Batch Bundle") + SLE = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(SABB) + .inner_join(SLE) + .on(SABB.name == SLE.serial_and_batch_bundle) + .select( + SABB.name, + SABB.voucher_type, + SABB.voucher_no, + SABB.voucher_detail_no, + SABB.is_cancelled, + ) + .where( + (SLE.serial_and_batch_bundle.isnotnull()) + & (SABB.docstatus == 2) + & (SABB.is_cancelled == 1) + & (SLE.is_cancelled == 0) + ) + ) + + for field in filters: + query = query.where(SABB[field] == filters[field]) + + data = query.run(as_dict=1) + return data + + @frappe.whitelist() -def remove_sabb_entry(selected_rows): +def fix_sabb_entries(selected_rows): if isinstance(selected_rows, str): selected_rows = frappe.parse_json(selected_rows) for row in selected_rows: doc = frappe.get_doc("Serial and Batch Bundle", row.get("name")) - doc.cancel() - doc.delete() + if doc.is_cancelled == 0 and not frappe.db.get_value( + "Stock Ledger Entry", + {"serial_and_batch_bundle": doc.name, "is_cancelled": 0}, + "name", + ): + doc.db_set({"is_cancelled": 1, "docstatus": 2}) - frappe.msgprint(_("Selected Serial and Batch Bundle entries have been removed.")) + for row in doc.entries: + row.db_set("docstatus", 2) + + elif doc.is_cancelled == 1 and frappe.db.get_value( + "Stock Ledger Entry", + {"serial_and_batch_bundle": doc.name, "is_cancelled": 0}, + "name", + ): + doc.db_set({"is_cancelled": 0, "docstatus": 1}) + + for row in doc.entries: + row.db_set("docstatus", 1) + + frappe.msgprint(_("Selected Serial and Batch Bundle entries have been fixed.")) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index 4d740bd829a..d3c5d2e8db5 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -18,16 +18,41 @@ def execute(filters=None): def get_data(filters): data = get_stock_ledger_entries(filters) - serial_nos_data = prepare_serial_nos(data) + bundles = get_bundles(data) + serial_nos_data = prepare_serial_nos(data, bundles) data = get_incorrect_serial_nos(serial_nos_data) return data -def prepare_serial_nos(data): +def get_bundles(data): + bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle] + bundle_dict = frappe._dict() + serial_nos_data = frappe.get_all( + "Serial and Batch Entry", + fields=["parent", "serial_no", "incoming_rate", "qty"], + filters={"parent": ("in", bundles), "serial_no": ("is", "set")}, + ) + + for entry in serial_nos_data: + bundle_dict.setdefault(entry.parent, []).append(entry) + + return bundle_dict + + +def prepare_serial_nos(data, bundles): serial_no_wise_data = {} for row in data: - if not row.serial_nos: + if not row.serial_nos and not row.serial_and_batch_bundle: + continue + + if row.serial_and_batch_bundle: + for bundle in bundles.get(row.serial_and_batch_bundle, []): + sle = copy.deepcopy(row) + sle.serial_no = bundle.serial_no + sle.qty = bundle.qty + sle.valuation_rate = bundle.incoming_rate * (1 if sle.qty > 0 else -1) + serial_no_wise_data.setdefault(bundle.serial_no, []).append(sle) continue for serial_no in get_serial_nos(row.serial_nos): @@ -54,6 +79,9 @@ def get_incorrect_serial_nos(serial_nos_data): total_value.qty += total_dict.qty total_value.valuation_rate += total_dict.valuation_rate + if total_dict.qty == 0 and abs(total_dict.valuation_rate) == 0: + continue + result.append(total_dict) result.append({}) @@ -81,6 +109,7 @@ def get_stock_ledger_entries(report_filters): "voucher_no", "item_code", "serial_no as serial_nos", + "serial_and_batch_bundle", "actual_qty", "posting_date", "posting_time", @@ -89,7 +118,8 @@ def get_stock_ledger_entries(report_filters): "(stock_value_difference / actual_qty) as valuation_rate", ] - filters = {"serial_no": ("is", "set"), "is_cancelled": 0} + filters = {"is_cancelled": 0} + or_filters = {"serial_no": ("is", "set"), "serial_and_batch_bundle": ("is", "set")} if report_filters.get("item_code"): filters["item_code"] = report_filters.get("item_code") @@ -104,6 +134,7 @@ def get_stock_ledger_entries(report_filters): "Stock Ledger Entry", fields=fields, filters=filters, + or_filters=or_filters, order_by="posting_date asc, posting_time asc, creation asc", ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index fbd30075be3..890899c7343 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -731,13 +731,55 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) - self.total_qty[ledger.batch_no] += flt(ledger.total_qty) + + entries = self.get_batch_wise_total_available_qty() + for row in entries: + self.total_qty[row.batch_no] += flt(row.total_qty) self.calculate_avg_rate_from_deprecarated_ledgers() self.calculate_avg_rate_for_non_batchwise_valuation() self.set_stock_value_difference() + def get_batch_wise_total_available_qty(self) -> list[dict]: + # Get total qty of each batch no from Serial and Batch Bundle without checking time condition + if not self.batchwise_valuation_batches: + return [] + + parent = frappe.qb.DocType("Serial and Batch Bundle") + child = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) + .select( + child.batch_no, + Sum(child.qty).as_("total_qty"), + ) + .where( + (parent.warehouse == self.sle.warehouse) + & (parent.item_code == self.sle.item_code) + & (child.batch_no.isin(self.batchwise_valuation_batches)) + & (parent.docstatus == 1) + & (parent.is_cancelled == 0) + & (parent.type_of_transaction.isin(["Inward", "Outward"])) + ) + .for_update() + .groupby(child.batch_no) + ) + + # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference + if self.sle.voucher_detail_no: + query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no) + elif self.sle.voucher_no: + query = query.where(parent.voucher_no != self.sle.voucher_no) + + query = query.where(parent.voucher_type != "Pick List") + + return query.run(as_dict=True) + def get_batch_no_ledgers(self) -> list[dict]: + # Get batch wise stock value difference from Serial and Batch Bundle considering time condition if not self.batchwise_valuation_batches: return [] @@ -765,11 +807,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation): .on(parent.name == child.parent) .select( child.batch_no, - Sum(Case().when(timestamp_condition, child.stock_value_difference).else_(0)).as_( - "incoming_rate" - ), - Sum(Case().when(timestamp_condition, child.qty).else_(0)).as_("qty"), - Sum(child.qty).as_("total_qty"), + Sum(child.stock_value_difference).as_("incoming_rate"), + Sum(child.qty).as_("qty"), ) .where( (parent.warehouse == self.sle.warehouse) @@ -790,6 +829,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation): query = query.where(parent.voucher_no != self.sle.voucher_no) query = query.where(parent.voucher_type != "Pick List") + if timestamp_condition: + query = query.where(timestamp_condition) return query.run(as_dict=True) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 54616149b37..2df4b257aa5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -893,7 +893,11 @@ class update_entries_after: sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference - if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0: + if ( + sle.is_adjustment_entry + and flt(sle.qty_after_transaction, self.flt_precision) == 0 + and flt(sle.stock_value, self.currency_precision) != 0 + ): sle.stock_value_difference = ( get_stock_value_difference( sle.item_code, @@ -901,6 +905,7 @@ class update_entries_after: sle.posting_date, sle.posting_time, voucher_detail_no=sle.voucher_detail_no, + creation=sle.creation, ) * -1 ) @@ -2358,7 +2363,7 @@ def is_internal_transfer(sle): def get_stock_value_difference( - item_code, warehouse, posting_date, posting_time, voucher_no=None, voucher_detail_no=None + item_code, warehouse, posting_date, posting_time, voucher_no=None, voucher_detail_no=None, creation=None ): table = frappe.qb.DocType("Stock Ledger Entry") posting_datetime = get_combine_datetime(posting_date, posting_time) @@ -2366,12 +2371,7 @@ def get_stock_value_difference( query = ( frappe.qb.from_(table) .select(Sum(table.stock_value_difference).as_("value")) - .where( - (table.is_cancelled == 0) - & (table.item_code == item_code) - & (table.warehouse == warehouse) - & (table.posting_datetime <= posting_datetime) - ) + .where((table.is_cancelled == 0) & (table.item_code == item_code) & (table.warehouse == warehouse)) ) if voucher_detail_no: @@ -2380,6 +2380,14 @@ def get_stock_value_difference( elif voucher_no: query = query.where(table.voucher_no != voucher_no) + if creation: + query = query.where( + (table.posting_datetime < posting_datetime) + | ((table.posting_datetime == posting_datetime) & (table.creation < creation)) + ) + else: + query = query.where(table.posting_datetime <= posting_datetime) + difference_amount = query.run() return flt(difference_amount[0][0]) if difference_amount else 0