mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-27 10:38:30 +00:00
Merge pull request #48745 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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")},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -1407,6 +1407,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
]);
|
||||
} else {
|
||||
this.conversion_factor(doc, cdt, cdn, true)
|
||||
this.calculate_taxes_and_totals()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user