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"))