diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index b9cde43653a..f81afbd1297 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -169,7 +169,7 @@ class Account(NestedSet): if par.root_type: self.root_type = par.root_type - if self.is_group: + if cint(self.is_group): db_value = self.get_doc_before_save() if db_value: if self.report_type != db_value.report_type: @@ -212,7 +212,7 @@ class Account(NestedSet): if doc_before_save and not doc_before_save.parent_account: throw(_("Root cannot be edited."), RootNotEditable) - if not self.parent_account and not self.is_group: + if not self.parent_account and not cint(self.is_group): throw(_("The root account {0} must be a group").format(frappe.bold(self.name))) def validate_root_company_and_sync_account_to_children(self): @@ -261,7 +261,7 @@ class Account(NestedSet): if self.check_gle_exists(): throw(_("Account with existing transaction cannot be converted to ledger")) - elif self.is_group: + elif cint(self.is_group): if self.account_type and not self.flags.exclude_account_type_check: throw(_("Cannot covert to Group because Account Type is selected.")) elif self.check_if_child_exists(): diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 84b6239a392..1687466096c 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -252,10 +252,6 @@ frappe.treeview_settings["Account"] = { root_company, ]); } else { - const node = treeview.tree.get_selected_node(); - if (node.is_root) { - frappe.throw(__("Cannot create root account.")); - } treeview.new_node(); } }, @@ -274,8 +270,7 @@ frappe.treeview_settings["Account"] = { ].treeview.page.fields_dict.root_company.get_value() || frappe.flags.ignore_root_company_validation) && node.expandable && - !node.hide_add && - !node.is_root + !node.hide_add ); }, click: function () { diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index bbf59aa7f02..76f1e573c41 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -550,7 +550,7 @@ def send_auto_email(): selected = frappe.get_list( "Process Statement Of Accounts", filters={"enable_auto_email": 1}, - or_filters={"to_date": format_date(today()), "posting_date": format_date(today())}, + or_filters={"to_date": today(), "posting_date": today()}, ) for entry in selected: send_emails(entry.name, from_scheduler=True) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 37863036492..11614467472 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -178,7 +178,7 @@ def start_repost(account_repost_doc=str) -> None: if doc.doctype in ["Sales Invoice", "Purchase Invoice"]: if not repost_doc.delete_cancelled_entries: doc.docstatus = 2 - doc.make_gl_entries_on_cancel() + doc.make_gl_entries_on_cancel(from_repost=True) doc.docstatus = 1 if doc.doctype == "Sales Invoice": @@ -190,7 +190,7 @@ def start_repost(account_repost_doc=str) -> None: elif doc.doctype == "Purchase Receipt": if not repost_doc.delete_cancelled_entries: doc.docstatus = 2 - doc.make_gl_entries_on_cancel() + doc.make_gl_entries_on_cancel(from_repost=True) doc.docstatus = 1 doc.make_gl_entries(from_repost=True) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 97ded97dd2e..386d28fd804 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1206,7 +1206,6 @@ class SalesInvoice(SellingController): self.make_exchange_gain_loss_journal() elif self.docstatus == 2: - cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 39faa24c16a..aeb266b6d87 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4668,6 +4668,59 @@ class TestSalesInvoice(FrappeTestCase): doc.db_set("do_not_use_batchwise_valuation", original_value) + def test_system_generated_exchange_gain_or_loss_je_after_repost(self): + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import ( + update_repost_settings, + ) + + update_repost_settings() + + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=80, + ) + + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "10" + pe.reference_date = nowdate() + pe.paid_from_account_currency = si.currency + pe.paid_to_account_currency = "INR" + pe.source_exchange_rate = 85 + pe.target_exchange_rate = 1 + pe.paid_amount = si.outstanding_amount + pe.insert() + pe.submit() + + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = si.company + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.save() + ral.submit() + + je = frappe.qb.DocType("Journal Entry") + jea = frappe.qb.DocType("Journal Entry Account") + q = ( + ( + frappe.qb.from_(je) + .join(jea) + .on(je.name == jea.parent) + .select(je.docstatus) + .where( + (je.voucher_type == "Exchange Gain Or Loss") + & (jea.reference_name == si.name) + & (jea.reference_type == "Sales Invoice") + & (je.is_system_generated == 1) + ) + ) + .limit(1) + .run() + ) + + self.assertEqual(q[0][0], 1) + def make_item_for_si(item_code, properties=None): from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 2bec888729a..94331ef1b92 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -203,6 +203,12 @@ def get_gl_entries(filters, accounting_dimensions): as_dict=1, ) + party_name_map = get_party_name_map() + + for gl_entry in gl_entries: + if gl_entry.party_type and gl_entry.party: + gl_entry.party_name = party_name_map.get(gl_entry.party_type, {}).get(gl_entry.party) + if filters.get("presentation_currency"): return convert_to_presentation_currency(gl_entries, currency_map, filters) else: @@ -337,6 +343,20 @@ def get_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" +def get_party_name_map(): + party_map = {} + + customers = frappe.get_all("Customer", fields=["name", "customer_name"]) + party_map["Customer"] = {c.name: c.customer_name for c in customers} + + suppliers = frappe.get_all("Supplier", fields=["name", "supplier_name"]) + party_map["Supplier"] = {s.name: s.supplier_name for s in suppliers} + + employees = frappe.get_all("Employee", fields=["name", "employee_name"]) + party_map["Employee"] = {e.name: e.employee_name for e in employees} + return party_map + + def get_accounts_with_children(accounts): if not isinstance(accounts, list): accounts = [d.strip() for d in accounts.strip().split(",") if d] @@ -701,6 +721,19 @@ def get_columns(filters): {"label": _("Party"), "fieldname": "party", "width": 100}, ] + supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") + customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") + + if supplier_master_name != "Supplier Name" or customer_master_name != "Customer Name": + columns.append( + { + "label": _("Party Name"), + "fieldname": "party_name", + "fieldtype": "Data", + "width": 150, + } + ) + if filters.get("include_dimensions"): columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index fe17924d527..add7a90b74b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -33,6 +33,7 @@ def execute(filters=None): "invoice_or_item", "customer", "customer_group", + "customer_name", "posting_date", "item_code", "item_name", @@ -95,6 +96,7 @@ def execute(filters=None): "customer": [ "customer", "customer_group", + "customer_name", "qty", "base_rate", "buying_rate", @@ -250,6 +252,10 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_ def get_columns(group_wise_columns, filters): columns = [] + + supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") + customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") + column_map = frappe._dict( { "parent": { @@ -395,6 +401,12 @@ def get_columns(group_wise_columns, filters): "options": "Customer Group", "width": 100, }, + "customer_name": { + "label": _("Customer Name"), + "fieldname": "customer_name", + "fieldtype": "Data", + "width": 150, + }, "territory": { "label": _("Territory"), "fieldname": "territory", @@ -419,6 +431,10 @@ def get_columns(group_wise_columns, filters): ) for col in group_wise_columns.get(scrub(filters.group_by)): + if col == "customer_name" and ( + supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name" + ): + continue columns.append(column_map.get(col)) columns.append( @@ -440,6 +456,7 @@ def get_column_names(): "invoice_or_item": "sales_invoice", "customer": "customer", "customer_group": "customer_group", + "customer_name": "customer_name", "posting_date": "posting_date", "item_code": "item_code", "item_name": "item_name", @@ -905,7 +922,7 @@ class GrossProfitGenerator: `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, `tabSales Invoice`.project, `tabSales Invoice`.update_stock, - `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, + `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice`.base_net_total as "invoice_base_net_total", `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, @@ -1003,6 +1020,7 @@ class GrossProfitGenerator: "update_stock": row.update_stock, "customer": row.customer, "customer_group": row.customer_group, + "customer_name": row.customer_name, "item_code": None, "item_name": None, "description": None, @@ -1032,6 +1050,7 @@ class GrossProfitGenerator: "project": row.project, "customer": row.customer, "customer_group": row.customer_group, + "customer_name": row.customer_name, "item_code": item.item_code, "item_name": item.item_name, "description": item.description, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index bdd5e89dda0..5bdc9900abf 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -578,7 +578,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( }, allow_child_item_selection: true, child_fieldname: "items", - child_columns: ["item_code", "qty", "ordered_qty"], + child_columns: ["item_code", "item_name", "qty", "ordered_qty"], }); }, __("Get Items From") diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e375c2c362c..2369c39f508 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -959,8 +959,9 @@ class StockController(AccountsController): make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher) - def make_gl_entries_on_cancel(self): - cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) + def make_gl_entries_on_cancel(self, from_repost=False): + if not from_repost: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) if frappe.db.sql( """select name from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index b78eae5f109..fcea1cafab9 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -284,6 +284,9 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { this.frm.set_value("currency", frappe.defaults.get_user_default("Currency")); } + if (this.frm.is_new() && this.frm.doc.opportunity_type === undefined) { + this.frm.doc.opportunity_type = __("Sales"); + } this.setup_queries(); } diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index f56286a636c..10df9d91209 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -152,7 +152,6 @@ "no_copy": 1 }, { - "default": "Sales", "fieldname": "opportunity_type", "fieldtype": "Link", "in_list_view": 1, @@ -670,4 +669,4 @@ "title_field": "title", "track_seen": 1, "track_views": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 9229ab22227..91d60c924bf 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -126,6 +126,7 @@ class Opportunity(TransactionBase, CRMNote): link_communications(self.opportunity_from, self.party_name, self) def validate(self): + self.set_opportunity_type() self.make_new_lead_if_required() self.validate_item_details() self.validate_uom_is_integer("uom", "qty") @@ -150,6 +151,10 @@ class Opportunity(TransactionBase, CRMNote): except Exception: continue + def set_opportunity_type(self): + if self.is_new() and not self.opportunity_type: + self.opportunity_type = _("Sales") + def set_exchange_rate(self): company_currency = frappe.get_cached_value("Company", self.company, "default_currency") if self.currency == company_currency: diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index f95946a1724..c3b0bb10fe6 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -23,6 +23,14 @@ frappe.ui.form.on("Job Card", { }; }); + frm.set_query("item_code", "scrap_items", () => { + return { + filters: { + disabled: 0, + }, + }; + }); + frm.set_indicator_formatter("sub_operation", function (doc) { if (doc.status == "Pending") { return "red"; diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 74b899555c0..1087f64276d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -419,3 +419,4 @@ erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-04 execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1) erpnext.patches.v15_0.add_company_payment_gateway_account +erpnext.patches.v15_0.update_uae_zero_rated_fetch diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py new file mode 100644 index 00000000000..57b8db59f97 --- /dev/null +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -0,0 +1,10 @@ +import frappe + +from erpnext.regional.united_arab_emirates.setup import make_custom_fields + + +def execute(): + if not frappe.db.get_value("Company", {"country": "United Arab Emirates"}): + return + + make_custom_fields() diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 1a68d7dec6a..7cf86adbe01 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -143,7 +143,7 @@ def get_total_emiratewise(filters): on i.parent = s.name where - s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 + s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 {conditions} group by s.vat_emirate; diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 36a079546e5..6a8c7b9438b 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -20,6 +20,7 @@ def make_custom_fields(): label="Is Zero Rated", fieldtype="Check", fetch_from="item_code.is_zero_rated", + fetch_if_empty=1, insert_after="description", print_hide=1, ) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index d71b87bd903..19c00a6c798 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -7,10 +7,6 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): - # maybe this should be a standard function rather than a regional one - if not doc.taxes: - return - if not doc.items: return @@ -20,6 +16,29 @@ def update_itemised_tax_data(doc): itemised_tax = get_itemised_tax(doc.taxes) + def determine_if_export(doc): + if doc.doctype != "Sales Invoice": + return False + + if not doc.customer_address: + if not doc.total_taxes_and_charges: + frappe.msgprint( + _("Please set Customer Address to determine if the transaction is an export."), + alert=True, + ) + + return False + + company_country = frappe.get_cached_value("Company", doc.company, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + return True + + return False + + is_export = determine_if_export(doc) + for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 # dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate @@ -30,6 +49,9 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate + if not tax_rate or row.get("is_zero_rated"): + row.is_zero_rated = is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 28756fb1eb6..78b819dc46a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -1219,6 +1219,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex read_only: 1, fieldname: "uom", label: __("UOM"), + options: "UOM", in_list_view: 1, }, { @@ -1292,7 +1293,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ - doctype: "Sales Order Item", name: d.name, item_name: d.item_name, item_code: d.item_code, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b6a979dbc61..9b14616373b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1785,7 +1785,9 @@ def create_pick_list(source_name, target_doc=None): doc.purpose = "Delivery" - doc.set_item_locations() + # Only auto-assign serial numbers if not picking manually + if not doc.pick_manually: + doc.set_item_locations() return doc diff --git a/erpnext/setup/doctype/brand/brand.py b/erpnext/setup/doctype/brand/brand.py index cf3f4744391..ccb8b71e64f 100644 --- a/erpnext/setup/doctype/brand/brand.py +++ b/erpnext/setup/doctype/brand/brand.py @@ -35,7 +35,7 @@ def get_brand_defaults(item, company): for d in brand.brand_defaults or []: if d.company == company: - row = copy.deepcopy(d.as_dict()) + row = d.as_dict(no_private_properties=True) row.pop("name") return row diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 54f57e707af..f68f0eed493 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -90,7 +90,7 @@ def get_item_group_defaults(item, company): for d in item_group.item_group_defaults or []: if d.company == company: - row = copy.deepcopy(d.as_dict()) + row = d.as_dict(no_private_properties=True) row.pop("name") return row diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index be8652f153b..6730d1cbdce 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -26,13 +26,27 @@ class PartyType(Document): @frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): cond = "" + account_type = None + if filters and filters.get("account"): account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") - cond = "and account_type = '%s'" % account_type + if account_type: + if account_type in ["Receivable", "Payable"]: + # Include Employee regardless of its configured account_type, but still respect the text filter + cond = "and (account_type = %(account_type)s or name = 'Employee')" + else: + cond = "and account_type = %(account_type)s" - return frappe.db.sql( + # Build parameters dictionary + params = {"txt": "%" + txt + "%", "start": start, "page_len": page_len} + if account_type: + params["account_type"] = account_type + + result = frappe.db.sql( f"""select name from `tabParty Type` - where `{searchfield}` LIKE %(txt)s {cond} - order by name limit %(page_len)s offset %(start)s""", - {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, + where `{searchfield}` LIKE %(txt)s {cond} + order by name limit %(page_len)s offset %(start)s""", + params, ) + + return result or [] diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 16ac4fd1017..f0923061e93 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -318,7 +318,8 @@ "fieldname": "shelf_life_in_days", "fieldtype": "Int", "label": "Shelf Life In Days", - "mandatory_depends_on": "eval:doc.has_batch_no && doc.has_expiry_date" + "mandatory_depends_on": "eval:doc.has_batch_no && doc.has_expiry_date", + "non_negative": 1 }, { "default": "2099-12-31", @@ -362,7 +363,8 @@ "depends_on": "is_stock_item", "fieldname": "weight_per_unit", "fieldtype": "Float", - "label": "Weight Per Unit" + "label": "Weight Per Unit", + "non_negative": 1 }, { "depends_on": "eval:doc.is_stock_item", @@ -534,13 +536,15 @@ "fieldname": "min_order_qty", "fieldtype": "Float", "label": "Minimum Order Qty", + "non_negative": 1, "oldfieldname": "min_order_qty", "oldfieldtype": "Currency" }, { "fieldname": "safety_stock", "fieldtype": "Float", - "label": "Safety Stock" + "label": "Safety Stock", + "non_negative": 1 }, { "fieldname": "purchase_details_cb", @@ -551,6 +555,7 @@ "fieldname": "lead_time_days", "fieldtype": "Int", "label": "Lead Time in days", + "non_negative": 1, "oldfieldname": "lead_time_days", "oldfieldtype": "Int" }, @@ -559,6 +564,7 @@ "fieldtype": "Float", "label": "Last Purchase Rate", "no_copy": 1, + "non_negative": 1, "oldfieldname": "last_purchase_rate", "oldfieldtype": "Currency", "read_only": 1 @@ -889,7 +895,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2025-02-03 23:43:57.253667", + "modified": "2025-08-08 14:58:48.674193", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -954,6 +960,7 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "item_name,description,item_group,customer_code", "show_name_in_global_search": 1, "show_preview_popup": 1, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3409e9a559a..0ea35ddc1fb 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1281,7 +1281,7 @@ def get_item_defaults(item_code, company): for d in item.item_defaults: if d.company == company: - row = copy.deepcopy(d.as_dict()) + row = d.as_dict(no_private_properties=True) row.pop("name") out.update(row) return out diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index c72fa864960..d021973a01f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -81,6 +81,7 @@ frappe.ui.form.on("Pick List", { }; }); }, + set_item_locations: (frm, save) => { if (!(frm.doc.locations && frm.doc.locations.length)) { frappe.msgprint(__("Add items in the Item Locations table")); @@ -101,11 +102,34 @@ frappe.ui.form.on("Pick List", { }, pick_manually: function (frm) { + // Update warehouse field read-only property frm.fields_dict.locations.grid.update_docfield_property( "warehouse", "read_only", !frm.doc.pick_manually ); + + // Clear auto-assigned serial numbers and related fields when switching to manual picking + if (frm.doc.pick_manually && frm.doc.locations) { + let has_changes = false; + frm.doc.locations.forEach((row) => { + if (row.serial_no || row.batch_no || row.serial_and_batch_bundle) { + row.serial_no = ""; + row.batch_no = ""; + row.serial_and_batch_bundle = ""; + row.picked_qty = 0; + has_changes = true; + } + }); + + if (has_changes) { + frappe.show_alert( + __("Cleared auto-assigned serial numbers and batch numbers for manual picking"), + 3 + ); + frm.refresh_field("locations"); + } + } }, get_item_locations: (frm) => { @@ -273,7 +297,7 @@ frappe.ui.form.on("Pick List", { max_qty_field: "qty", dont_allow_new_row: true, prompt_qty: frm.doc.prompt_qty, - serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field. + serial_no_field: "serial_no", }; const barcode_scanner = new erpnext.utils.BarcodeScanner(opts); barcode_scanner.process_scan(); diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f554f4bd140..2420c166161 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -572,10 +572,18 @@ class PickList(TransactionBase): if not item.item_code: frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") - if not cint( - frappe.get_cached_value("Item", item.item_code, "is_stock_item") - ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}): - continue + + # Check if item is stock item or product bundle + is_stock_item = cint(frappe.get_cached_value("Item", item.item_code, "is_stock_item")) + is_product_bundle = frappe.db.exists( + "Product Bundle", {"new_item_code": item.item_code, "disabled": 0} + ) + + # Include non-stock items for delivery purposes, but skip them for warehouse assignment + if not is_stock_item and not is_product_bundle: + # For non-stock items, set warehouse to None and continue processing + item.warehouse = None + item_code = item.item_code reference = item.sales_order_item or item.material_request_item key = (item_code, item.uom, item.warehouse, item.batch_no, reference) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8d04141d78c..e31ba966653 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -567,20 +567,15 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): or args.get("warehouse") ) - if not warehouse: - defaults = frappe.defaults.get_defaults() or {} - warehouse_exists = frappe.db.exists( - "Warehouse", {"name": defaults.default_warehouse, "company": args.company} - ) - if defaults.get("default_warehouse") and warehouse_exists: - warehouse = defaults.default_warehouse - else: warehouse = args.get("warehouse") if not warehouse: - default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") - if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company: + default_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse") + if ( + default_warehouse + and frappe.get_cached_value("Warehouse", default_warehouse, "company") == args.company + ): return default_warehouse return warehouse