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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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",

View File

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

View File

@@ -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)},

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

View File

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

View File

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

View File

@@ -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"))
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ cstr(item.get("item_name"))
+ "</span>"
)
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)
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ item.get(party + "_name")
+ "</span>"
)
value = f"{item.get(party)}: {item.get(party + '_name')}"
else:
value = item.get(party)
else:

View File

@@ -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"
}
]
}
],
"timeout": 0
}

View File

@@ -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:

View File

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

View File

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

View File

@@ -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},

View File

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

View File

@@ -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")},

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 () {
return {
filters: {

View File

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

View File

@@ -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;

View File

@@ -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:

View File

@@ -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",

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.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

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 {
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
)
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):

View File

@@ -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(),
]);
},

View File

@@ -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",
},
{

View File

@@ -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",
},
{

View File

@@ -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",
},
{

View File

@@ -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",

View File

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

View File

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