diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index 183049c8dfc..84b6239a392 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -291,12 +291,14 @@ frappe.treeview_settings["Account"] = {
label: __("View Ledger"),
click: function (node, btn) {
frappe.route_options = {
- account: node.label,
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
company:
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
};
+ if (node.parent_label) {
+ frappe.route_options["account"] = node.label;
+ }
frappe.set_route("query-report", "General Ledger");
},
btnClass: "hidden-xs",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d05e9e3b2d1..d7386902c81 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -1044,9 +1044,7 @@ class JournalEntry(AccountsController):
def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0
- currency = (
- bank_account_currency
- ) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
+ currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
party_type = None
for d in self.get("accounts"):
if d.party_type in ["Customer", "Supplier"] and d.party:
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index 4d2d14abea4..0c9a20882c6 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -580,6 +580,18 @@ class TestJournalEntry(unittest.TestCase):
]
self.assertEqual(expected, actual)
+ def test_pay_to_recd_from(self):
+ jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
+ jv.pay_to_recd_from = "_Test Receiver"
+ jv.save()
+ self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
+
+ jv.pay_to_recd_from = "_Test Receiver 2"
+ jv.save()
+ jv.submit()
+
+ self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
+
def make_journal_entry(
account1,
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index c15309df294..aaf5142362f 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1443,6 +1443,8 @@
"width": "50%"
},
{
+ "fetch_from": "sales_partner.commission_rate",
+ "fetch_if_empty": 1,
"fieldname": "commission_rate",
"fieldtype": "Float",
"label": "Commission Rate (%)",
@@ -1571,7 +1573,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2024-11-26 13:10:50.309570",
+ "modified": "2025-07-17 16:51:40.886083",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index fedc6a7772d..913ab7e6c47 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -337,6 +337,11 @@ class POSInvoiceMergeLog(Document):
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ""
+ # Unset Commission Section
+ invoice.set("sales_partner", None)
+ invoice.set("commission_rate", 0)
+ invoice.set("total_commission", 0)
+
return invoice
def get_new_sales_invoice(self):
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 10d94a21794..9ec3555b49a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1223,6 +1223,9 @@ class PurchaseInvoice(BuyingController):
def get_provisional_accounts(self):
self.provisional_accounts = frappe._dict()
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
+ if not linked_purchase_receipts:
+ return
+
pr_items = frappe.get_all(
"Purchase Receipt Item",
filters={"parent": ("in", linked_purchase_receipts)},
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
index 084ea9b80ea..ed30ad415d0 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -86,7 +86,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac
"finance_book": cstr(filters.get("finance_book")),
}
- gl_filters["dimensions"] = set(dimension_list)
+ gl_filters["dimensions"] = tuple(set(dimension_list))
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
@@ -179,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
def get_condition(dimension):
conditions = []
- conditions.append(f"{frappe.scrub(dimension)} in (%(dimensions)s)")
+ conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
return " and {}".format(" and ".join(conditions)) if conditions else ""
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 0bb14604991..91b244f94fa 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -204,7 +204,7 @@ def get_gl_entries(filters, accounting_dimensions):
)
if filters.get("presentation_currency"):
- return convert_to_presentation_currency(gl_entries, currency_map)
+ return convert_to_presentation_currency(gl_entries, currency_map, filters)
else:
return gl_entries
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index ae822c5b413..559ba4a70ab 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -178,7 +178,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice",
"fieldtype": "Link",
"options": "Purchase Invoice",
- "width": 120,
+ "width": 150,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
]
@@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters):
def get_items(filters, additional_table_columns):
doctype = "Purchase Invoice"
- pi = frappe.qb.DocType(doctype)
- pii = frappe.qb.DocType(f"{doctype} Item")
+ pi = frappe.qb.DocType(doctype).as_("invoice")
+ pii = frappe.qb.DocType(f"{doctype} Item").as_("invoice_item")
Item = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(pi)
@@ -331,6 +331,7 @@ def get_items(filters, additional_table_columns):
pi.unrealized_profit_loss_account,
pii.item_code,
pii.description,
+ pii.item_name,
pii.item_group,
pii.item_name.as_("pi_item_name"),
pii.item_group.as_("pi_item_group"),
@@ -374,7 +375,7 @@ def get_items(filters, additional_table_columns):
if match_conditions:
query += " and " + match_conditions
- query = apply_order_by_conditions(query, pi, pii, filters)
+ query = apply_order_by_conditions(query, filters)
return frappe.db.sql(query, params, as_dict=True)
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 73d8c3f7d14..b4a72c5374f 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
@@ -198,7 +198,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice",
"fieldtype": "Link",
"options": "Sales Invoice",
- "width": 120,
+ "width": 150,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
]
@@ -343,7 +343,7 @@ def get_columns(additional_table_columns, filters):
return columns
-def apply_conditions(query, si, sii, filters, additional_conditions=None):
+def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
for opts in ("company", "customer"):
if filters.get(opts):
query = query.where(si[opts] == filters[opts])
@@ -355,10 +355,7 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"):
- sales_invoice = frappe.db.get_all(
- "Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
- )
- query = query.where(si.name.isin(sales_invoice))
+ query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
if filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
@@ -397,15 +394,15 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
return query
-def apply_order_by_conditions(query, si, ii, filters):
+def apply_order_by_conditions(query, filters):
if not filters.get("group_by"):
- query += f" order by {si.posting_date} desc, {ii.item_group} desc"
+ query += "order by invoice.posting_date desc, invoice_item.item_group desc"
elif filters.get("group_by") == "Invoice":
- query += f" order by {ii.parent} desc"
+ query += "order by invoice_item.parent desc"
elif filters.get("group_by") == "Item":
- query += f" order by {ii.item_code}"
+ query += "order by invoice_item.item_code"
elif filters.get("group_by") == "Item Group":
- query += f" order by {ii.item_group}"
+ query += "order by invoice_item.item_group"
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc"
@@ -415,14 +412,17 @@ def apply_order_by_conditions(query, si, ii, filters):
def get_items(filters, additional_query_columns, additional_conditions=None):
doctype = "Sales Invoice"
- si = frappe.qb.DocType(doctype)
- sii = frappe.qb.DocType(f"{doctype} Item")
+ si = frappe.qb.DocType("Sales Invoice").as_("invoice")
+ sii = frappe.qb.DocType("Sales Invoice Item").as_("invoice_item")
+ sip = frappe.qb.DocType("Sales Invoice Payment")
item = frappe.qb.DocType("Item")
query = (
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(
@@ -462,6 +462,7 @@ 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)
@@ -481,7 +482,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
if filters.get("customer_group"):
query = query.where(si.customer_group == filters["customer_group"])
- query = apply_conditions(query, si, sii, filters, additional_conditions)
+ query = apply_conditions(query, si, sii, sip, filters, additional_conditions)
from frappe.desk.reportview import build_match_conditions
@@ -491,7 +492,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
if match_conditions:
query += " and " + match_conditions
- query = apply_order_by_conditions(query, si, sii, filters)
+ query = apply_order_by_conditions(query, filters)
return frappe.db.sql(query, params, as_dict=True)
@@ -763,25 +764,13 @@ def add_total_row(
def get_display_value(filters, group_by_field, item):
if filters.get("group_by") == "Item":
if item.get("item_code") != item.get("item_name"):
- value = (
- cstr(item.get("item_code"))
- + "
"
- + ""
- + cstr(item.get("item_name"))
- + ""
- )
+ value = f"{item.get('item_code')}: {item.get('item_name')}"
else:
value = item.get("item_code", "")
elif filters.get("group_by") in ("Customer", "Supplier"):
party = frappe.scrub(filters.get("group_by"))
if item.get(party) != item.get(party + "_name"):
- value = (
- item.get(party)
- + "
"
- + ""
- + item.get(party + "_name")
- + ""
- )
+ value = f"{item.get(party)}: {item.get(party + '_name')}"
else:
value = item.get(party)
else:
diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
index 9dd4e437f7f..9a1131e069b 100644
--- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
+++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
@@ -1,21 +1,22 @@
{
"add_total_row": 0,
+ "add_translate_data": 0,
"columns": [],
"creation": "2013-05-06 12:28:23",
- "disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
- "idx": 3,
+ "idx": 6,
"is_standard": "Yes",
- "modified": "2021-10-06 06:26:07.881340",
+ "letterhead": null,
+ "modified": "2025-07-17 23:16:19.892044",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Partners Commission",
"owner": "Administrator",
"prepared_report": 0,
- "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
+ "query": "SELECT\n sales_partner as \"Sales Partner:Link / Sales Partner:220\",\n sum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n sum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n sum(total_commission) as \"Total Commission:Currency:170\",\n sum(total_commission)*100 / sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n (\n SELECT\n sales_partner,\n base_net_total,\n total_commission,\n amount_eligible_for_commission\n FROM\n `tabSales Invoice` \n WHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\n\n UNION ALL\n\n SELECT\n sales_partner,\n base_net_total,\n total_commission,\n amount_eligible_for_commission\n FROM\n `tabPOS Invoice`\n WHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\n ) AS sub\nGROUP BY\n sales_partner\nORDER BY\n \"Total Commission:Currency:120\"",
"ref_doctype": "Sales Invoice",
"report_name": "Sales Partners Commission",
"report_type": "Query Report",
@@ -26,5 +27,6 @@
{
"role": "Accounts User"
}
- ]
-}
\ No newline at end of file
+ ],
+ "timeout": 0
+}
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 2a72b10e4eb..136a0acbbb0 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate
-def convert_to_presentation_currency(gl_entries, currency_info):
+def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
"""
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`.
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
company_currency = currency_info["company_currency"]
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
+ exchange_gain_or_loss = False
+
+ if filters and isinstance(filters.get("account"), list):
+ account_filter = filters.get("account")
+ gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
+
+ exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
for entry in gl_entries:
debit = flt(entry["debit"])
@@ -107,7 +114,11 @@ def convert_to_presentation_currency(gl_entries, currency_info):
credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"]
- if len(account_currencies) == 1 and account_currency == presentation_currency:
+ if (
+ len(account_currencies) == 1
+ and account_currency == presentation_currency
+ and not exchange_gain_or_loss
+ ):
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 9132cb15fd9..30081e275ff 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -512,7 +512,8 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
-
+ if referenced_row.get("outstanding_amount"):
+ referenced_row.outstanding_amount -= flt(entry.allocated_amount)
doc.save(ignore_permissions=True)
# re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 4e726333a7f..73d8a42c505 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -447,13 +447,12 @@ class BuyingController(SubcontractingController):
raise_error_if_no_rate=False,
)
- d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
+ d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1))
else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
d.sales_incoming_rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
- * (d.conversion_factor or 1),
- d.precision("rate"),
+ * (d.conversion_factor or 1)
)
def validate_for_subcontracting(self):
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 8390ecada10..b4610337ceb 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -42,14 +42,14 @@ def employee_query(
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
)
+ search_conditions = " or ".join([f"{field} like %(txt)s" for field in fields])
mcond = "" if ignore_permissions else get_match_cond(doctype)
return frappe.db.sql(
"""select {fields} from `tabEmployee`
where status in ('Active', 'Suspended')
and docstatus < 2
- and ({key} like %(txt)s
- or employee_name like %(txt)s)
+ and ({key} like %(txt)s or {search_conditions})
{fcond} {mcond}
order by
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
@@ -62,6 +62,7 @@ def employee_query(
"key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions),
"mcond": mcond,
+ "search_conditions": search_conditions,
}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 011f21fe388..599221185d9 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -4,7 +4,7 @@
from collections import defaultdict
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.meta import get_field_precision
from frappe.utils import cint, flt, format_datetime, get_datetime
@@ -40,11 +40,12 @@ def validate_return_against(doc):
frappe.throw(
_("The {0} {1} does not match with the {0} {2} in the {3} {4}").format(
doc.meta.get_label(party_type),
- doc.get(party_type),
- ref_doc.get(party_type),
+ bold(doc.get(party_type)),
+ bold(ref_doc.get(party_type)),
ref_doc.doctype,
ref_doc.name,
- )
+ ),
+ title=_("Party Mismatch"),
)
if (
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 5f7cfb165d4..d85c1b28f97 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -68,10 +68,13 @@ class SellingController(StockController):
serial_nos = frappe.get_all(
"Serial and Batch Entry",
- filters={"parent": ("in", bundle_ids)},
+ filters={"parent": ("in", bundle_ids), "serial_no": ("is", "set")},
pluck="serial_no",
)
+ if not serial_nos:
+ return
+
if serial_nos := frappe.get_all(
"Serial No",
filters={"name": ("in", serial_nos), "customer": ("is", "set")},
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 1d55c64663f..0c535af5be2 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -37,6 +37,14 @@ frappe.ui.form.on("Production Plan", {
};
});
+ frm.set_query("sub_assembly_warehouse", function (doc) {
+ return {
+ filters: {
+ company: doc.company,
+ },
+ };
+ });
+
frm.set_query("material_request", "material_requests", function () {
return {
filters: {
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 5106ded95e8..c77d34950ec 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -2788,56 +2788,45 @@ class TestWorkOrder(FrappeTestCase):
fg_warehouse="_Test Warehouse 2 - _TC",
)
- # Initial check
- self.assertEqual(wo.operations[0].operation, "Test Operation A")
- self.assertEqual(wo.operations[1].operation, "Test Operation B")
- self.assertEqual(wo.operations[2].operation, "Test Operation C")
- self.assertEqual(wo.operations[3].operation, "Test Operation D")
-
wo = frappe.copy_doc(wo)
- wo.operations[3].sequence_id = 2
+ wo.operations[3].sequence_id = None
+
+ # Test 1 : If any one operation does not have sequence ID then error will be thrown
+ self.assertRaises(frappe.ValidationError, wo.submit)
+
+ for op in wo.operations:
+ op.sequence_id = None
wo.submit()
- # Test 2 : Sort line items in child table based on sequence ID
- self.assertEqual(wo.operations[0].operation, "Test Operation A")
- self.assertEqual(wo.operations[1].operation, "Test Operation B")
- self.assertEqual(wo.operations[2].operation, "Test Operation D")
- self.assertEqual(wo.operations[3].operation, "Test Operation C")
+ # Test 2 : If none of the operations have sequence ID then they will be sequenced as per their idx
+ for op in wo.operations:
+ self.assertEqual(op.sequence_id, op.idx)
wo = frappe.copy_doc(wo)
- wo.operations[3].sequence_id = 1
- wo.submit()
+ wo.operations[0].sequence_id = 2
- self.assertEqual(wo.operations[0].operation, "Test Operation A")
- self.assertEqual(wo.operations[1].operation, "Test Operation C")
- self.assertEqual(wo.operations[2].operation, "Test Operation B")
- self.assertEqual(wo.operations[3].operation, "Test Operation D")
+ # Test 3 : Sequence IDs should not miss the correct sequence of numbers
+ self.assertRaises(frappe.ValidationError, wo.submit)
- wo = frappe.copy_doc(wo)
- wo.operations[0].sequence_id = 3
- wo.submit()
+ wo.operations[1].sequence_id = 1
- self.assertEqual(wo.operations[0].operation, "Test Operation C")
- self.assertEqual(wo.operations[1].operation, "Test Operation B")
- self.assertEqual(wo.operations[2].operation, "Test Operation D")
- self.assertEqual(wo.operations[3].operation, "Test Operation A")
-
- wo = frappe.copy_doc(wo)
- wo.operations[1].sequence_id = 0
-
- # Test 3 - Error should be thrown if any one operation does not have sequence id but others do
+ # Test 4 : Sequence IDs should be in the correct ascending order
self.assertRaises(frappe.ValidationError, wo.submit)
workstation = frappe.get_doc("Workstation", "Test Workstation A")
workstation.production_capacity = 4
workstation.save()
-
wo = frappe.copy_doc(wo)
+ wo.operations[0].sequence_id = 1
wo.operations[1].sequence_id = 2
+ wo.operations[2].sequence_id = 2
+ wo.operations[3].sequence_id = 3
wo.submit()
- # Test 4 - If Sequence ID is same then planned start time for both operations should be same
- self.assertEqual(wo.operations[1].planned_start_time, wo.operations[2].planned_start_time)
+ # Test 5 : If two operations have the same sequence ID then the next operation will start 10 mins after the longest previous operation ends
+ self.assertEqual(
+ wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
+ )
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 3db6d165328..3d2d5417604 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -419,7 +419,7 @@ frappe.ui.form.on("Work Order", {
frm.doc.material_transferred_for_manufacturing -
frm.doc.produced_qty -
frm.doc.process_loss_qty;
- if (pending_complete) {
+ if (pending_complete > 0) {
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
title = __("{0} items in progress", [pending_complete]);
bars.push({
@@ -829,6 +829,19 @@ erpnext.work_order = {
description: __("Max: {0}", [max]),
default: max,
},
+ {
+ fieldtype: "Check",
+ label: __("Consider Process Loss"),
+ fieldname: "consider_process_loss",
+ default: 0,
+ onchange: function () {
+ if (this.value) {
+ frm.qty_prompt.set_value("qty", max - frm.doc.process_loss_qty);
+ } else {
+ frm.qty_prompt.set_value("qty", max);
+ }
+ },
+ },
];
if (purpose === "Disassemble") {
@@ -850,7 +863,7 @@ erpnext.work_order = {
}
return new Promise((resolve, reject) => {
- frappe.prompt(
+ frm.qty_prompt = frappe.prompt(
fields,
(data) => {
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 9b1bf28f997..9d862e84da7 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -168,6 +168,31 @@ class WorkOrder(Document):
validate_uom_is_integer(self, "stock_uom", ["required_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items")))
+ self.validate_operations_sequence()
+
+ def validate_operations_sequence(self):
+ if all([not op.sequence_id for op in self.operations]):
+ for op in self.operations:
+ op.sequence_id = op.idx
+ else:
+ sequence_id = 1
+ for op in self.operations:
+ if op.idx == 1 and op.sequence_id != 1:
+ frappe.throw(
+ _("Row #1: Sequence ID must be 1 for Operation {0}.").format(
+ frappe.bold(op.operation)
+ )
+ )
+ elif op.sequence_id != sequence_id and op.sequence_id != sequence_id + 1:
+ frappe.throw(
+ _("Row #{0}: Sequence ID must be {1} or {2} for Operation {3}.").format(
+ op.idx,
+ frappe.bold(sequence_id),
+ frappe.bold(sequence_id + 1),
+ frappe.bold(op.operation),
+ )
+ )
+ sequence_id = op.sequence_id
def set_warehouses(self):
for row in self.required_items:
@@ -637,17 +662,6 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
- if all([op.sequence_id for op in self.operations]):
- self.operations = sorted(self.operations, key=lambda op: op.sequence_id)
- for idx, op in enumerate(self.operations):
- op.idx = idx + 1
- elif any([op.sequence_id for op in self.operations]):
- frappe.throw(
- _(
- "Row #{0}: Incorrect Sequence ID. If any single operation has a Sequence ID then all other operations must have one too."
- ).format(next((op.idx for op in self.operations if not op.sequence_id), None))
- )
-
for idx, row in enumerate(self.operations):
qty = self.qty
while qty > 0:
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index 0185812a4b6..38b325b73ab 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -194,6 +194,7 @@
"fieldname": "sequence_id",
"fieldtype": "Int",
"label": "Sequence ID",
+ "non_negative": 1,
"print_hide": 1
},
{
@@ -224,7 +225,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-04-09 16:21:47.110564",
+ "modified": "2025-05-15 15:10:06.885440",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 447e264ad75..5f4c3672228 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -413,3 +413,4 @@ erpnext.patches.v15_0.update_pick_list_fields
erpnext.patches.v15_0.update_pegged_currencies
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
erpnext.patches.v15_0.rename_price_list_to_buying_price_list
+erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice
diff --git a/erpnext/patches/v15_0/remove_sales_partner_from_consolidated_sales_invoice.py b/erpnext/patches/v15_0/remove_sales_partner_from_consolidated_sales_invoice.py
new file mode 100644
index 00000000000..ac1daeef44d
--- /dev/null
+++ b/erpnext/patches/v15_0/remove_sales_partner_from_consolidated_sales_invoice.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+ SalesInvoice = frappe.qb.DocType("Sales Invoice")
+
+ query = (
+ frappe.qb.update(SalesInvoice)
+ .set(SalesInvoice.sales_partner, "")
+ .set(SalesInvoice.commission_rate, 0)
+ .set(SalesInvoice.total_commission, 0)
+ .where(SalesInvoice.is_consolidated == 1)
+ )
+
+ # For develop/version-16
+ if frappe.db.has_column("Sales Invoice", "is_created_using_pos"):
+ query = query.where(SalesInvoice.is_created_using_pos == 0)
+
+ query.run()
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index c7260ccc722..03cb670c4df 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1407,6 +1407,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
]);
} else {
this.conversion_factor(doc, cdt, cdn, true)
+ this.calculate_taxes_and_totals()
}
}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 2f2f745bedb..b89e14b5bcf 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -771,6 +771,14 @@ class SalesOrder(SellingController):
voucher_type=self.doctype, voucher_no=self.name, sre_list=sre_list, notify=notify
)
+ def set_missing_values(self, for_validate=False):
+ super().set_missing_values(for_validate)
+
+ if self.delivery_date:
+ for item in self.items:
+ if not item.delivery_date:
+ item.delivery_date = self.delivery_date
+
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
"""Returns the unreserved quantity for the Sales Order Item."""
@@ -1352,6 +1360,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty)
target.project = source_parent.project
+ def update_item_for_packed_item(source, target, source_parent):
+ target.qty = flt(source.qty) - flt(source.ordered_qty)
+
suppliers = [item.get("supplier") for item in selected_items if item.get("supplier")]
suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order
@@ -1405,13 +1416,35 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
and doc.supplier == supplier
and doc.item_code in items_to_map
- and doc.delivered_by_supplier == 1,
+ and not is_product_bundle(doc.item_code),
+ },
+ "Packed Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "sales_order_packed_item"],
+ ["parent", "sales_order"],
+ ["uom", "uom"],
+ ["conversion_factor", "conversion_factor"],
+ ["parent_item", "product_bundle"],
+ ["rate", "rate"],
+ ],
+ "field_no_map": [
+ "price_list_rate",
+ "item_tax_template",
+ "discount_percentage",
+ "discount_amount",
+ "supplier",
+ "pricing_rules",
+ ],
+ "postprocess": update_item_for_packed_item,
+ "condition": lambda doc: doc.parent_item in items_to_map,
},
},
target_doc,
set_missing_values,
)
+ set_delivery_date(doc.items, source_name)
doc.insert()
frappe.db.commit()
purchase_orders.append(doc)
@@ -1427,9 +1460,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
if isinstance(selected_items, str):
selected_items = json.loads(selected_items)
- items_to_map = [
- item.get("item_code") for item in selected_items if item.get("item_code") and item.get("item_code")
- ]
+ items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")]
items_to_map = list(set(items_to_map))
def is_drop_ship_order(target):
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 50bdc294987..6905292420b 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -502,6 +502,7 @@ erpnext.PointOfSale.Controller = class {
() => frappe.dom.freeze(),
() => this.make_new_invoice(),
() => this.item_selector.toggle_component(true),
+ () => this.cart.enable_customer_selection(),
() => frappe.dom.unfreeze(),
]);
},
diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js
index 4a1ebabbccf..1150de86b80 100644
--- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js
+++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.js
@@ -13,7 +13,7 @@ frappe.query_reports["Sales Partner Commission Summary"] = {
fieldname: "doctype",
label: __("Document Type"),
fieldtype: "Select",
- options: "Sales Order\nDelivery Note\nSales Invoice",
+ options: "Sales Order\nDelivery Note\nSales Invoice\nPOS Invoice",
default: "Sales Order",
},
{
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js
index 6809b38c7ed..8373e886ce5 100644
--- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js
@@ -21,7 +21,7 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = {
fieldname: "doctype",
label: __("Document Type"),
fieldtype: "Select",
- options: "Sales Order\nDelivery Note\nSales Invoice",
+ options: "Sales Order\nDelivery Note\nSales Invoice\nPOS Invoice",
default: "Sales Order",
},
{
diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js
index b7bca8ae5fa..f6f7c3f3cf3 100644
--- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js
+++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.js
@@ -13,7 +13,7 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
fieldname: "doctype",
label: __("Document Type"),
fieldtype: "Select",
- options: "Sales Order\nDelivery Note\nSales Invoice",
+ options: "Sales Order\nDelivery Note\nSales Invoice\nPOS Invoice",
default: "Sales Order",
},
{
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index cb415e3813a..31726dff277 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -23,6 +23,7 @@
"use_serial_batch_fields",
"column_break_11",
"serial_and_batch_bundle",
+ "delivered_by_supplier",
"section_break_bgys",
"serial_no",
"column_break_qlha",
@@ -290,13 +291,20 @@
{
"fieldname": "column_break_qlha",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "delivered_by_supplier",
+ "fieldtype": "Check",
+ "label": "Supplier delivers to Customer",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-02-18 13:07:02.789654",
+ "modified": "2025-07-09 19:12:45.850219",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index ceb2fdb0087..5a4f3e7722d 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -27,6 +27,7 @@ class PackedItem(Document):
actual_qty: DF.Float
batch_no: DF.Link | None
conversion_factor: DF.Float
+ delivered_by_supplier: DF.Check
description: DF.TextEditor | None
incoming_rate: DF.Currency
item_code: DF.Link | None
@@ -209,6 +210,7 @@ def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data
pi_row.uom = item_data.stock_uom
pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty)
pi_row.conversion_factor = main_item_row.conversion_factor
+ pi_row.delivered_by_supplier = main_item_row.get("delivered_by_supplier")
if not pi_row.description:
pi_row.description = packing_item.get("description")
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index 68caff40356..0401ba0d954 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -106,7 +106,10 @@ def get_columns(filters):
def validate_filters(filters):
if not (filters.get("item_code") or filters.get("warehouse")):
- sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0])
+ table = frappe.qb.DocType("Stock Ledger Entry")
+ sle_count = flt(
+ frappe.qb.from_(table).select(Count(table.name)).where(table.is_cancelled == 0).run()[0][0]
+ )
if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse"))