diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index c77870d0886..fe2b806a76b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -576,6 +576,7 @@ class PaymentReconciliation(Document): "difference_amount": flt(row.get("difference_amount")), "difference_account": row.get("difference_account"), "difference_posting_date": row.get("gain_loss_posting_date"), + "debit_or_credit_note_posting_date": row.get("debit_or_credit_note_posting_date"), "cost_center": row.get("cost_center"), } ) @@ -765,7 +766,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None): { "doctype": "Journal Entry", "voucher_type": voucher_type, - "posting_date": today(), + "posting_date": inv.get("debit_or_credit_note_posting_date") or today(), "company": company, "multi_currency": 1 if inv.currency != company_currency else 0, "accounts": [ diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 3f85b213500..e4430de5049 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -20,6 +20,7 @@ "section_break_5", "difference_amount", "gain_loss_posting_date", + "debit_or_credit_note_posting_date", "column_break_7", "difference_account", "exchange_rate", @@ -168,12 +169,17 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "debit_or_credit_note_posting_date", + "fieldtype": "Date", + "label": "Debit / Credit Note Posting Date" } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-12-14 13:38:26.104150", + "modified": "2025-08-20 19:12:50.406769", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", @@ -183,4 +189,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py index b57ebecbac2..125ae28f3f5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py @@ -18,6 +18,7 @@ class PaymentReconciliationAllocation(Document): amount: DF.Currency cost_center: DF.Link | None currency: DF.Link | None + debit_or_credit_note_posting_date: DF.Date | None difference_account: DF.Link | None difference_amount: DF.Currency exchange_rate: DF.Float diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 0e97d6969bd..7df3bce9b4a 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_f ) from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -from erpnext.accounts.utils import create_payment_ledger_entry +from erpnext.accounts.utils import create_payment_ledger_entry, is_immutable_ledger_enabled from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError @@ -838,7 +838,3 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map): ), InvalidAccountDimensionError, ) - - -def is_immutable_ledger_enabled(): - return frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger") diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index baeaca2f354..15117b2aa43 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1743,40 +1743,38 @@ def create_err_and_its_journals(companies: list | None = None) -> None: jv and frappe.get_doc("Journal Entry", jv).submit() +def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None: + """ + Internal helper to avoid code duplication and typos. + Fetches companies by frequency and triggers ERR. + """ + companies = frappe.db.get_all( + "Company", + filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": frequency}, + fields=["name", "submit_err_jv"], + ) + create_err_and_its_journals(companies) + + def auto_create_exchange_rate_revaluation_daily() -> None: """ Executed by background job """ - companies = frappe.db.get_all( - "Company", - filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Daily"}, - fields=["name", "submit_err_jv"], - ) - create_err_and_its_journals(companies) + _auto_create_exchange_rate_revaluation_for("Daily") def auto_create_exchange_rate_revaluation_weekly() -> None: """ Executed by background job """ - companies = frappe.db.get_all( - "Company", - filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Weekly"}, - fields=["name", "submit_err_jv"], - ) - create_err_and_its_journals(companies) + _auto_create_exchange_rate_revaluation_for("Weekly") def auto_create_exchange_rate_revaluation_monthly() -> None: """ Executed by background job """ - companies = frappe.db.get_all( - "Company", - filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Montly"}, - fields=["name", "submit_err_jv"], - ) - create_err_and_its_journals(companies) + _auto_create_exchange_rate_revaluation_for("Monthly") def get_payment_ledger_entries(gl_entries, cancel=0): @@ -1897,6 +1895,9 @@ def create_payment_ledger_entry( if cancel: delink_original_entry(ple, partial_cancel=partial_cancel) + if is_immutable_ledger_enabled(): + ple.delinked = 0 + ple.posting_date = frappe.form_dict.get("posting_date") or getdate() ple.flags.ignore_permissions = 1 ple.flags.adv_adj = adv_adj @@ -1984,7 +1985,6 @@ def delink_original_entry(pl_entry, partial_cancel=False): ple = qb.DocType("Payment Ledger Entry") query = ( qb.update(ple) - .set(ple.delinked, True) .set(ple.modified, now()) .set(ple.modified_by, frappe.session.user) .where( @@ -2003,6 +2003,9 @@ def delink_original_entry(pl_entry, partial_cancel=False): if partial_cancel: query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) + if not is_immutable_ledger_enabled(): + query = query.set(ple.delinked, True) + query.run() @@ -2459,3 +2462,7 @@ def build_qb_match_conditions(doctype, user=None) -> list: criterion.append(cond) return criterion + + +def is_immutable_ledger_enabled(): + return frappe.get_single_value("Accounts Settings", "enable_immutable_ledger") diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 04994acc6ab..e35ebf6ed06 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -322,6 +322,9 @@ class Asset(AccountsController): finance_books = get_item_details(self.item_code, self.asset_category, self.gross_purchase_amount) self.set("finance_books", finance_books) + if self.asset_owner == "Company" and not self.asset_owner_company: + self.asset_owner_company = self.company + def validate_finance_books(self): if not self.calculate_depreciation or len(self.finance_books) == 1: return diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index e1413096af3..1e3c1ffc598 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -456,7 +456,7 @@ class AssetDepreciationSchedule(Document): continue depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) value_after_depreciation = flt( - value_after_depreciation - flt(depreciation_amount), + flt(value_after_depreciation) - flt(depreciation_amount), asset_doc.precision("gross_purchase_amount"), ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index dca3285f538..d82a5e18c23 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -599,6 +599,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( docstatus: 1, status: ["not in", ["Stopped", "Expired"]], }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "rate", "amount"], }); }, __("Get Items From") diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index b74474be89e..0a316bd2974 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -64,6 +64,11 @@ frappe.ui.form.on("Supplier", { }, }; }); + + frm.make_methods = { + "Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name), + "Pricing Rule": () => erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name), + }; }, refresh: function (frm) { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 5557f1a80ae..35cb2eebf8f 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc @@ -235,7 +237,12 @@ def get_list_context(context=None): @frappe.whitelist() -def make_purchase_order(source_name, target_doc=None): +def make_purchase_order(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + def set_missing_values(source, target): target.run_method("set_missing_values") target.run_method("get_schedule_dates") @@ -244,6 +251,11 @@ def make_purchase_order(source_name, target_doc=None): def update_item(obj, target, source_parent): target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + doclist = get_mapped_doc( "Supplier Quotation", source_name, @@ -265,6 +277,7 @@ def make_purchase_order(source_name, target_doc=None): ["sales_order", "sales_order"], ], "postprocess": update_item, + "condition": select_item, }, "Purchase Taxes and Charges": { "doctype": "Purchase Taxes and Charges", diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index e41ac0dd0ab..15c2760332e 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -108,7 +108,7 @@ status_map = { ["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"], [ "Ordered", - "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'", + "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']", ], [ "Transferred", @@ -134,10 +134,6 @@ status_map = { "Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'", ], - [ - "Manufactured", - "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'", - ], ], "POS Opening Entry": [ ["Draft", None], diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index e2613465dd3..a6b8513298c 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -462,7 +462,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2025-07-03 10:54:30.444139", + "modified": "2025-08-21 17:57:58.314809", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -512,6 +512,7 @@ "row_format": "Dynamic", "search_fields": "project_name,customer, status, priority, is_active", "show_name_in_global_search": 1, + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 79d85c20a0b..e3680549cc0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -551,6 +551,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item_code(doc, cdt, cdn) { var me = this; + frappe.flags.dialog_set = false; + var item = frappe.get_doc(cdt, cdn); var update_stock = 0, show_batch_dialog = 0; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 1cf3275a530..2b6d41c2d54 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -209,17 +209,9 @@ $.extend(erpnext.utils, { }, make_bank_account: function (doctype, docname) { - frappe.call({ - method: "erpnext.accounts.doctype.bank_account.bank_account.make_bank_account", - args: { - doctype: doctype, - docname: docname, - }, - freeze: true, - callback: function (r) { - var doclist = frappe.model.sync(r.message); - frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - }, + frappe.new_doc("Bank Account", { + party_type: doctype, + party: docname, }); }, diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index 0719e5ed99f..b8d1b4afa2a 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -113,12 +113,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.flags.trigger_from_barcode_scanner = true; const { item_code, barcode, batch_no, serial_no, uom, default_warehouse } = data; - - const warehouse = this.has_last_scanned_warehouse - ? this.frm.doc.last_scanned_warehouse || default_warehouse - : null; - - let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse); + let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, default_warehouse); const is_new_row = !row?.item_code; if (!row) { if (this.dont_allow_new_row) { @@ -151,7 +146,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_serial_no(row, serial_no), () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), - () => this.set_warehouse(row, warehouse), + () => this.set_warehouse(row), () => this.clean_up(), () => this.revert_selector_flag(), () => resolve(row), @@ -412,12 +407,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - async set_warehouse(row, warehouse) { - const warehouse_field = this.get_warehouse_field(); + async set_warehouse(row) { + if (!this.has_last_scanned_warehouse) return; - if (warehouse && frappe.meta.has_field(row.doctype, warehouse_field)) { - await frappe.model.set_value(row.doctype, row.name, warehouse_field, warehouse); - } + const last_scanned_warehouse = this.frm.doc.last_scanned_warehouse; + if (!last_scanned_warehouse) return; + + const warehouse_field = this.get_warehouse_field(); + if (!warehouse_field || !frappe.meta.has_field(row.doctype, warehouse_field)) return; + + await frappe.model.set_value(row.doctype, row.name, warehouse_field, last_scanned_warehouse); } show_scan_message(idx, is_existing_row = false, qty = 1) { @@ -438,15 +437,19 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return is_duplicate; } - get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse) { + get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, default_warehouse) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; // Check if batch is scanned and table has batch no field let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field); let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field); - const warehouse_field = this.get_warehouse_field(); - let has_warehouse_field = frappe.meta.has_field(cur_grid.doctype, warehouse_field); + const warehouse_field = this.has_last_scanned_warehouse && this.get_warehouse_field(); + const has_warehouse_field = + warehouse_field && frappe.meta.has_field(cur_grid.doctype, warehouse_field); + const warehouse = has_warehouse_field + ? this.frm.doc.last_scanned_warehouse || default_warehouse + : null; const matching_row = (row) => { const item_match = row.item_code == item_code; @@ -509,7 +512,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { handle_warehouse_scan(data) { const warehouse = data.warehouse; const warehouse_field = this.get_warehouse_field(); - const warehouse_field_label = frappe.meta.get_label(this.items_table_name, warehouse_field); + const cur_grid = this.frm.fields_dict[this.items_table_name].grid; + const warehouse_field_label = frappe.meta.get_label(cur_grid.doctype, warehouse_field); if (!this.last_scanned_warehouse_initialized) { this.setup_last_scanned_warehouse(); diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 11a3eecb967..aa8bea55094 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -14,6 +14,7 @@ frappe.ui.form.on("Customer", { method: "erpnext.selling.doctype.customer.customer.make_opportunity", frm: cur_frm, }), + "Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name), }; frm.add_fetch("lead_name", "company_name", "customer_name"); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 75260a5a3fa..0118e89cd3c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1144,6 +1144,17 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a target.debit_to = get_party_account("Customer", source.customer, source.company) def update_item(source, target, source_parent): + def get_billed_qty(so_item_name): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Sales Invoice Item") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty).as_("qty")) + .where((table.docstatus == 1) & (table.so_detail == so_item_name)) + ) + return query.run(pluck="qty")[0] or 0 + if source_parent.has_unit_price_items: # 0 Amount rows (as seen in Unit Price Items) should be mapped as it is pending_amount = flt(source.amount) - flt(source.billed_amt) @@ -1153,8 +1164,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a target.base_amount = target.amount * flt(source_parent.conversion_rate) target.qty = ( - target.amount / flt(source.rate) - if (source.rate and source.billed_amt) + source.qty - get_billed_qty(source.name) + if (source.qty and source.billed_amt) else (source.qty if is_unit_price_row(source) else source.qty - source.returned_qty) ) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ae807e5b1c0..04a3a989cae 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1830,7 +1830,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): wo.reload() so.reload() self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) - self.assertEqual(mr.status, "Manufactured") + self.assertEqual(mr.status, "Ordered") @change_settings( "Accounts Settings", @@ -2396,6 +2396,30 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertFalse(so.per_billed) self.assertEqual(so.status, "To Deliver and Bill") + def test_pending_quantity_after_update_item_during_invoice_creation(self): + so = make_sales_order(qty=30, rate=100) + + si1 = make_sales_invoice(so.name) + si1.get("items")[0].qty = 10 + si1.insert() + si1.submit() + + first_item_of_so = so.get("items")[0] + trans_item = json.dumps( + [ + { + "item_code": first_item_of_so.item_code, + "rate": 1000, + "qty": first_item_of_so.qty, + "docname": first_item_of_so.name, + }, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) + + si2 = make_sales_invoice(so.name) + self.assertEqual(si2.items[0].qty, 20) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 7a1efa82fa0..2e45d5511b3 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -21,6 +21,12 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo }; frappe.ui.form.on("Employee", { + setup: function (frm) { + frm.make_methods = { + "Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name), + }; + }, + onload: function (frm) { frm.set_query("department", function () { return { diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index ff928ea6a22..69d8c15803d 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -35,7 +35,7 @@ frappe.listview_settings["Material Request"] = { return [__("Partially Received"), "yellow", "per_received,<,100"]; } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) { return [__("Received"), "green", "per_received,=,100"]; - } else if (doc.material_request_type == "Purchase") { + } else if (["Purchase", "Manufacture"].includes(doc.material_request_type)) { return [__("Ordered"), "green", "per_ordered,=,100"]; } else if (doc.material_request_type == "Material Transfer") { return [__("Transfered"), "green", "per_ordered,=,100"]; @@ -43,8 +43,6 @@ frappe.listview_settings["Material Request"] = { return [__("Issued"), "green", "per_ordered,=,100"]; } else if (doc.material_request_type == "Customer Provided") { return [__("Received"), "green", "per_ordered,=,100"]; - } else if (doc.material_request_type == "Manufacture") { - return [__("Manufactured"), "green", "per_ordered,=,100"]; } } }, diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 30b2d7fcdc2..66dd9c0c795 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -866,6 +866,23 @@ class TestMaterialRequest(FrappeTestCase): for perm in permissions: perm.delete() + def test_manufacture_type_status_over_wo(self): + from erpnext.stock.doctype.material_request.material_request import raise_work_orders + + mr = make_material_request( + item_code="_Test FG Item", material_request_type="Manufacture", do_not_submit=False + ) + + work_order = raise_work_orders(mr.name) + wo = frappe.get_doc("Work Order", work_order[0]) + wo.wip_warehouse = "_Test Warehouse 1 - _TC" + wo.submit() + + mr.reload() + + self.assertEqual(mr.per_ordered, 100) + self.assertEqual(mr.status, "Ordered") + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1c4d28c495e..bc7023fbebb 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1322,8 +1322,8 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): "postprocess": update_item, "filter": lambda d: ( get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 - ) - and select_item(d), + ), + "condition": select_item, }, "Purchase Taxes and Charges": { "doctype": "Purchase Taxes and Charges", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index a3bd20b80ba..5fdc3d56ebd 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -608,7 +608,7 @@ class TestSerialandBatchBundle(FrappeTestCase): def test_serial_no_valuation_for_legacy_ledgers(self): sn_item = make_item( "Test Serial No Valuation for Legacy Ledgers", - properties={"has_serial_no": 1, "serial_no_series": "SNN-TSNVL.-#####"}, + properties={"has_serial_no": 1, "serial_no_series": "SNN-TSNVL-.#####"}, ).name serial_nos = [] diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e31ba966653..c912c529b22 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1277,17 +1277,33 @@ def get_pos_profile(company, pos_profile=None, user=None): @frappe.whitelist() def get_conversion_factor(item_code, uom): - variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True) - filters = {"parent": item_code, "uom": uom} + item = frappe.get_cached_value("Item", item_code, ["variant_of", "stock_uom"], as_dict=True) + if not item_code or not item or uom == item.stock_uom: + return {"conversion_factor": 1.0} + + item_codes = [item_code] + if item.variant_of: + item_codes.append(item.variant_of) + + parent = frappe.qb.DocType("Item") + child = frappe.qb.DocType("UOM Conversion Detail") + query = ( + frappe.qb.from_(parent) + .join(child) + .on(parent.name == child.parent) + .select(child.conversion_factor) + .where((parent.name.isin(item_codes)) & (child.uom == uom)) + .orderby(parent.has_variants) + .limit(1) + ) + conversion_factor = query.run(pluck="conversion_factor") - if variant_of: - filters["parent"] = ("in", (item_code, variant_of)) - conversion_factor = frappe.get_all("UOM Conversion Detail", filters, pluck="conversion_factor") if not conversion_factor: - stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") - conversion_factor = [get_uom_conv_factor(uom, stock_uom) or 1] + conversion_factor = get_uom_conv_factor(uom, item.stock_uom) + else: + conversion_factor = conversion_factor[0] - return {"conversion_factor": conversion_factor[-1]} + return {"conversion_factor": conversion_factor or 1.0} @frappe.whitelist() diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index c53e80096d3..b59ee57b154 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -61,10 +61,10 @@ frappe.query_reports["Stock Balance"] = { }, }); - data = data.map(({ name, description }) => { + data = data.map(({ name, ...rest }) => { return { value: name, - description: description, + description: Object.values(rest), }; }); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index db6c0c06281..9b85a83c03b 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -56,11 +56,10 @@ frappe.query_reports["Stock Ledger"] = { as_dict: 1, }, }); - - data = data.map(({ name, description }) => { + data = data.map(({ name, ...rest }) => { return { value: name, - description: description, + description: Object.values(rest), }; });