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

chore: release v15
This commit is contained in:
ruthra kumar
2025-08-12 17:33:04 +05:30
committed by GitHub
29 changed files with 252 additions and 53 deletions

View File

@@ -169,7 +169,7 @@ class Account(NestedSet):
if par.root_type: if par.root_type:
self.root_type = 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() db_value = self.get_doc_before_save()
if db_value: if db_value:
if self.report_type != db_value.report_type: 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: if doc_before_save and not doc_before_save.parent_account:
throw(_("Root cannot be edited."), RootNotEditable) 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))) throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
def validate_root_company_and_sync_account_to_children(self): def validate_root_company_and_sync_account_to_children(self):
@@ -261,7 +261,7 @@ class Account(NestedSet):
if self.check_gle_exists(): if self.check_gle_exists():
throw(_("Account with existing transaction cannot be converted to ledger")) 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: if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected.")) throw(_("Cannot covert to Group because Account Type is selected."))
elif self.check_if_child_exists(): elif self.check_if_child_exists():

View File

@@ -252,10 +252,6 @@ frappe.treeview_settings["Account"] = {
root_company, root_company,
]); ]);
} else { } else {
const node = treeview.tree.get_selected_node();
if (node.is_root) {
frappe.throw(__("Cannot create root account."));
}
treeview.new_node(); treeview.new_node();
} }
}, },
@@ -274,8 +270,7 @@ frappe.treeview_settings["Account"] = {
].treeview.page.fields_dict.root_company.get_value() || ].treeview.page.fields_dict.root_company.get_value() ||
frappe.flags.ignore_root_company_validation) && frappe.flags.ignore_root_company_validation) &&
node.expandable && node.expandable &&
!node.hide_add && !node.hide_add
!node.is_root
); );
}, },
click: function () { click: function () {

View File

@@ -550,7 +550,7 @@ def send_auto_email():
selected = frappe.get_list( selected = frappe.get_list(
"Process Statement Of Accounts", "Process Statement Of Accounts",
filters={"enable_auto_email": 1}, 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: for entry in selected:
send_emails(entry.name, from_scheduler=True) send_emails(entry.name, from_scheduler=True)

View File

@@ -178,7 +178,7 @@ def start_repost(account_repost_doc=str) -> None:
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]: if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
if not repost_doc.delete_cancelled_entries: if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2 doc.docstatus = 2
doc.make_gl_entries_on_cancel() doc.make_gl_entries_on_cancel(from_repost=True)
doc.docstatus = 1 doc.docstatus = 1
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
@@ -190,7 +190,7 @@ def start_repost(account_repost_doc=str) -> None:
elif doc.doctype == "Purchase Receipt": elif doc.doctype == "Purchase Receipt":
if not repost_doc.delete_cancelled_entries: if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2 doc.docstatus = 2
doc.make_gl_entries_on_cancel() doc.make_gl_entries_on_cancel(from_repost=True)
doc.docstatus = 1 doc.docstatus = 1
doc.make_gl_entries(from_repost=True) doc.make_gl_entries(from_repost=True)

View File

@@ -1206,7 +1206,6 @@ class SalesInvoice(SellingController):
self.make_exchange_gain_loss_journal() self.make_exchange_gain_loss_journal()
elif self.docstatus == 2: 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) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No": if update_outstanding == "No":

View File

@@ -4668,6 +4668,59 @@ class TestSalesInvoice(FrappeTestCase):
doc.db_set("do_not_use_batchwise_valuation", original_value) 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): def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -203,6 +203,12 @@ def get_gl_entries(filters, accounting_dimensions):
as_dict=1, 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"): if filters.get("presentation_currency"):
return convert_to_presentation_currency(gl_entries, currency_map, filters) return convert_to_presentation_currency(gl_entries, currency_map, filters)
else: else:
@@ -337,6 +343,20 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else "" 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): def get_accounts_with_children(accounts):
if not isinstance(accounts, list): if not isinstance(accounts, list):
accounts = [d.strip() for d in accounts.strip().split(",") if d] 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}, {"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"): if filters.get("include_dimensions"):
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}) columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})

View File

@@ -33,6 +33,7 @@ def execute(filters=None):
"invoice_or_item", "invoice_or_item",
"customer", "customer",
"customer_group", "customer_group",
"customer_name",
"posting_date", "posting_date",
"item_code", "item_code",
"item_name", "item_name",
@@ -95,6 +96,7 @@ def execute(filters=None):
"customer": [ "customer": [
"customer", "customer",
"customer_group", "customer_group",
"customer_name",
"qty", "qty",
"base_rate", "base_rate",
"buying_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): def get_columns(group_wise_columns, filters):
columns = [] 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( column_map = frappe._dict(
{ {
"parent": { "parent": {
@@ -395,6 +401,12 @@ def get_columns(group_wise_columns, filters):
"options": "Customer Group", "options": "Customer Group",
"width": 100, "width": 100,
}, },
"customer_name": {
"label": _("Customer Name"),
"fieldname": "customer_name",
"fieldtype": "Data",
"width": 150,
},
"territory": { "territory": {
"label": _("Territory"), "label": _("Territory"),
"fieldname": "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)): 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(column_map.get(col))
columns.append( columns.append(
@@ -440,6 +456,7 @@ def get_column_names():
"invoice_or_item": "sales_invoice", "invoice_or_item": "sales_invoice",
"customer": "customer", "customer": "customer",
"customer_group": "customer_group", "customer_group": "customer_group",
"customer_name": "customer_name",
"posting_date": "posting_date", "posting_date": "posting_date",
"item_code": "item_code", "item_code": "item_code",
"item_name": "item_name", "item_name": "item_name",
@@ -905,7 +922,7 @@ class GrossProfitGenerator:
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
`tabSales Invoice`.project, `tabSales Invoice`.update_stock, `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`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice`.base_net_total as "invoice_base_net_total", `tabSales Invoice`.base_net_total as "invoice_base_net_total",
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
@@ -1003,6 +1020,7 @@ class GrossProfitGenerator:
"update_stock": row.update_stock, "update_stock": row.update_stock,
"customer": row.customer, "customer": row.customer,
"customer_group": row.customer_group, "customer_group": row.customer_group,
"customer_name": row.customer_name,
"item_code": None, "item_code": None,
"item_name": None, "item_name": None,
"description": None, "description": None,
@@ -1032,6 +1050,7 @@ class GrossProfitGenerator:
"project": row.project, "project": row.project,
"customer": row.customer, "customer": row.customer,
"customer_group": row.customer_group, "customer_group": row.customer_group,
"customer_name": row.customer_name,
"item_code": item.item_code, "item_code": item.item_code,
"item_name": item.item_name, "item_name": item.item_name,
"description": item.description, "description": item.description,

View File

@@ -578,7 +578,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}, },
allow_child_item_selection: true, allow_child_item_selection: true,
child_fieldname: "items", child_fieldname: "items",
child_columns: ["item_code", "qty", "ordered_qty"], child_columns: ["item_code", "item_name", "qty", "ordered_qty"],
}); });
}, },
__("Get Items From") __("Get Items From")

View File

@@ -959,8 +959,9 @@ class StockController(AccountsController):
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) 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) update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
def make_gl_entries_on_cancel(self): def make_gl_entries_on_cancel(self, from_repost=False):
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) if not from_repost:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
if frappe.db.sql( if frappe.db.sql(
"""select name from `tabGL Entry` where voucher_type=%s """select name from `tabGL Entry` where voucher_type=%s
and voucher_no=%s""", and voucher_no=%s""",

View File

@@ -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")); 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(); this.setup_queries();
} }

View File

@@ -152,7 +152,6 @@
"no_copy": 1 "no_copy": 1
}, },
{ {
"default": "Sales",
"fieldname": "opportunity_type", "fieldname": "opportunity_type",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@@ -670,4 +669,4 @@
"title_field": "title", "title_field": "title",
"track_seen": 1, "track_seen": 1,
"track_views": 1 "track_views": 1
} }

View File

@@ -126,6 +126,7 @@ class Opportunity(TransactionBase, CRMNote):
link_communications(self.opportunity_from, self.party_name, self) link_communications(self.opportunity_from, self.party_name, self)
def validate(self): def validate(self):
self.set_opportunity_type()
self.make_new_lead_if_required() self.make_new_lead_if_required()
self.validate_item_details() self.validate_item_details()
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
@@ -150,6 +151,10 @@ class Opportunity(TransactionBase, CRMNote):
except Exception: except Exception:
continue continue
def set_opportunity_type(self):
if self.is_new() and not self.opportunity_type:
self.opportunity_type = _("Sales")
def set_exchange_rate(self): def set_exchange_rate(self):
company_currency = frappe.get_cached_value("Company", self.company, "default_currency") company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
if self.currency == company_currency: if self.currency == company_currency:

View File

@@ -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) { frm.set_indicator_formatter("sub_operation", function (doc) {
if (doc.status == "Pending") { if (doc.status == "Pending") {
return "red"; return "red";

View File

@@ -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 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) 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.add_company_payment_gateway_account
erpnext.patches.v15_0.update_uae_zero_rated_fetch

View File

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

View File

@@ -143,7 +143,7 @@ def get_total_emiratewise(filters):
on on
i.parent = s.name i.parent = s.name
where 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} {conditions}
group by group by
s.vat_emirate; s.vat_emirate;

View File

@@ -20,6 +20,7 @@ def make_custom_fields():
label="Is Zero Rated", label="Is Zero Rated",
fieldtype="Check", fieldtype="Check",
fetch_from="item_code.is_zero_rated", fetch_from="item_code.is_zero_rated",
fetch_if_empty=1,
insert_after="description", insert_after="description",
print_hide=1, print_hide=1,
) )

View File

@@ -7,10 +7,6 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax
def update_itemised_tax_data(doc): 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: if not doc.items:
return return
@@ -20,6 +16,29 @@ def update_itemised_tax_data(doc):
itemised_tax = get_itemised_tax(doc.taxes) 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: for row in doc.items:
tax_rate, tax_amount = 0.0, 0.0 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 # 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_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount"))
tax_rate += _tax_rate 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_rate = flt(tax_rate, row.precision("tax_rate"))
row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.tax_amount = flt(tax_amount, row.precision("tax_amount"))
row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))

View File

@@ -1219,6 +1219,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
read_only: 1, read_only: 1,
fieldname: "uom", fieldname: "uom",
label: __("UOM"), label: __("UOM"),
options: "UOM",
in_list_view: 1, 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); let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) { if (pending_qty > 0) {
po_items.push({ po_items.push({
doctype: "Sales Order Item",
name: d.name, name: d.name,
item_name: d.item_name, item_name: d.item_name,
item_code: d.item_code, item_code: d.item_code,

View File

@@ -1785,7 +1785,9 @@ def create_pick_list(source_name, target_doc=None):
doc.purpose = "Delivery" 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 return doc

View File

@@ -35,7 +35,7 @@ def get_brand_defaults(item, company):
for d in brand.brand_defaults or []: for d in brand.brand_defaults or []:
if d.company == company: if d.company == company:
row = copy.deepcopy(d.as_dict()) row = d.as_dict(no_private_properties=True)
row.pop("name") row.pop("name")
return row return row

View File

@@ -90,7 +90,7 @@ def get_item_group_defaults(item, company):
for d in item_group.item_group_defaults or []: for d in item_group.item_group_defaults or []:
if d.company == company: if d.company == company:
row = copy.deepcopy(d.as_dict()) row = d.as_dict(no_private_properties=True)
row.pop("name") row.pop("name")
return row return row

View File

@@ -26,13 +26,27 @@ class PartyType(Document):
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_party_type(doctype, txt, searchfield, start, page_len, filters): def get_party_type(doctype, txt, searchfield, start, page_len, filters):
cond = "" cond = ""
account_type = None
if filters and filters.get("account"): if filters and filters.get("account"):
account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") 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` f"""select name from `tabParty Type`
where `{searchfield}` LIKE %(txt)s {cond} where `{searchfield}` LIKE %(txt)s {cond}
order by name limit %(page_len)s offset %(start)s""", order by name limit %(page_len)s offset %(start)s""",
{"txt": "%" + txt + "%", "start": start, "page_len": page_len}, params,
) )
return result or []

View File

@@ -318,7 +318,8 @@
"fieldname": "shelf_life_in_days", "fieldname": "shelf_life_in_days",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Shelf Life In Days", "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", "default": "2099-12-31",
@@ -362,7 +363,8 @@
"depends_on": "is_stock_item", "depends_on": "is_stock_item",
"fieldname": "weight_per_unit", "fieldname": "weight_per_unit",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Weight Per Unit" "label": "Weight Per Unit",
"non_negative": 1
}, },
{ {
"depends_on": "eval:doc.is_stock_item", "depends_on": "eval:doc.is_stock_item",
@@ -534,13 +536,15 @@
"fieldname": "min_order_qty", "fieldname": "min_order_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Minimum Order Qty", "label": "Minimum Order Qty",
"non_negative": 1,
"oldfieldname": "min_order_qty", "oldfieldname": "min_order_qty",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
{ {
"fieldname": "safety_stock", "fieldname": "safety_stock",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Safety Stock" "label": "Safety Stock",
"non_negative": 1
}, },
{ {
"fieldname": "purchase_details_cb", "fieldname": "purchase_details_cb",
@@ -551,6 +555,7 @@
"fieldname": "lead_time_days", "fieldname": "lead_time_days",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Lead Time in days", "label": "Lead Time in days",
"non_negative": 1,
"oldfieldname": "lead_time_days", "oldfieldname": "lead_time_days",
"oldfieldtype": "Int" "oldfieldtype": "Int"
}, },
@@ -559,6 +564,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Last Purchase Rate", "label": "Last Purchase Rate",
"no_copy": 1, "no_copy": 1,
"non_negative": 1,
"oldfieldname": "last_purchase_rate", "oldfieldname": "last_purchase_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"read_only": 1 "read_only": 1
@@ -889,7 +895,7 @@
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2025-02-03 23:43:57.253667", "modified": "2025-08-08 14:58:48.674193",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",
@@ -954,6 +960,7 @@
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "item_name,description,item_group,customer_code", "search_fields": "item_name,description,item_group,customer_code",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"show_preview_popup": 1, "show_preview_popup": 1,

View File

@@ -1281,7 +1281,7 @@ def get_item_defaults(item_code, company):
for d in item.item_defaults: for d in item.item_defaults:
if d.company == company: if d.company == company:
row = copy.deepcopy(d.as_dict()) row = d.as_dict(no_private_properties=True)
row.pop("name") row.pop("name")
out.update(row) out.update(row)
return out return out

View File

@@ -81,6 +81,7 @@ frappe.ui.form.on("Pick List", {
}; };
}); });
}, },
set_item_locations: (frm, save) => { set_item_locations: (frm, save) => {
if (!(frm.doc.locations && frm.doc.locations.length)) { if (!(frm.doc.locations && frm.doc.locations.length)) {
frappe.msgprint(__("Add items in the Item Locations table")); frappe.msgprint(__("Add items in the Item Locations table"));
@@ -101,11 +102,34 @@ frappe.ui.form.on("Pick List", {
}, },
pick_manually: function (frm) { pick_manually: function (frm) {
// Update warehouse field read-only property
frm.fields_dict.locations.grid.update_docfield_property( frm.fields_dict.locations.grid.update_docfield_property(
"warehouse", "warehouse",
"read_only", "read_only",
!frm.doc.pick_manually !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) => { get_item_locations: (frm) => {
@@ -273,7 +297,7 @@ frappe.ui.form.on("Pick List", {
max_qty_field: "qty", max_qty_field: "qty",
dont_allow_new_row: true, dont_allow_new_row: true,
prompt_qty: frm.doc.prompt_qty, 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); const barcode_scanner = new erpnext.utils.BarcodeScanner(opts);
barcode_scanner.process_scan(); barcode_scanner.process_scan();

View File

@@ -572,10 +572,18 @@ class PickList(TransactionBase):
if not item.item_code: if not item.item_code:
frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") frappe.throw(f"Row #{item.idx}: Item Code is Mandatory")
if not cint(
frappe.get_cached_value("Item", item.item_code, "is_stock_item") # Check if item is stock item or product bundle
) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}): is_stock_item = cint(frappe.get_cached_value("Item", item.item_code, "is_stock_item"))
continue 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 item_code = item.item_code
reference = item.sales_order_item or item.material_request_item reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, item.warehouse, item.batch_no, reference) key = (item_code, item.uom, item.warehouse, item.batch_no, reference)

View File

@@ -567,20 +567,15 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults=None):
or args.get("warehouse") 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: else:
warehouse = args.get("warehouse") warehouse = args.get("warehouse")
if not warehouse: if not warehouse:
default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") default_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse")
if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company: if (
default_warehouse
and frappe.get_cached_value("Warehouse", default_warehouse, "company") == args.company
):
return default_warehouse return default_warehouse
return warehouse return warehouse