Merge pull request #48745 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-07-23 08:23:03 +05:30
committed by GitHub
33 changed files with 237 additions and 116 deletions

View File

@@ -291,12 +291,14 @@ frappe.treeview_settings["Account"] = {
label: __("View Ledger"), label: __("View Ledger"),
click: function (node, btn) { click: function (node, btn) {
frappe.route_options = { frappe.route_options = {
account: node.label,
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], 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], to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
company: company:
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(), 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"); frappe.set_route("query-report", "General Ledger");
}, },
btnClass: "hidden-xs", btnClass: "hidden-xs",

View File

@@ -1044,9 +1044,7 @@ class JournalEntry(AccountsController):
def set_print_format_fields(self): def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0 bank_amount = party_amount = total_amount = 0.0
currency = ( currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
bank_account_currency
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
party_type = None party_type = None
for d in self.get("accounts"): for d in self.get("accounts"):
if d.party_type in ["Customer", "Supplier"] and d.party: if d.party_type in ["Customer", "Supplier"] and d.party:

View File

@@ -580,6 +580,18 @@ class TestJournalEntry(unittest.TestCase):
] ]
self.assertEqual(expected, actual) 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( def make_journal_entry(
account1, account1,

View File

@@ -1443,6 +1443,8 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate", "fieldname": "commission_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Commission Rate (%)", "label": "Commission Rate (%)",
@@ -1571,7 +1573,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-11-26 13:10:50.309570", "modified": "2025-07-17 16:51:40.886083",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -337,6 +337,11 @@ class POSInvoiceMergeLog(Document):
invoice.flags.ignore_pos_profile = True invoice.flags.ignore_pos_profile = True
invoice.pos_profile = "" invoice.pos_profile = ""
# Unset Commission Section
invoice.set("sales_partner", None)
invoice.set("commission_rate", 0)
invoice.set("total_commission", 0)
return invoice return invoice
def get_new_sales_invoice(self): def get_new_sales_invoice(self):

View File

@@ -1223,6 +1223,9 @@ class PurchaseInvoice(BuyingController):
def get_provisional_accounts(self): def get_provisional_accounts(self):
self.provisional_accounts = frappe._dict() self.provisional_accounts = frappe._dict()
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt]) 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( pr_items = frappe.get_all(
"Purchase Receipt Item", "Purchase Receipt Item",
filters={"parent": ("in", linked_purchase_receipts)}, filters={"parent": ("in", linked_purchase_receipts)},

View File

@@ -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")), "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"): if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book") 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): def get_condition(dimension):
conditions = [] 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 "" return " and {}".format(" and ".join(conditions)) if conditions else ""

View File

@@ -204,7 +204,7 @@ def get_gl_entries(filters, accounting_dimensions):
) )
if filters.get("presentation_currency"): 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: else:
return gl_entries return gl_entries

View File

@@ -178,7 +178,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice", "fieldname": "invoice",
"fieldtype": "Link", "fieldtype": "Link",
"options": "Purchase Invoice", "options": "Purchase Invoice",
"width": 120, "width": 150,
}, },
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, {"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): def get_items(filters, additional_table_columns):
doctype = "Purchase Invoice" doctype = "Purchase Invoice"
pi = frappe.qb.DocType(doctype) pi = frappe.qb.DocType(doctype).as_("invoice")
pii = frappe.qb.DocType(f"{doctype} Item") pii = frappe.qb.DocType(f"{doctype} Item").as_("invoice_item")
Item = frappe.qb.DocType("Item") Item = frappe.qb.DocType("Item")
query = ( query = (
frappe.qb.from_(pi) frappe.qb.from_(pi)
@@ -331,6 +331,7 @@ def get_items(filters, additional_table_columns):
pi.unrealized_profit_loss_account, pi.unrealized_profit_loss_account,
pii.item_code, pii.item_code,
pii.description, pii.description,
pii.item_name,
pii.item_group, pii.item_group,
pii.item_name.as_("pi_item_name"), pii.item_name.as_("pi_item_name"),
pii.item_group.as_("pi_item_group"), pii.item_group.as_("pi_item_group"),
@@ -374,7 +375,7 @@ def get_items(filters, additional_table_columns):
if match_conditions: if match_conditions:
query += " and " + 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) return frappe.db.sql(query, params, as_dict=True)

View File

@@ -198,7 +198,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice", "fieldname": "invoice",
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Invoice", "options": "Sales Invoice",
"width": 120, "width": 150,
}, },
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
] ]
@@ -343,7 +343,7 @@ def get_columns(additional_table_columns, filters):
return columns 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"): for opts in ("company", "customer"):
if filters.get(opts): if filters.get(opts):
query = query.where(si[opts] == filters[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")) query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"): if filters.get("mode_of_payment"):
sales_invoice = frappe.db.get_all( query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
)
query = query.where(si.name.isin(sales_invoice))
if filters.get("warehouse"): if filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): 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 return query
def apply_order_by_conditions(query, si, ii, filters): def apply_order_by_conditions(query, filters):
if not filters.get("group_by"): 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": 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": 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": 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"): elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by")) filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc" 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): def get_items(filters, additional_query_columns, additional_conditions=None):
doctype = "Sales Invoice" doctype = "Sales Invoice"
si = frappe.qb.DocType(doctype) si = frappe.qb.DocType("Sales Invoice").as_("invoice")
sii = frappe.qb.DocType(f"{doctype} Item") sii = frappe.qb.DocType("Sales Invoice Item").as_("invoice_item")
sip = frappe.qb.DocType("Sales Invoice Payment")
item = frappe.qb.DocType("Item") item = frappe.qb.DocType("Item")
query = ( query = (
frappe.qb.from_(si) frappe.qb.from_(si)
.join(sii) .join(sii)
.on(si.name == sii.parent) .on(si.name == sii.parent)
.left_join(sip)
.on(sip.parent == si.name)
.left_join(item) .left_join(item)
.on(sii.item_code == item.name) .on(sii.item_code == item.name)
.select( .select(
@@ -462,6 +462,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.update_stock, si.update_stock,
sii.uom, sii.uom,
sii.qty, sii.qty,
sip.mode_of_payment,
) )
.where(si.docstatus == 1) .where(si.docstatus == 1)
.where(sii.parenttype == doctype) .where(sii.parenttype == doctype)
@@ -481,7 +482,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
if filters.get("customer_group"): if filters.get("customer_group"):
query = query.where(si.customer_group == filters["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 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: if match_conditions:
query += " and " + 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) 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): def get_display_value(filters, group_by_field, item):
if filters.get("group_by") == "Item": if filters.get("group_by") == "Item":
if item.get("item_code") != item.get("item_name"): if item.get("item_code") != item.get("item_name"):
value = ( value = f"{item.get('item_code')}: {item.get('item_name')}"
cstr(item.get("item_code"))
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ cstr(item.get("item_name"))
+ "</span>"
)
else: else:
value = item.get("item_code", "") value = item.get("item_code", "")
elif filters.get("group_by") in ("Customer", "Supplier"): elif filters.get("group_by") in ("Customer", "Supplier"):
party = frappe.scrub(filters.get("group_by")) party = frappe.scrub(filters.get("group_by"))
if item.get(party) != item.get(party + "_name"): if item.get(party) != item.get(party + "_name"):
value = ( value = f"{item.get(party)}: {item.get(party + '_name')}"
item.get(party)
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ item.get(party + "_name")
+ "</span>"
)
else: else:
value = item.get(party) value = item.get(party)
else: else:

View File

@@ -1,21 +1,22 @@
{ {
"add_total_row": 0, "add_total_row": 0,
"add_translate_data": 0,
"columns": [], "columns": [],
"creation": "2013-05-06 12:28:23", "creation": "2013-05-06 12:28:23",
"disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [], "filters": [],
"idx": 3, "idx": 6,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2021-10-06 06:26:07.881340", "letterhead": null,
"modified": "2025-07-17 23:16:19.892044",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Partners Commission", "name": "Sales Partners Commission",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "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", "ref_doctype": "Sales Invoice",
"report_name": "Sales Partners Commission", "report_name": "Sales Partners Commission",
"report_type": "Query Report", "report_type": "Query Report",
@@ -26,5 +27,6 @@
{ {
"role": "Accounts User" "role": "Accounts User"
} }
] ],
} "timeout": 0
}

View File

@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate 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 Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`. in `currency_info`.
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
company_currency = currency_info["company_currency"] company_currency = currency_info["company_currency"]
account_currencies = list(set(entry["account_currency"] for entry in gl_entries)) 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: for entry in gl_entries:
debit = flt(entry["debit"]) 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"]) credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["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["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency entry["credit"] = credit_in_account_currency
else: else:

View File

@@ -512,7 +512,8 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict, dimensions_dict=dimensions_dict,
) )
if referenced_row.get("outstanding_amount"):
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)

View File

@@ -447,13 +447,12 @@ class BuyingController(SubcontractingController):
raise_error_if_no_rate=False, 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: else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate" field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
d.sales_incoming_rate = flt( d.sales_incoming_rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1), * (d.conversion_factor or 1)
d.precision("rate"),
) )
def validate_for_subcontracting(self): def validate_for_subcontracting(self):

View File

@@ -42,14 +42,14 @@ def employee_query(
ptype="select" if frappe.only_has_select_perm(doctype) else "read", 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) mcond = "" if ignore_permissions else get_match_cond(doctype)
return frappe.db.sql( return frappe.db.sql(
"""select {fields} from `tabEmployee` """select {fields} from `tabEmployee`
where status in ('Active', 'Suspended') where status in ('Active', 'Suspended')
and docstatus < 2 and docstatus < 2
and ({key} like %(txt)s and ({key} like %(txt)s or {search_conditions})
or employee_name like %(txt)s)
{fcond} {mcond} {fcond} {mcond}
order by order by
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end), (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
@@ -62,6 +62,7 @@ def employee_query(
"key": searchfield, "key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions), "fcond": get_filters_cond(doctype, filters, conditions),
"mcond": mcond, "mcond": mcond,
"search_conditions": search_conditions,
} }
), ),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},

View File

@@ -4,7 +4,7 @@
from collections import defaultdict from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils import cint, flt, format_datetime, get_datetime from frappe.utils import cint, flt, format_datetime, get_datetime
@@ -40,11 +40,12 @@ def validate_return_against(doc):
frappe.throw( frappe.throw(
_("The {0} {1} does not match with the {0} {2} in the {3} {4}").format( _("The {0} {1} does not match with the {0} {2} in the {3} {4}").format(
doc.meta.get_label(party_type), doc.meta.get_label(party_type),
doc.get(party_type), bold(doc.get(party_type)),
ref_doc.get(party_type), bold(ref_doc.get(party_type)),
ref_doc.doctype, ref_doc.doctype,
ref_doc.name, ref_doc.name,
) ),
title=_("Party Mismatch"),
) )
if ( if (

View File

@@ -68,10 +68,13 @@ class SellingController(StockController):
serial_nos = frappe.get_all( serial_nos = frappe.get_all(
"Serial and Batch Entry", "Serial and Batch Entry",
filters={"parent": ("in", bundle_ids)}, filters={"parent": ("in", bundle_ids), "serial_no": ("is", "set")},
pluck="serial_no", pluck="serial_no",
) )
if not serial_nos:
return
if serial_nos := frappe.get_all( if serial_nos := frappe.get_all(
"Serial No", "Serial No",
filters={"name": ("in", serial_nos), "customer": ("is", "set")}, filters={"name": ("in", serial_nos), "customer": ("is", "set")},

View File

@@ -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 () { frm.set_query("material_request", "material_requests", function () {
return { return {
filters: { filters: {

View File

@@ -2788,56 +2788,45 @@ class TestWorkOrder(FrappeTestCase):
fg_warehouse="_Test Warehouse 2 - _TC", 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 = 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() wo.submit()
# Test 2 : Sort line items in child table based on sequence ID # Test 2 : If none of the operations have sequence ID then they will be sequenced as per their idx
self.assertEqual(wo.operations[0].operation, "Test Operation A") for op in wo.operations:
self.assertEqual(wo.operations[1].operation, "Test Operation B") self.assertEqual(op.sequence_id, op.idx)
self.assertEqual(wo.operations[2].operation, "Test Operation D")
self.assertEqual(wo.operations[3].operation, "Test Operation C")
wo = frappe.copy_doc(wo) wo = frappe.copy_doc(wo)
wo.operations[3].sequence_id = 1 wo.operations[0].sequence_id = 2
wo.submit()
self.assertEqual(wo.operations[0].operation, "Test Operation A") # Test 3 : Sequence IDs should not miss the correct sequence of numbers
self.assertEqual(wo.operations[1].operation, "Test Operation C") self.assertRaises(frappe.ValidationError, wo.submit)
self.assertEqual(wo.operations[2].operation, "Test Operation B")
self.assertEqual(wo.operations[3].operation, "Test Operation D")
wo = frappe.copy_doc(wo) wo.operations[1].sequence_id = 1
wo.operations[0].sequence_id = 3
wo.submit()
self.assertEqual(wo.operations[0].operation, "Test Operation C") # Test 4 : Sequence IDs should be in the correct ascending order
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
self.assertRaises(frappe.ValidationError, wo.submit) self.assertRaises(frappe.ValidationError, wo.submit)
workstation = frappe.get_doc("Workstation", "Test Workstation A") workstation = frappe.get_doc("Workstation", "Test Workstation A")
workstation.production_capacity = 4 workstation.production_capacity = 4
workstation.save() workstation.save()
wo = frappe.copy_doc(wo) wo = frappe.copy_doc(wo)
wo.operations[0].sequence_id = 1
wo.operations[1].sequence_id = 2 wo.operations[1].sequence_id = 2
wo.operations[2].sequence_id = 2
wo.operations[3].sequence_id = 3
wo.submit() wo.submit()
# Test 4 - If Sequence ID is same then planned start time for both operations should be same # 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[1].planned_start_time, wo.operations[2].planned_start_time) 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): def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):

View File

@@ -419,7 +419,7 @@ frappe.ui.form.on("Work Order", {
frm.doc.material_transferred_for_manufacturing - frm.doc.material_transferred_for_manufacturing -
frm.doc.produced_qty - frm.doc.produced_qty -
frm.doc.process_loss_qty; frm.doc.process_loss_qty;
if (pending_complete) { if (pending_complete > 0) {
var width = (pending_complete / frm.doc.qty) * 100 - added_min; var width = (pending_complete / frm.doc.qty) * 100 - added_min;
title = __("{0} items in progress", [pending_complete]); title = __("{0} items in progress", [pending_complete]);
bars.push({ bars.push({
@@ -829,6 +829,19 @@ erpnext.work_order = {
description: __("Max: {0}", [max]), description: __("Max: {0}", [max]),
default: 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") { if (purpose === "Disassemble") {
@@ -850,7 +863,7 @@ erpnext.work_order = {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
frappe.prompt( frm.qty_prompt = frappe.prompt(
fields, fields,
(data) => { (data) => {
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;

View File

@@ -168,6 +168,31 @@ class WorkOrder(Document):
validate_uom_is_integer(self, "stock_uom", ["required_qty"]) validate_uom_is_integer(self, "stock_uom", ["required_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items"))) 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): def set_warehouses(self):
for row in self.required_items: for row in self.required_items:
@@ -637,17 +662,6 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 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): for idx, row in enumerate(self.operations):
qty = self.qty qty = self.qty
while qty > 0: while qty > 0:

View File

@@ -194,6 +194,7 @@
"fieldname": "sequence_id", "fieldname": "sequence_id",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Sequence ID", "label": "Sequence ID",
"non_negative": 1,
"print_hide": 1 "print_hide": 1
}, },
{ {
@@ -224,7 +225,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-04-09 16:21:47.110564", "modified": "2025-05-15 15:10:06.885440",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",

View File

@@ -413,3 +413,4 @@ erpnext.patches.v15_0.update_pick_list_fields
erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.update_pegged_currencies
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log 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.rename_price_list_to_buying_price_list
erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice

View File

@@ -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()

View File

@@ -1407,6 +1407,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
]); ]);
} else { } else {
this.conversion_factor(doc, cdt, cdn, true) this.conversion_factor(doc, cdt, cdn, true)
this.calculate_taxes_and_totals()
} }
} }

View File

@@ -771,6 +771,14 @@ class SalesOrder(SellingController):
voucher_type=self.doctype, voucher_no=self.name, sre_list=sre_list, notify=notify 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: def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
"""Returns the unreserved quantity for the Sales Order Item.""" """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.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty)
target.project = source_parent.project 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 = [item.get("supplier") for item in selected_items if item.get("supplier")]
suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order 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 "condition": lambda doc: doc.ordered_qty < doc.stock_qty
and doc.supplier == supplier and doc.supplier == supplier
and doc.item_code in items_to_map 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, target_doc,
set_missing_values, set_missing_values,
) )
set_delivery_date(doc.items, source_name)
doc.insert() doc.insert()
frappe.db.commit() frappe.db.commit()
purchase_orders.append(doc) 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): if isinstance(selected_items, str):
selected_items = json.loads(selected_items) selected_items = json.loads(selected_items)
items_to_map = [ items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")]
item.get("item_code") for item in selected_items if item.get("item_code") and item.get("item_code")
]
items_to_map = list(set(items_to_map)) items_to_map = list(set(items_to_map))
def is_drop_ship_order(target): def is_drop_ship_order(target):

View File

@@ -502,6 +502,7 @@ erpnext.PointOfSale.Controller = class {
() => frappe.dom.freeze(), () => frappe.dom.freeze(),
() => this.make_new_invoice(), () => this.make_new_invoice(),
() => this.item_selector.toggle_component(true), () => this.item_selector.toggle_component(true),
() => this.cart.enable_customer_selection(),
() => frappe.dom.unfreeze(), () => frappe.dom.unfreeze(),
]); ]);
}, },

View File

@@ -13,7 +13,7 @@ frappe.query_reports["Sales Partner Commission Summary"] = {
fieldname: "doctype", fieldname: "doctype",
label: __("Document Type"), label: __("Document Type"),
fieldtype: "Select", fieldtype: "Select",
options: "Sales Order\nDelivery Note\nSales Invoice", options: "Sales Order\nDelivery Note\nSales Invoice\nPOS Invoice",
default: "Sales Order", default: "Sales Order",
}, },
{ {

View File

@@ -21,7 +21,7 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = {
fieldname: "doctype", fieldname: "doctype",
label: __("Document Type"), label: __("Document Type"),
fieldtype: "Select", fieldtype: "Select",
options: "Sales Order\nDelivery Note\nSales Invoice", options: "Sales Order\nDelivery Note\nSales Invoice\nPOS Invoice",
default: "Sales Order", default: "Sales Order",
}, },
{ {

View File

@@ -13,7 +13,7 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
fieldname: "doctype", fieldname: "doctype",
label: __("Document Type"), label: __("Document Type"),
fieldtype: "Select", fieldtype: "Select",
options: "Sales Order\nDelivery Note\nSales Invoice", options: "Sales Order\nDelivery Note\nSales Invoice\nPOS Invoice",
default: "Sales Order", default: "Sales Order",
}, },
{ {

View File

@@ -23,6 +23,7 @@
"use_serial_batch_fields", "use_serial_batch_fields",
"column_break_11", "column_break_11",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"delivered_by_supplier",
"section_break_bgys", "section_break_bgys",
"serial_no", "serial_no",
"column_break_qlha", "column_break_qlha",
@@ -290,13 +291,20 @@
{ {
"fieldname": "column_break_qlha", "fieldname": "column_break_qlha",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"label": "Supplier delivers to Customer",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-02-18 13:07:02.789654", "modified": "2025-07-09 19:12:45.850219",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Packed Item", "name": "Packed Item",

View File

@@ -27,6 +27,7 @@ class PackedItem(Document):
actual_qty: DF.Float actual_qty: DF.Float
batch_no: DF.Link | None batch_no: DF.Link | None
conversion_factor: DF.Float conversion_factor: DF.Float
delivered_by_supplier: DF.Check
description: DF.TextEditor | None description: DF.TextEditor | None
incoming_rate: DF.Currency incoming_rate: DF.Currency
item_code: DF.Link | None 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.uom = item_data.stock_uom
pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty) pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty)
pi_row.conversion_factor = main_item_row.conversion_factor 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: if not pi_row.description:
pi_row.description = packing_item.get("description") pi_row.description = packing_item.get("description")

View File

@@ -106,7 +106,10 @@ def get_columns(filters):
def validate_filters(filters): def validate_filters(filters):
if not (filters.get("item_code") or filters.get("warehouse")): 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: if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse")) frappe.throw(_("Please set filter based on Item or Warehouse"))