diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66efc178b1c..5f0abc70c5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Entire Repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index 2bd6c12a0a3..260bcd11db5 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "accounting_dimension", + "fieldname", "disabled", "column_break_2", "company", @@ -90,11 +91,17 @@ "fieldname": "apply_restriction_on_values", "fieldtype": "Check", "label": "Apply restriction on dimension values" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "hidden": 1, + "label": "Fieldname" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-07 14:59:41.869117", + "modified": "2025-08-08 14:13:22.203011", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", @@ -139,8 +146,8 @@ } ], "quick_entry": 1, - "sort_field": "modified", + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 7c843cf552e..040583a2847 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -17,17 +17,16 @@ class AccountingDimensionFilter(Document): from frappe.types import DF from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension - from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ( - ApplicableOnAccount, - ) + from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount - accounting_dimension: DF.Literal + accounting_dimension: DF.Literal[None] accounts: DF.Table[ApplicableOnAccount] allow_or_restrict: DF.Literal["Allow", "Restrict"] apply_restriction_on_values: DF.Check company: DF.Link dimensions: DF.Table[AllowedDimension] disabled: DF.Check + fieldname: DF.Data | None # end: auto-generated types def before_save(self): @@ -37,6 +36,10 @@ class AccountingDimensionFilter(Document): self.set("dimensions", []) def validate(self): + self.fieldname = frappe.db.get_value( + "Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname" + ) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension + self.validate_applicable_accounts() def validate_applicable_accounts(self): @@ -72,7 +75,7 @@ def get_dimension_filter_map(): """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, a.is_mandatory + p.allow_or_restrict, p.fieldname, a.is_mandatory FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` p @@ -87,8 +90,6 @@ def get_dimension_filter_map(): dimension_filter_map = {} for f in filters: - f.fieldname = scrub(f.accounting_dimension) - build_map( dimension_filter_map, f.fieldname, diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index e7dab34d04a..9c4f2f8fd49 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -462,9 +462,8 @@ def unset_existing_data(company): "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", ]: - frappe.db.sql( - f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec - ) + dt = frappe.qb.DocType(doctype) + frappe.qb.from_(dt).where(dt.company == company).delete().run() def set_default_accounts(company): diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 00ed85a4e0b..0f5831a967d 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -11,6 +11,7 @@ -> Resolves dunning automatically """ + import json import frappe @@ -156,40 +157,66 @@ class Dunning(AccountsController): ] -def resolve_dunning(doc, state): - """ - Check if all payments have been made and resolve dunning, if yes. Called - when a Payment Entry is submitted. - """ - for reference in doc.references: - # Consider partial and full payments: - # Submitting full payment: outstanding_amount will be 0 - # Submitting 1st partial payment: outstanding_amount will be the pending installment - # Cancelling full payment: outstanding_amount will revert to total amount - # Cancelling last partial payment: outstanding_amount will revert to pending amount - submit_condition = reference.outstanding_amount < reference.total_amount - cancel_condition = reference.outstanding_amount <= reference.total_amount +def update_linked_dunnings(doc, previous_outstanding_amount): + if ( + doc.doctype != "Sales Invoice" + or doc.is_return + or previous_outstanding_amount == doc.outstanding_amount + ): + return - if reference.reference_doctype == "Sales Invoice" and ( - submit_condition if doc.docstatus == 1 else cancel_condition - ): - state = "Resolved" if doc.docstatus == 2 else "Unresolved" - dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state) + to_resolve = doc.outstanding_amount < previous_outstanding_amount + state = "Unresolved" if to_resolve else "Resolved" + dunnings = get_linked_dunnings_as_per_state(doc.name, state) + if not dunnings: + return - for dunning in dunnings: - resolve = True - dunning = frappe.get_doc("Dunning", dunning.get("name")) - for overdue_payment in dunning.overdue_payments: - outstanding_inv = frappe.get_value( - "Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount" - ) - outstanding_ps = frappe.get_value( - "Payment Schedule", overdue_payment.payment_schedule, "outstanding" - ) - resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True + dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings] + invoices = set() + payment_schedule_ids = set() - dunning.status = "Resolved" if resolve else "Unresolved" - dunning.save() + for dunning in dunnings: + for overdue_payment in dunning.overdue_payments: + invoices.add(overdue_payment.sales_invoice) + if overdue_payment.payment_schedule: + payment_schedule_ids.add(overdue_payment.payment_schedule) + + invoice_outstanding_amounts = dict( + frappe.get_all( + "Sales Invoice", + filters={"name": ["in", list(invoices)]}, + fields=["name", "outstanding_amount"], + as_list=True, + ) + ) + + ps_outstanding_amounts = ( + dict( + frappe.get_all( + "Payment Schedule", + filters={"name": ["in", list(payment_schedule_ids)]}, + fields=["name", "outstanding"], + as_list=True, + ) + ) + if payment_schedule_ids + else {} + ) + + for dunning in dunnings: + has_outstanding = False + for overdue_payment in dunning.overdue_payments: + invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice] + ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0) + has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0 + if has_outstanding: + break + + new_status = "Resolved" if not has_outstanding else "Unresolved" + + if dunning.status != new_status: + dunning.status = new_status + dunning.save() def get_linked_dunnings_as_per_state(sales_invoice, state): diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index ad45b882035..4fe8e7bf9f8 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -139,6 +139,64 @@ class TestDunning(FrappeTestCase): self.assertEqual(sales_invoice.status, "Overdue") self.assertEqual(dunning.status, "Unresolved") + def test_dunning_resolution_from_credit_note(self): + """ + Test that dunning is resolved when a credit note is issued against the original invoice. + """ + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=add_days(today(), -10), qty=1, rate=100 + ) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning.submit() + + self.assertEqual(dunning.status, "Unresolved") + + credit_note = frappe.copy_doc(sales_invoice) + credit_note.is_return = 1 + credit_note.return_against = sales_invoice.name + credit_note.update_outstanding_for_self = 0 + + for item in credit_note.items: + item.qty = -item.qty + + credit_note.save() + credit_note.submit() + + dunning.reload() + self.assertEqual(dunning.status, "Resolved") + + credit_note.cancel() + dunning.reload() + self.assertEqual(dunning.status, "Unresolved") + + def test_dunning_not_affected_by_standalone_credit_note(self): + """ + Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked. + """ + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=add_days(today(), -10), qty=1, rate=100 + ) + dunning = create_dunning_from_sales_invoice(sales_invoice.name) + dunning.submit() + + self.assertEqual(dunning.status, "Unresolved") + + credit_note = frappe.copy_doc(sales_invoice) + credit_note.is_return = 1 + credit_note.return_against = sales_invoice.name + credit_note.update_outstanding_for_self = 1 + + for item in credit_note.items: + item.qty = -item.qty + + credit_note.save() + + credit_note = frappe.get_doc("Sales Invoice", credit_note.name) + credit_note.submit() + + dunning.reload() + self.assertEqual(dunning.status, "Unresolved") + def create_dunning(overdue_days, dunning_type_name=None): posting_date = add_days(today(), -1 * overdue_days) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index 1e844afc4b0..f46d94baa35 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import flt, today @@ -45,22 +46,30 @@ def get_loyalty_details( if not expiry_date: expiry_date = today() - condition = "" - if company: - condition = " and company=%s " % frappe.db.escape(company) - if not include_expired_entry: - condition += " and expiry_date>='%s' " % expiry_date + LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry") - loyalty_point_details = frappe.db.sql( - f"""select sum(loyalty_points) as loyalty_points, - sum(purchase_amount) as total_spent from `tabLoyalty Point Entry` - where customer=%s and loyalty_program=%s and posting_date <= %s - {condition} - group by customer""", - (customer, loyalty_program, expiry_date), - as_dict=1, + query = ( + frappe.qb.from_(LoyaltyPointEntry) + .select( + Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"), + Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"), + ) + .where( + (LoyaltyPointEntry.customer == customer) + & (LoyaltyPointEntry.loyalty_program == loyalty_program) + & (LoyaltyPointEntry.posting_date <= expiry_date) + ) + .groupby(LoyaltyPointEntry.customer) ) + if company: + query = query.where(LoyaltyPointEntry.company == company) + + if not include_expired_entry: + query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date) + + loyalty_point_details = query.run(as_dict=True) + if loyalty_point_details: return loyalty_point_details[0] else: diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b034e48e3ec..cc0da8e7b09 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -118,9 +118,9 @@ class PaymentEntry(AccountsController): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.update_payment_requests() + self.update_payment_schedule() self.make_gl_entries() self.update_outstanding_amounts() - self.update_payment_schedule() self.set_status() def validate_for_repost(self): @@ -221,10 +221,10 @@ class PaymentEntry(AccountsController): ) super().on_cancel() self.update_payment_requests(cancel=True) + self.update_payment_schedule(cancel=1) self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.delink_advance_entry_references() - self.update_payment_schedule(cancel=1) self.set_status() def update_payment_requests(self, cancel=False): diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 2f170631226..b392e8045b5 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -55,6 +55,16 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex }); erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); + + if (this.frm.doc.pos_profile) { + frappe.db + .get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop") + .then((r) => { + if (!r.exc) { + this.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop; + } + }); + } } onload_post_render(frm) { @@ -113,6 +123,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex this.frm.meta.default_print_format = r.message.print_format || ""; this.frm.doc.campaign = r.message.campaign; this.frm.allow_print_before_pay = r.message.allow_print_before_pay; + this.frm.skip_default_payment = r.message.skip_default_payment; } this.frm.script_manager.trigger("update_stock"); this.calculate_taxes_and_totals(); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b96e29979a8..5870cd9c9da 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -62,6 +62,7 @@ "items_section", "update_stock", "scan_barcode", + "last_scanned_warehouse", "items", "pricing_rule_details", "pricing_rules", @@ -1569,6 +1570,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index b0c69be4a1c..87360543efd 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -216,6 +216,7 @@ class POSInvoice(SalesInvoice): self.validate_loyalty_transaction() self.validate_company_with_pos_company() self.validate_full_payment() + self.update_packing_list() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -367,9 +368,9 @@ class POSInvoice(SalesInvoice): ) elif is_stock_item and flt(available_stock) < flt(d.stock_qty): frappe.throw( - _( - "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." - ).format(d.idx, item_code, warehouse, available_stock), + _("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format( + d.idx, item_code, warehouse + ), title=_("Item Unavailable"), ) @@ -680,6 +681,7 @@ class POSInvoice(SalesInvoice): "print_format": print_format, "campaign": profile.get("campaign"), "allow_print_before_pay": profile.get("allow_print_before_pay"), + "skip_default_payment": profile.get("disable_grand_total_to_default_mop"), } @frappe.whitelist() @@ -774,10 +776,8 @@ def get_bundle_availability(bundle_item_code, warehouse): bundle_bin_qty = 1000000 for item in product_bundle.items: item_bin_qty = get_bin_qty(item.item_code, warehouse) - item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) - available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.qty + max_available_bundles = item_bin_qty / item.qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): @@ -800,13 +800,49 @@ def get_bin_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse): + """ + Calculate total quantity reserved for the given item and warehouse. + + Includes: + - Direct sales of the item in submitted POS Invoices + - Sales of the item as a component of a Product Bundle + + Excludes consolidated invoices (already merged into Sales Invoices via + POS Closing Entry). Used to reflect near real-time availability in the + POS UI and to prevent overselling while multiple sessions may be active. + """ + pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse) + packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse) + + reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty + + return reserved_qty + + +def get_pos_reserved_qty_from_table(child_table, item_code, warehouse): + """ + Get the total reserved quantity for a given item in POS Invoices + from a specific child table. + + Args: + child_table (str): Name of the child table to query + (e.g., "POS Invoice Item", "Packed Item"). + item_code (str): The Item Code to filter by. + warehouse (str): The Warehouse to filter by. + + Returns: + float: The total reserved quantity for the item in the given + warehouse from submitted, unconsolidated POS Invoices. + """ p_inv = frappe.qb.DocType("POS Invoice") - p_item = frappe.qb.DocType("POS Invoice Item") + p_item = frappe.qb.DocType(child_table) + + qty_column = "qty" if child_table == "Packed Item" else "stock_qty" reserved_qty = ( frappe.qb.from_(p_inv) .from_(p_item) - .select(Sum(p_item.stock_qty).as_("stock_qty")) + .select(Sum(p_item[qty_column]).as_("stock_qty")) .where( (p_inv.name == p_item.parent) & (IfNull(p_inv.consolidated_invoice, "") == "") diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5bf15ac325b..17fad2eca43 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -153,6 +153,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. per_billed: ["<", 99.99], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") @@ -175,6 +178,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. company: me.frm.doc.company, is_return: 0, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 83e704734ce..ca31f1de4ec 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -47,6 +47,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "col_break_warehouse", "update_stock", "set_warehouse", @@ -1644,6 +1645,13 @@ "label": "Select Dispatch Address ", "options": "Address", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index abbdd5523c8..4f0d6a64c36 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _, qb, throw from frappe.model.mapper import get_mapped_doc @@ -2070,7 +2072,12 @@ def make_inter_company_sales_invoice(source_name, target_doc=None): @frappe.whitelist() -def make_purchase_receipt(source_name, target_doc=None): +def make_purchase_receipt(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.received_qty) target.received_qty = flt(obj.qty) - flt(obj.received_qty) @@ -2080,6 +2087,11 @@ def make_purchase_receipt(source_name, target_doc=None): (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) ) + 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 + doc = get_mapped_doc( "Purchase Invoice", source_name, @@ -2103,7 +2115,7 @@ def make_purchase_receipt(source_name, target_doc=None): "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"}, }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index db9083a9f91..a19850af8f1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -58,6 +58,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( me.frm.script_manager.trigger("is_pos"); me.frm.refresh_fields(); + frappe.db + .get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop") + .then((r) => { + if (!r.exc) { + me.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop; + } + }); } erpnext.queries.setup_warehouse_query(this.frm); } @@ -259,6 +266,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( per_billed: ["<", 99.99], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") @@ -288,6 +298,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( status: ["!=", "Lost"], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "rate", "amount"], }); }, __("Get Items From") @@ -319,6 +332,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( filters: filters, }; }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"], }); }, __("Get Items From") @@ -497,8 +513,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( }, callback: function (r) { if (!r.exc) { - if (r.message && r.message.print_format) { + if (r.message) { me.frm.pos_print_format = r.message.print_format; + me.frm.skip_default_payment = r.message.skip_default_payment; } me.frm.trigger("update_stock"); if (me.frm.doc.taxes_and_charges) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 07b97920ae2..816a6bfeded 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -45,6 +45,7 @@ "items_section", "scan_barcode", "update_stock", + "last_scanned_warehouse", "column_break_39", "set_warehouse", "set_target_warehouse", @@ -2177,6 +2178,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 386d28fd804..2de3feb9a35 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -700,6 +700,7 @@ class SalesInvoice(SellingController): "allow_edit_discount": pos.get("allow_user_to_edit_discount"), "campaign": pos.get("campaign"), "allow_print_before_pay": pos.get("allow_print_before_pay"), + "skip_default_payment": pos.get("disable_grand_total_to_default_mop"), } def update_time_sheet(self, sales_invoice): 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 cdeddf3d38b..d7884b3e973 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 @@ -25,17 +25,25 @@ def get_group_by_asset_category_data(filters): asset_categories = get_asset_categories_for_grouped_by_category(filters) assets = get_assets_for_grouped_by_category(filters) + asset_value_adjustment_map = get_asset_value_adjustment_map_by_category(filters) for asset_category in asset_categories: row = frappe._dict() row.update(asset_category) + adjustments = asset_value_adjustment_map.get(asset_category.get("asset_category"), {}) + row.adjustment_before_from_date = flt(adjustments.get("adjustment_before_from_date", 0)) + row.adjustment_till_to_date = flt(adjustments.get("adjustment_till_to_date", 0)) + row.adjustment_during_period = row.adjustment_till_to_date - row.adjustment_before_from_date + + row.value_as_on_from_date += row.adjustment_before_from_date row.value_as_on_to_date = ( flt(row.value_as_on_from_date) + flt(row.value_of_new_purchase) - flt(row.value_of_sold_asset) - flt(row.value_of_scrapped_asset) - flt(row.value_of_capitalized_asset) + + flt(row.adjustment_during_period) ) row.update( @@ -229,26 +237,93 @@ def get_assets_for_grouped_by_category(filters): ) +def get_asset_value_adjustment_map_by_category(filters): + asset_value_adjustments = frappe.db.sql( + """ + SELECT + a.asset_category AS asset_category, + IFNULL( + SUM( + CASE + WHEN gle.posting_date < %(from_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_before_from_date, + IFNULL( + SUM( + CASE + WHEN gle.posting_date <= %(to_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_till_to_date + + FROM `tabGL Entry` gle + JOIN `tabAsset` a ON gle.against_voucher = a.name + JOIN `tabAsset Category Account` aca + ON aca.parent = a.asset_category + AND aca.company_name = %(company)s + WHERE gle.is_cancelled = 0 + AND a.docstatus = 1 + AND a.company = %(company)s + AND a.purchase_date <= %(to_date)s + AND gle.account = aca.fixed_asset_account + GROUP BY a.asset_category + """, + {"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company}, + as_dict=1, + ) + + category_value_adjustment_map = {} + + for r in asset_value_adjustments: + category_value_adjustment_map[r["asset_category"]] = { + "adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)), + "adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)), + } + + return category_value_adjustment_map + + def get_group_by_asset_data(filters): data = [] asset_details = get_asset_details_for_grouped_by_category(filters) assets = get_assets_for_grouped_by_asset(filters) + asset_value_adjustment_map = get_asset_value_adjustment_map(filters) for asset_detail in asset_details: row = frappe._dict() row.update(asset_detail) + row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) + adjustments = asset_value_adjustment_map.get( + asset_detail.get("name", ""), + { + "adjustment_before_from_date": 0.0, + "adjustment_till_to_date": 0.0, + }, + ) + row.adjustment_before_from_date = adjustments["adjustment_before_from_date"] + row.adjustment_till_to_date = adjustments["adjustment_till_to_date"] + row.adjustment_during_period = flt(row.adjustment_till_to_date) - flt(row.adjustment_before_from_date) + + row.value_as_on_from_date += row.adjustment_before_from_date + row.value_as_on_to_date = ( flt(row.value_as_on_from_date) + flt(row.value_of_new_purchase) - flt(row.value_of_sold_asset) - flt(row.value_of_scrapped_asset) - flt(row.value_of_capitalized_asset) + + flt(row.adjustment_during_period) ) - row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) - row.accumulated_depreciation_as_on_to_date = ( flt(row.accumulated_depreciation_as_on_from_date) + flt(row.depreciation_amount_during_the_period) @@ -432,6 +507,59 @@ def get_assets_for_grouped_by_asset(filters): ) +def get_asset_value_adjustment_map(filters): + asset_with_value_adjustments = frappe.db.sql( + """ + SELECT + a.name AS asset, + IFNULL( + SUM( + CASE + WHEN gle.posting_date < %(from_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_before_from_date, + IFNULL( + SUM( + CASE + WHEN gle.posting_date <= %(to_date)s + AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_till_to_date + + FROM `tabGL Entry` gle + JOIN `tabAsset` a ON gle.against_voucher = a.name + JOIN `tabAsset Category Account` aca + ON aca.parent = a.asset_category + AND aca.company_name = %(company)s + WHERE gle.is_cancelled = 0 + AND a.docstatus = 1 + AND a.company = %(company)s + AND a.purchase_date <= %(to_date)s + AND gle.account = aca.fixed_asset_account + GROUP BY a.name + """, + {"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company}, + as_dict=1, + ) + + asset_value_adjustment_map = {} + + for r in asset_with_value_adjustments: + asset_value_adjustment_map[r["asset"]] = { + "adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)), + "adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)), + } + + return asset_value_adjustment_map + + def get_columns(filters): columns = [] diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 4ff8ed2d432..172f322de22 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -1,29 +1,34 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-12-06 13:22:23", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:17:51.995451", - "modified_by": "Administrator", - "module": "Accounts", - "name": "General Ledger", - "owner": "Administrator", - "ref_doctype": "GL Entry", - "report_name": "General Ledger", - "report_type": "Script Report", + "add_total_row": 1, + "add_translate_data": 0, + "columns": [], + "creation": "2013-12-06 13:22:23", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-08-13 12:47:27.645023", + "modified_by": "Administrator", + "module": "Accounts", + "name": "General Ledger", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "General Ledger", + "report_type": "Script Report", "roles": [ { "role": "Accounts User" - }, + }, { "role": "Accounts Manager" - }, + }, { "role": "Auditor" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 073efdadb6a..d6ffd3db5ac 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -355,7 +355,13 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None): query = query.where(si.posting_date <= filters.get("to_date")) if filters.get("mode_of_payment"): - query = query.where(sip.mode_of_payment == filters.get("mode_of_payment")) + subquery = ( + frappe.qb.from_(sip) + .select(sip.parent) + .where(sip.mode_of_payment == filters.get("mode_of_payment")) + .groupby(sip.parent) + ) + query = query.where(si.name.isin(subquery)) if filters.get("warehouse"): if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): @@ -424,8 +430,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None): frappe.qb.from_(si) .join(sii) .on(si.name == sii.parent) - .left_join(sip) - .on(sip.parent == si.name) .left_join(item) .on(sii.item_code == item.name) .select( @@ -465,7 +469,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None): si.update_stock, sii.uom, sii.qty, - sip.mode_of_payment, ) .where(si.docstatus == 1) .where(sii.parenttype == doctype) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 25c31b9deaa..baeaca2f354 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1906,6 +1906,8 @@ def create_payment_ledger_entry( def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party): + from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings + if not voucher_type or not voucher_no: return @@ -1938,6 +1940,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa ): outstanding = voucher_outstanding[0] ref_doc = frappe.get_doc(voucher_type, voucher_no) + previous_outstanding_amount = ref_doc.outstanding_amount outstanding_amount = flt( outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount") ) @@ -1951,6 +1954,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa outstanding_amount, ) + update_linked_dunnings(ref_doc, previous_outstanding_amount) ref_doc.set_status(update=True) ref_doc.notify_update() diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 79e266d14c2..04994acc6ab 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -122,6 +122,7 @@ class Asset(AccountsController): def validate(self): self.validate_category() self.validate_precision() + self.validate_linked_purchase_docs() self.set_purchase_doc_row_item() self.validate_asset_values() self.validate_asset_and_reference() @@ -409,6 +410,21 @@ class Asset(AccountsController): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) + def validate_linked_purchase_docs(self): + for doctype_field, doctype_name in [ + ("purchase_receipt", "Purchase Receipt"), + ("purchase_invoice", "Purchase Invoice"), + ]: + linked_doc = getattr(self, doctype_field, None) + if linked_doc: + docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus") + if docstatus == 0: + frappe.throw( + _("{0} is still in Draft. Please submit it before saving the Asset.").format( + get_link_to_form(doctype_name, linked_doc) + ) + ) + def validate_gross_and_purchase_amount(self): if self.is_existing_asset: return @@ -1083,7 +1099,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non def make_journal_entry(asset_name): asset = frappe.get_doc("Asset", asset_name) ( - _, + fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account, ) = get_depreciation_accounts(asset.asset_category, asset.company) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 5bdc9900abf..dca3285f538 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -463,8 +463,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( if (internal) { let button_label = me.frm.doc.company === me.frm.doc.represents_company - ? "Internal Sales Order" - : "Inter Company Sales Order"; + ? __("Internal Sales Order") + : __("Inter Company Sales Order"); me.frm.add_custom_button( button_label, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 59b44a22e61..cfea482d217 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -41,8 +41,9 @@ "ignore_pricing_rule", "before_items_section", "scan_barcode", - "set_from_warehouse", + "last_scanned_warehouse", "items_col_break", + "set_from_warehouse", "set_warehouse", "items_section", "items", @@ -1294,6 +1295,13 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, @@ -1301,7 +1309,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-04-09 16:54:08.836106", + "modified": "2025-07-31 17:19:40.816883", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 246690c3e39..0f4d7cf496c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -724,7 +724,12 @@ def set_missing_values(source, target): @frappe.whitelist() -def make_purchase_receipt(source_name, target_doc=None): +def make_purchase_receipt(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items") def is_unit_price_row(source): @@ -738,6 +743,11 @@ def make_purchase_receipt(source_name, target_doc=None): (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) ) + 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 + doc = get_mapped_doc( "Purchase Order", source_name, @@ -765,7 +775,8 @@ def make_purchase_receipt(source_name, target_doc=None): "condition": lambda doc: ( True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty) ) - and doc.delivered_by_supplier != 1, + and doc.delivered_by_supplier != 1 + and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, }, @@ -777,8 +788,8 @@ def make_purchase_receipt(source_name, target_doc=None): @frappe.whitelist() -def make_purchase_invoice(source_name, target_doc=None): - return get_mapped_purchase_invoice(source_name, target_doc) +def make_purchase_invoice(source_name, target_doc=None, args=None): + return get_mapped_purchase_invoice(source_name, target_doc, args=args) @frappe.whitelist() @@ -792,7 +803,12 @@ def make_purchase_invoice_from_portal(purchase_order_name): frappe.response.location = "/purchase-invoices/" + doc.name -def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False): +def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) @@ -832,6 +848,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions or item_group.get("buying_cost_center") ) + 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 + fields = { "Purchase Order": { "doctype": "Purchase Invoice", @@ -854,7 +875,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), + "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, } diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 27793236dc3..d11424b555f 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.core.doctype.communication.email import make from frappe.desk.form.load import get_attachments from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Order from frappe.utils import get_url from frappe.utils.print_format import download_pdf from frappe.utils.user import get_user_fullname @@ -582,35 +583,32 @@ def get_supplier_tag(): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters): - conditions = "" - if txt: - conditions += "and rfq.name like '%%" + txt + "%%' " + rfq = frappe.qb.DocType("Request for Quotation") + rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier") - if filters.get("transaction_date"): - conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date")) - - rfq_data = frappe.db.sql( - f""" - select - distinct rfq.name, rfq.transaction_date, - rfq.company - from - `tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier - where - rfq.name = rfq_supplier.parent - and rfq_supplier.supplier = %(supplier)s - and rfq.docstatus = 1 - and rfq.company = %(company)s - {conditions} - order by rfq.transaction_date ASC - limit %(page_len)s offset %(start)s """, - { - "page_len": page_len, - "start": start, - "company": filters.get("company"), - "supplier": filters.get("supplier"), - }, - as_dict=1, + query = ( + frappe.qb.from_(rfq) + .from_(rfq_supplier) + .select(rfq.name) + .distinct() + .select(rfq.transaction_date, rfq.company) + .where( + (rfq.name == rfq_supplier.parent) + & (rfq_supplier.supplier == filters.get("supplier")) + & (rfq.docstatus == 1) + & (rfq.company == filters.get("company")) + ) + .orderby(rfq.transaction_date, order=Order.asc) + .limit(page_len) + .offset(start) ) + if txt: + query = query.where(rfq.name.like(f"%%{txt}%%")) + + if filters.get("transaction_date"): + query = query.where(rfq.transaction_date == filters.get("transaction_date")) + + rfq_data = query.run(as_dict=1) + return rfq_data diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 17141751ca1..55e04a2e262 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -583,21 +583,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql( - """select distinct bo.name, bo.blanket_order_type, bo.to_date - from `tabBlanket Order` bo, `tabBlanket Order Item` boi - where - boi.parent = bo.name - and boi.item_code = {item_code} - and bo.blanket_order_type = '{blanket_order_type}' - and bo.company = {company} - and bo.docstatus = 1""".format( - item_code=frappe.db.escape(filters.get("item")), - blanket_order_type=filters.get("blanket_order_type"), - company=frappe.db.escape(filters.get("company")), + bo = frappe.qb.DocType("Blanket Order") + bo_item = frappe.qb.DocType("Blanket Order Item") + + blanket_orders = ( + frappe.qb.from_(bo) + .from_(bo_item) + .select(bo.name) + .distinct() + .select(bo.blanket_order_type, bo.to_date) + .where( + (bo_item.parent == bo.name) + & (bo_item.item_code == filters.get("item")) + & (bo.blanket_order_type == filters.get("blanket_order_type")) + & (bo.company == filters.get("company")) + & (bo.docstatus == 1) ) + .run() ) + return blanket_orders + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -615,7 +621,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): if filters.get("company"): condition += "and tabAccount.company = %(company)s" - condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}" + condition += " and tabAccount.disabled = %(disabled)s" return frappe.db.sql( f"""select tabAccount.name from `tabAccount` @@ -625,7 +631,11 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): and tabAccount.`{searchfield}` LIKE %(txt)s {condition} {get_match_cond(doctype)} order by idx desc, name""", - {"txt": "%" + txt + "%", "company": filters.get("company", "")}, + { + "txt": "%" + txt + "%", + "company": filters.get("company", ""), + "disabled": cint(filters.get("disabled", 0)), + }, ) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2369c39f508..8c2b4db3fc9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1854,6 +1854,7 @@ def make_bundle_for_material_transfer(**kwargs): row.warehouse = kwargs.warehouse + bundle_doc.set_incoming_rate() bundle_doc.calculate_qty_and_amount() bundle_doc.flags.ignore_permissions = True bundle_doc.flags.ignore_validate = True diff --git a/erpnext/hooks.py b/erpnext/hooks.py index db9408b0f7e..a4df5628497 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -363,7 +363,9 @@ doc_events = { "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", ], - "on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"], + "on_cancel": [ + "erpnext.regional.italy.utils.sales_invoice_on_cancel", + ], "on_trash": "erpnext.regional.check_deletion_permission", }, "Purchase Invoice": { @@ -375,9 +377,7 @@ doc_events = { "Payment Entry": { "on_submit": [ "erpnext.regional.create_transaction_log", - "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", ], - "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_trash": "erpnext.regional.check_deletion_permission", }, "Address": { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1087f64276d..ae52e1e3a09 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -420,3 +420,4 @@ erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08- execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) 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 diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 98be0204518..3def5922c07 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -48,6 +48,7 @@ def execute(): dunning.validate() dunning.flags.ignore_validate_update_after_submit = True + dunning.flags.ignore_links = True dunning.save() # Reverse entries only if dunning is submitted and not resolved diff --git a/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py new file mode 100644 index 00000000000..25a35bc87fc --- /dev/null +++ b/erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py @@ -0,0 +1,36 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + default_accounting_dimension() + ADF = DocType("Accounting Dimension Filter") + AD = DocType("Accounting Dimension") + + accounting_dimension_filter = ( + frappe.qb.from_(ADF) + .join(AD) + .on(AD.document_type == ADF.accounting_dimension) + .select(ADF.name, AD.fieldname, ADF.accounting_dimension) + ).run(as_dict=True) + + for doc in accounting_dimension_filter: + value = doc.fieldname or frappe.scrub(doc.accounting_dimension) + frappe.db.set_value( + "Accounting Dimension Filter", + doc.name, + "fieldname", + value, + update_modified=False, + ) + + +def default_accounting_dimension(): + ADF = DocType("Accounting Dimension Filter") + for dim in ("Cost Center", "Project"): + ( + frappe.qb.update(ADF) + .set(ADF.fieldname, frappe.scrub(dim)) + .where(ADF.accounting_dimension == dim) + .run() + ) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 1dcee64e54f..c4ddbcaa8b1 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -342,12 +342,16 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to @frappe.whitelist() def get_timesheet_detail_rate(timelog, currency): - timelog_detail = frappe.db.sql( - f"""SELECT tsd.billing_amount as billing_amount, - ts.currency as currency FROM `tabTimesheet Detail` tsd - INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent - WHERE tsd.name = '{timelog}'""", - as_dict=1, + ts = frappe.qb.DocType("Timesheet") + ts_detail = frappe.qb.DocType("Timesheet Detail") + + timelog_detail = ( + frappe.qb.from_(ts_detail) + .inner_join(ts) + .on(ts.name == ts_detail.parent) + .select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency")) + .where(ts_detail.name == timelog) + .run(as_dict=1) )[0] if timelog_detail.currency: diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index be503a45d56..fe0110ce2cb 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -931,8 +931,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { set_default_payment(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; - if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { - + if ( + this.frm.doc.is_pos + && !cint(this.frm.skip_default_payment) + && (update_paid_amount===undefined || update_paid_amount) + ) { $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { let base_amount, amount; diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c9c589b6599..79d85c20a0b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -6,6 +6,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup() { super.setup(); let me = this; + this.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm }); this.set_fields_onload_for_line_item(); this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; @@ -473,8 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe scan_barcode() { frappe.flags.dialog_set = false; - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.process_scan(); + this.barcode_scanner.process_scan(); } barcode(doc, cdt, cdn) { @@ -923,8 +923,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return; } - var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier"; - var party_name = me.frm.doc[party_type.toLowerCase()]; + var party_type, party_name; + if( me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer"){ + party_type = "Customer", + party_name = me.frm.doc.party_name + } + else{ + party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier"; + party_name = me.frm.doc[party_type.toLowerCase()]; + } if (party_name) { frappe.call({ method: "frappe.client.get_value", diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index d6ef7944cee..0719e5ed99f 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -12,6 +12,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.batch_no_field = opts.batch_no_field || "batch_no"; this.uom_field = opts.uom_field || "uom"; this.qty_field = opts.qty_field || "qty"; + this.warehouse_field = opts.warehouse_field || "warehouse"; // field name on row which defines max quantity to be scanned e.g. picklist this.max_qty_field = opts.max_qty_field; // scanner won't add a new row if this flag is set. @@ -20,7 +21,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.prompt_qty = opts.prompt_qty; this.items_table_name = opts.items_table_name || "items"; - this.items_table = this.frm.doc[this.items_table_name]; // optional sound name to play when scan either fails or passes. // see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds @@ -34,8 +34,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch_no: "LOT12", // present if batch was scanned // serial_no: "987XYZ", // present if serial no was scanned // uom: "Kg", // present if barcode UOM is different from default + // warehouse: "Store-001", // present if warehouse was found (location-first scanning) // } this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode"; + this.has_last_scanned_warehouse = frappe.meta.has_field(this.frm.doctype, "last_scanned_warehouse"); } process_scan() { @@ -50,14 +52,31 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.scan_api_call(input, (r) => { const data = r && r.message; - if (!data || Object.keys(data).length === 0) { - this.show_alert(__("Cannot find Item with this Barcode"), "red"); + if ( + !data || + Object.keys(data).length === 0 || + (data.warehouse && !this.has_last_scanned_warehouse) + ) { + this.show_alert( + this.has_last_scanned_warehouse + ? __("Cannot find Item or Warehouse with this Barcode") + : __("Cannot find Item with this Barcode"), + "red" + ); this.clean_up(); this.play_fail_sound(); reject(); return; } + // Handle warehouse scanning + if (data.warehouse) { + this.handle_warehouse_scan(data); + this.play_success_sound(); + resolve(); + return; + } + me.update_table(data) .then((row) => { this.play_success_sound(); @@ -77,6 +96,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { method: this.scan_api, args: { search_value: input, + ctx: { + set_warehouse: this.frm.doc.set_warehouse, + company: this.frm.doc.company, + }, }, }) .then((r) => { @@ -89,11 +112,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; frappe.flags.trigger_from_barcode_scanner = true; - const { item_code, barcode, batch_no, serial_no, uom } = data; + const { item_code, barcode, batch_no, serial_no, uom, default_warehouse } = data; - let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode); + const warehouse = this.has_last_scanned_warehouse + ? this.frm.doc.last_scanned_warehouse || default_warehouse + : null; - this.is_new_row = false; + let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse); + const is_new_row = !row?.item_code; if (!row) { if (this.dont_allow_new_row) { this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); @@ -101,7 +127,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { reject(); return; } - this.is_new_row = true; // add new row if new item/batch is scanned row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); @@ -120,12 +145,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_selector_trigger_flag(data), () => this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => { - this.show_scan_message(row.idx, row.item_code, qty); + this.show_scan_message(row.idx, !is_new_row, qty); }), () => this.set_barcode_uom(row, uom), () => 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.clean_up(), () => this.revert_selector_flag(), () => resolve(row), @@ -386,9 +412,17 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - show_scan_message(idx, exist = null, qty = 1) { + async set_warehouse(row, warehouse) { + const warehouse_field = this.get_warehouse_field(); + + if (warehouse && frappe.meta.has_field(row.doctype, warehouse_field)) { + await frappe.model.set_value(row.doctype, row.name, warehouse_field, warehouse); + } + } + + show_scan_message(idx, is_existing_row = false, qty = 1) { // show new row or qty increase toast - if (exist) { + if (is_existing_row) { this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green"); } else { this.show_alert(__("Row #{0}: Item added", [idx]), "green"); @@ -404,13 +438,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return is_duplicate; } - get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) { + get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, 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 matching_row = (row) => { const item_match = row.item_code == item_code; const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no; @@ -418,20 +455,94 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); const item_scanned = row.has_item_scanned; + let warehouse_match = true; + if (has_warehouse_field) { + if (warehouse) { + warehouse_match = row[warehouse_field] === warehouse; + } else { + warehouse_match = !row[warehouse_field]; + } + } + return ( item_match && uom_match && + warehouse_match && !item_scanned && (!is_batch_no_scan || batch_match) && (!check_max_qty || qty_in_limit) ); }; - return this.items_table.find(matching_row) || this.get_existing_blank_row(); + const items_table = this.frm.doc[this.items_table_name] || []; + + return items_table.find(matching_row) || items_table.find((d) => !d.item_code); } - get_existing_blank_row() { - return this.items_table.find((d) => !d.item_code); + setup_last_scanned_warehouse() { + this.frm.set_df_property("last_scanned_warehouse", "options", "Warehouse"); + this.frm.set_df_property("last_scanned_warehouse", "fieldtype", "Link"); + this.frm.set_df_property("last_scanned_warehouse", "formatter", function (value, df, options, doc) { + const link_formatter = frappe.form.get_formatter(df.fieldtype); + const link_value = link_formatter(value, df, options, doc); + + if (!value) { + return link_value; + } + + const clear_btn = ` + + ${frappe.utils.icon("close", "xs", "es-icon")} + + `; + + return link_value + clear_btn; + }); + + this.frm.$wrapper.on("click", ".btn-clear-last-scanned-warehouse", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.clear_warehouse_context(); + }); + } + + 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); + + if (!this.last_scanned_warehouse_initialized) { + this.setup_last_scanned_warehouse(); + this.last_scanned_warehouse_initialized = true; + } + + this.frm.set_value("last_scanned_warehouse", warehouse); + this.show_alert( + __("{0} will be set as the {1} in subsequently scanned items", [ + __(warehouse).bold(), + __(warehouse_field_label).bold(), + ]), + "green", + 6 + ); + } + + clear_warehouse_context() { + this.frm.set_value("last_scanned_warehouse", null); + this.show_alert( + __( + "The last scanned warehouse has been cleared and won't be set in the subsequently scanned items" + ), + "blue", + 6 + ); + } + + get_warehouse_field() { + if (typeof this.warehouse_field === "function") { + return this.warehouse_field(this.frm.doc); + } + return this.warehouse_field; } play_success_sound() { diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 29a2696470f..d17e4f416a4 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -593,3 +593,11 @@ body[data-route="pos"] { .frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { white-space: normal; } + +.btn-clear-last-scanned-warehouse { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + z-index: 1; +} diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 609f45a608f..5c981244d10 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -32,6 +32,7 @@ class ProductBundle(Document): def validate(self): self.validate_main_item() self.validate_child_items() + self.validate_child_items_qty_non_zero() from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty") @@ -88,6 +89,15 @@ class ProductBundle(Document): ).format(item.idx, frappe.bold(item.item_code)) ) + def validate_child_items_qty_non_zero(self): + for item in self.items: + if item.qty <= 0: + frappe.throw( + _( + "Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}" + ).format(item.idx, frappe.bold(item.item_code)) + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index b3aaae99c4d..ae5b980bb9b 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -33,6 +33,7 @@ "ignore_pricing_rule", "items_section", "scan_barcode", + "last_scanned_warehouse", "items", "sec_break23", "total_qty", @@ -1094,13 +1095,20 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2025-05-27 16:04:39.208077", + "modified": "2025-07-31 17:23:48.875382", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1199,4 +1207,4 @@ "states": [], "timeline_field": "party_name", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 70084764799..d6b2fe73cac 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/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 @@ -347,7 +349,7 @@ def get_list_context(context=None): @frappe.whitelist() -def make_sales_order(source_name: str, target_doc=None): +def make_sales_order(source_name: str, target_doc=None, args=None): if not frappe.db.get_singles_value( "Selling Settings", "allow_sales_order_creation_for_expired_quotation" ): @@ -359,10 +361,15 @@ def make_sales_order(source_name: str, target_doc=None): ): frappe.throw(_("Validity period of this quotation has ended.")) - return _make_sales_order(source_name, target_doc) + return _make_sales_order(source_name, target_doc, args=args) -def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): +def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + customer = _make_customer(source_name, ignore_permissions) ordered_items = get_ordered_items(source_name) @@ -430,6 +437,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): # Simple row return True + 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( "Quotation", source_name, @@ -439,7 +451,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, "postprocess": update_item, - "condition": can_map_row, + "condition": lambda d: can_map_row(d) and select_item(d), }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, @@ -476,11 +488,16 @@ def set_expired_status(): @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None): - return _make_sales_invoice(source_name, target_doc) +def make_sales_invoice(source_name, target_doc=None, args=None): + return _make_sales_invoice(source_name, target_doc, args=args) -def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): +def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + customer = _make_customer(source_name, ignore_permissions) def set_missing_values(source, target): @@ -496,6 +513,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.cost_center = None 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( "Quotation", source_name, @@ -504,7 +526,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): "Quotation Item": { "doctype": "Sales Invoice Item", "postprocess": update_item, - "condition": lambda row: not row.is_alternative, + "condition": lambda row: not row.is_alternative and select_item(row), }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 78b819dc46a..f29cd253818 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -738,8 +738,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex if (internal) { let button_label = me.frm.doc.company === me.frm.doc.represents_company - ? "Internal Purchase Order" - : "Inter Company Purchase Order"; + ? __("Internal Purchase Order") + : __("Inter Company Purchase Order"); me.frm.add_custom_button( button_label, @@ -793,6 +793,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex docstatus: 1, status: ["!=", "Lost"], }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "rate", "amount"], }); setTimeout(() => { diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a3219cac8f0..1542721d117 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -41,6 +41,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "column_break_28", "set_warehouse", "reserve_stock", @@ -1657,6 +1658,13 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 9b14616373b..75260a5a3fa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -977,6 +977,11 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): def is_unit_price_row(source): return has_unit_price_items and source.qty == 0 + def select_item(d): + filtered_items = kwargs.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + def set_missing_values(source, target): if kwargs.get("ignore_pricing_rule"): # Skip pricing rule when the dn is creating from the pick list @@ -1042,7 +1047,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): "name": "so_detail", "parent": "against_sales_order", }, - "condition": condition, + "condition": lambda d: condition(d) and select_item(d), "postprocess": update_item, } @@ -1098,7 +1103,12 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): @frappe.whitelist() -def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): +def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + # 0 qty is accepted, as the qty is uncertain for some items has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") @@ -1158,6 +1168,11 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if cost_center: target.cost_center = cost_center + 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( "Sales Order", source_name, @@ -1182,7 +1197,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): True if is_unit_price_row(doc) else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))) - ), + ) + and select_item(doc), }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 59f293a96b0..92e9ad060df 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -342,7 +342,6 @@ erpnext.PointOfSale.Payment = class { } render_payment_section() { - this.remove_grand_total_from_default_mop(); this.render_payment_mode_dom(); this.make_invoice_fields_control(); this.update_totals_section(); diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 9cdf24cf451..c78e9b5b60d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -480,14 +480,19 @@ def install_defaults(args=None): # nosemgrep create_bank_account(args) -def set_global_defaults(args): +def set_global_defaults(kwargs): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") + company = frappe.db.get_value( + "Company", + {"company_name": kwargs.get("company_name")}, + "name", + ) global_defaults.update( { - "default_currency": args.get("currency"), - "default_company": args.get("company_name"), - "country": args.get("country"), + "default_currency": kwargs.get("currency"), + "default_company": company, + "country": kwargs.get("country"), } ) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 440e104abb6..0cd4e24ff93 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -182,6 +182,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( company: me.frm.doc.company, project: me.frm.doc.project || undefined, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "delivered_qty"], }); }, __("Get Items From") @@ -231,6 +234,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends ( }, get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query", size: "extra-large", + allow_child_item_selection: true, + child_fieldname: "locations", + child_columns: ["item_code", "item_name", "stock_qty", "delivered_qty"], }); }, __("Get Items From") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 3a9cca1c418..bdfbbb93916 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -38,6 +38,7 @@ "ignore_pricing_rule", "items_section", "scan_barcode", + "last_scanned_warehouse", "col_break_warehouse", "set_warehouse", "set_target_warehouse", @@ -1390,6 +1391,13 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-truck", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 762664bd566..6cadbdc5e47 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _ from frappe.contacts.doctype.address.address import get_company_address @@ -824,6 +826,11 @@ def get_returned_qty_map(delivery_note): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + doc = frappe.get_doc("Delivery Note", source_name) to_make_invoice_qty_map = {} @@ -875,6 +882,11 @@ def make_sales_invoice(source_name, target_doc=None, args=None): return pending_qty + 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 + doc = get_mapped_doc( "Delivery Note", source_name, @@ -897,6 +909,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None): "filter": lambda d: get_pending_qty(d) <= 0 if not doc.get("is_return") else get_pending_qty(d) > 0, + "condition": select_item, }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 40b87b79f97..0136bcfcb9d 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -674,13 +674,13 @@ def prepare_data_for_internal_transfer(): company = "_Test Company with perpetual inventory" customer = create_internal_customer( - "_Test Internal Customer 3", + "_Test Internal Customer 2", company, company, ) supplier = create_internal_supplier( - "_Test Internal Supplier 3", + "_Test Internal Supplier 2", company, company, ) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 60725b9ce9c..0c467df381c 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -20,9 +20,9 @@ "amended_from", "warehouse_section", "scan_barcode", - "column_break_13", - "set_from_warehouse", + "last_scanned_warehouse", "column_break5", + "set_from_warehouse", "set_warehouse", "items_section", "items", @@ -350,22 +350,25 @@ "fieldname": "column_break_35", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List" + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-07-28 15:13:49.000037", + "modified": "2025-07-31 17:19:01.166208", "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 409623a348f..6cd5a035c99 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -11,6 +11,7 @@ import frappe import frappe.defaults from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Order from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate @@ -555,39 +556,44 @@ def get_items_based_on_default_supplier(supplier): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): - conditions = "" - if txt: - conditions += "and mr.name like '%%" + txt + "%%' " - - if filters.get("transaction_date"): - date = filters.get("transaction_date")[1] - conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' " - supplier = filters.get("supplier") supplier_items = get_items_based_on_default_supplier(supplier) if not supplier_items: frappe.throw(_("{0} is not the default supplier for any items.").format(supplier)) - material_requests = frappe.db.sql( - """select distinct mr.name, transaction_date,company - from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where mr.name = mr_item.parent - and mr_item.item_code in ({}) - and mr.material_request_type = 'Purchase' - and mr.per_ordered < 99.99 - and mr.docstatus = 1 - and mr.status != 'Stopped' - and mr.company = %s - {} - order by mr_item.item_code ASC - limit {} offset {} """.format( - ", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start) - ), - (*tuple(supplier_items), filters.get("company")), - as_dict=1, + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + + query = ( + frappe.qb.from_(mr) + .from_(mr_item) + .select(mr.name) + .distinct() + .select(mr.transaction_date, mr.company) + .where( + (mr.name == mr_item.parent) + & (mr_item.item_code.isin(supplier_items)) + & (mr.material_request_type == "Purchase") + & (mr.per_ordered < 99.99) + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.company == filters.get("company")) + ) + .orderby(mr_item.item_code, order=Order.asc) + .limit(cint(page_len)) + .offset(cint(start)) ) + if txt: + query = query.where(mr.name.like(f"%%{txt}%%")) + + if filters.get("transaction_date"): + date = filters.get("transaction_date")[1] + query = query.where(mr.transaction_date[date[0] : date[1]]) + + material_requests = query.run(as_dict=True) + return material_requests diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 2420c166161..9417751f682 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1252,11 +1252,16 @@ def create_dn_wo_so(pick_list, delivery_note=None): @frappe.whitelist() def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None): """Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer""" + if kwargs is None: + kwargs = {} + if isinstance(kwargs, str): + kwargs = json.loads(kwargs) + pick_list = frappe.get_doc("Pick List", source_name) validate_item_locations(pick_list) - sales_order_arg = kwargs.get("sales_order") if kwargs else None - customer_arg = kwargs.get("customer") if kwargs else None + sales_order_arg = kwargs.get("sales_order") + customer_arg = kwargs.get("customer") if sales_order_arg: sales_orders = {sales_order_arg} @@ -1270,7 +1275,7 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None): pluck="name", ) - delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc) + delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs) if not sales_order_arg and not all(item.sales_order for item in pick_list.locations): if isinstance(delivery_note, str): @@ -1296,10 +1301,15 @@ def create_dn_with_so(sales_dict, pick_list): return delivery_note -def create_dn_from_so(pick_list, sales_order_list, delivery_note=None): +def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None): if not sales_order_list: return delivery_note + def select_item(d): + filtered_items = kwargs.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + item_table_mapper = { "doctype": "Delivery Note Item", "field_map": { @@ -1307,7 +1317,9 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None): "name": "so_detail", "parent": "against_sales_order", }, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1 + and select_item(doc), } kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule} diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bcecf8be14d..db065a80c92 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -150,7 +150,11 @@ frappe.ui.form.on("Purchase Receipt", { docstatus: 1, per_received: ["<", 100], company: frm.doc.company, + update_stock: 0, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "received_qty"], }); }, __("Get Items From") @@ -255,6 +259,9 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend per_received: ["<", 99.99], company: me.frm.doc.company, }, + allow_child_item_selection: true, + child_fieldname: "items", + child_columns: ["item_code", "item_name", "qty", "received_qty"], }); }, __("Get Items From") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index cef346a6d85..603f4a121d1 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -40,6 +40,7 @@ "ignore_pricing_rule", "sec_warehouse", "scan_barcode", + "last_scanned_warehouse", "column_break_31", "set_warehouse", "set_from_warehouse", @@ -1285,6 +1286,13 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "grid_page_length": 50, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 9e7dea631a9..1c4d28c495e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _, throw from frappe.desk.notifications import clear_doctype_notifications @@ -1225,6 +1227,11 @@ def get_item_wise_returned_qty(pr_doc): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc("Purchase Receipt", source_name) @@ -1279,6 +1286,11 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): return pending_qty, returned_qty + 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( "Purchase Receipt", source_name, @@ -1308,9 +1320,10 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): "wip_composite_asset": "wip_composite_asset", }, "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, + "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), }, "Purchase Taxes and Charges": { "doctype": "Purchase Taxes and Charges", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 7b5fb517095..53af3e12896 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -131,7 +131,12 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = source_warehouse or item.get("warehouse") + warehouse = ( + (source_warehouse or item.get("warehouse")) + if not item.get("t_warehouse") + else item.get("t_warehouse") + ) + if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 51455ef0d24..67918ee1dfd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1003,6 +1003,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle setup() { var me = this; + this.barcode_scanner = new erpnext.utils.BarcodeScanner({ + frm: this.frm, + warehouse_field: (doc) => { + return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse"; + }, + }); + this.setup_posting_date_time_check(); this.frm.fields_dict.bom_no.get_query = function () { @@ -1130,8 +1137,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle scan_barcode() { frappe.flags.dialog_set = false; - const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm }); - barcode_scanner.process_scan(); + this.barcode_scanner.process_scan(); } on_submit() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index adec80dcab2..023dca5bdf2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -50,6 +50,7 @@ "target_address_display", "sb0", "scan_barcode", + "last_scanned_warehouse", "items_section", "items", "get_stock_and_rate", @@ -691,6 +692,13 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-file-text", diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 68e57a3d971..cde5be4d6ee 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2020,6 +2020,70 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.items[0].basic_rate, 300) + def test_batch_item_additional_cost_for_material_transfer_entry(self): + item_code = "_Test Batch Item Additional Cost MTE" + make_item( + item_code, + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_naming_series": "BT-MTE.#####", + }, + ) + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=2, + basic_rate=100, + use_serial_batch_fields=1, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + se = make_stock_entry( + item_code=item_code, + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch_no, + use_serial_batch_fields=1, + qty=2, + purpose="Material Transfer", + do_not_save=True, + ) + + se.append( + "additional_costs", + { + "cost_center": "Main - _TC", + "amount": 50, + "expense_account": "Stock Adjustment - _TC", + "description": "Test Additional Cost", + }, + ) + se.save() + self.assertEqual(se.additional_costs[0].amount, 50) + self.assertEqual(se.items[0].basic_rate, 100) + self.assertEqual(se.items[0].valuation_rate, 125) + + se.submit() + self.assertEqual(se.items[0].basic_rate, 100) + self.assertEqual(se.items[0].valuation_rate, 125) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": item_code, + "warehouse": "_Test Warehouse 1 - _TC", + "voucher_type": "Stock Entry", + "voucher_no": se.name, + }, + "incoming_rate", + ) + + self.assertEqual(incoming_rate, 125.0) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 44dd2952409..d8dd2a7560a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -7,6 +7,7 @@ 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 }); }, onload: function (frm) { @@ -96,8 +97,7 @@ frappe.ui.form.on("Stock Reconciliation", { }, scan_barcode: function (frm) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm }); - barcode_scanner.process_scan(); + frm.barcode_scanner.process_scan(); }, scan_mode: function (frm) { diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 4712b8aeb16..76f2691be72 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -18,6 +18,7 @@ "set_warehouse", "section_break_22", "scan_barcode", + "last_scanned_warehouse", "column_break_12", "scan_mode", "sb9", @@ -178,6 +179,13 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.last_scanned_warehouse", + "fieldname": "last_scanned_warehouse", + "fieldtype": "Data", + "is_virtual": 1, + "label": "Last Scanned Warehouse" } ], "icon": "fa fa-upload-alt", diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 203340a9ff5..21180accf83 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -185,7 +185,21 @@ class SerialBatchBundle: } if self.sle.actual_qty < 0 and self.is_material_transfer(): - values_to_update["valuation_rate"] = flt(sn_doc.avg_rate) + basic_rate = flt(sn_doc.avg_rate) + ste_detail = frappe.db.get_value( + "Stock Entry Detail", + self.sle.voucher_detail_no, + ["additional_cost", "transfer_qty"], + as_dict=True, + ) + + additional_cost = 0.0 + + if ste_detail: + additional_cost = flt(ste_detail.additional_cost) / flt(ste_detail.transfer_qty) + + values_to_update["basic_rate"] = basic_rate + values_to_update["valuation_rate"] = basic_rate + additional_cost if not frappe.db.get_single_value( "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle" diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index bc646fae45c..6e66c100466 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -81,3 +81,44 @@ class TestStockUtilities(FrappeTestCase, StockTestMixin): self.assertEqual(serial_scan["serial_no"], serial.name) self.assertEqual(serial_scan["has_batch_no"], 0) self.assertEqual(serial_scan["has_serial_no"], 1) + + def test_barcode_scanning_of_warehouse(self): + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Warehouse for Barcode", + "company": "_Test Company", + } + ).insert() + + warehouse_2 = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Warehouse for Barcode 2", + "company": "_Test Company", + } + ).insert() + + warehouse_scan = scan_barcode(warehouse.name) + self.assertEqual(warehouse_scan["warehouse"], warehouse.name) + + item_with_warehouse = self.make_item( + properties={ + "item_defaults": [{"company": "_Test Company", "default_warehouse": warehouse.name}], + "barcodes": [{"barcode": "w12345"}], + } + ) + + item_scan = scan_barcode("w12345") + self.assertEqual(item_scan["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan.get("default_warehouse"), None) + + ctx = {"company": "_Test Company"} + item_scan_with_ctx = scan_barcode("w12345", ctx=ctx) + self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse.name) + + ctx = {"company": "_Test Company", "set_warehouse": warehouse_2.name} + item_scan_with_ctx = scan_barcode("w12345", ctx=ctx) + self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name) + self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse_2.name) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 637d56f093a..781fc81445c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -126,8 +126,9 @@ def get_stock_balance( extra_cond = "" if inventory_dimensions_dict: for field, value in inventory_dimensions_dict.items(): + column = frappe.utils.sanitize_column(field) args[field] = value - extra_cond += f" and {field} = %({field})s" + extra_cond += f" and {column} = %({field})s" last_entry = get_previous_sle(args, extra_cond=extra_cond) @@ -584,13 +585,24 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool @frappe.whitelist() -def scan_barcode(search_value: str) -> BarcodeScanResult: +def scan_barcode(search_value: str, ctx: dict | str | None = None) -> BarcodeScanResult: def set_cache(data: BarcodeScanResult): frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120) + _update_item_info(data, ctx) def get_cache() -> BarcodeScanResult | None: - if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"): - return data + data = frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}") + if not data: + return + + _update_item_info(data, ctx) + return data + + if ctx is None: + ctx = frappe._dict() + + else: + ctx = frappe.parse_json(ctx) if scan_data := get_cache(): return scan_data @@ -603,7 +615,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: as_dict=True, ) if barcode_data: - _update_item_info(barcode_data) set_cache(barcode_data) return barcode_data @@ -615,7 +626,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: as_dict=True, ) if serial_no_data: - _update_item_info(serial_no_data) set_cache(serial_no_data) return serial_no_data @@ -634,22 +644,36 @@ def scan_barcode(search_value: str) -> BarcodeScanResult: ).format(search_value, batch_no_data.item_code) ) - _update_item_info(batch_no_data) set_cache(batch_no_data) return batch_no_data + warehouse = frappe.get_cached_value("Warehouse", search_value, ("name", "disabled"), as_dict=True) + if warehouse and not warehouse.disabled: + warehouse_data = {"warehouse": warehouse.name} + set_cache(warehouse_data) + return warehouse_data + return {} -def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]: - if item_code := scan_result.get("item_code"): - if item_info := frappe.get_cached_value( - "Item", - item_code, - ["has_batch_no", "has_serial_no"], - as_dict=True, - ): - scan_result.update(item_info) +def _update_item_info(scan_result: dict[str, str | None], ctx: dict | None = None) -> dict[str, str | None]: + from erpnext.stock.get_item_details import get_item_warehouse + + item_code = scan_result.get("item_code") + if not item_code: + return scan_result + + if item_info := frappe.get_cached_value( + "Item", + item_code, + ("has_batch_no", "has_serial_no"), + as_dict=True, + ): + scan_result.update(item_info) + + if ctx and (warehouse := get_item_warehouse(frappe._dict(name=item_code), ctx, overwrite_warehouse=True)): + scan_result["default_warehouse"] = warehouse + return scan_result diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index b8bd95bcbca..e975f384c25 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -152,6 +152,7 @@ "width": "100px" }, { + "default": "Now", "description": "Time at which materials were received", "fieldname": "posting_time", "fieldtype": "Time",