Merge pull request #52731 from frappe/version-16-hotfix

chore: release v16
This commit is contained in:
ruthra kumar
2026-02-17 19:38:48 +05:30
committed by GitHub
41 changed files with 895 additions and 307 deletions

View File

@@ -640,7 +640,7 @@
}
],
"grid_page_length": 50,
"hide_toolbar": 1,
"hide_toolbar": 0,
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,

View File

@@ -121,7 +121,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Apply On",
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
"options": "Item Code\nItem Group\nBrand\nTransaction",
"reqd": 1
},
{
@@ -657,7 +657,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2025-08-20 11:40:07.096854",
"modified": "2026-02-17 12:24:07.553505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
@@ -714,9 +714,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -45,7 +45,7 @@ class PricingRule(Document):
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
apply_discount_on_rate: DF.Check
apply_multiple_pricing_rules: DF.Check
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
apply_recursion_over: DF.Float
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
brands: DF.Table[PricingRuleBrand]

View File

@@ -10,7 +10,7 @@
],
"fields": [
{
"depends_on": "eval:parent.apply_on == 'Item Code'",
"depends_on": "eval:parent.apply_on == 'Brand'",
"fieldname": "brand",
"fieldtype": "Link",
"in_list_view": 1,
@@ -28,14 +28,15 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:17.857046",
"modified": "2026-02-17 12:17:13.073587",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule Brand",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -10,7 +10,7 @@
],
"fields": [
{
"depends_on": "eval:parent.apply_on == 'Item Code'",
"depends_on": "eval:parent.apply_on == 'Item Group'",
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -28,14 +28,15 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:18.221095",
"modified": "2026-02-17 12:16:57.778471",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule Item Group",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -52,6 +52,7 @@
"stock_uom_rate",
"is_free_item",
"apply_tds",
"allow_zero_valuation_rate",
"section_break_22",
"net_rate",
"net_amount",
@@ -97,7 +98,6 @@
"service_start_date",
"service_end_date",
"reference",
"allow_zero_valuation_rate",
"item_tax_rate",
"bom",
"include_exploded_items",
@@ -420,6 +420,7 @@
"options": "UOM"
},
{
"depends_on": "eval:parent.update_stock",
"fieldname": "warehouse_section",
"fieldtype": "Section Break",
"label": "Warehouse"
@@ -447,7 +448,6 @@
"print_hide": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
@@ -459,14 +459,12 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No",
"no_copy": 1
},
{
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
"label": "Rejected Serial No",
@@ -577,6 +575,7 @@
},
{
"default": "0",
"depends_on": "eval:parent.update_stock",
"fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check",
"label": "Allow Zero Valuation Rate",
@@ -800,7 +799,7 @@
"read_only": 1
},
{
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
"depends_on": "eval:parent.is_internal_supplier",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -896,7 +895,7 @@
"label": "Consider for Tax Withholding"
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -906,7 +905,7 @@
"search_index": 1
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -922,7 +921,7 @@
"options": "Asset"
},
{
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -992,7 +991,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-12-13 14:10:02.379392",
"modified": "2026-02-15 21:07:49.455930",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -52,6 +52,7 @@
"is_free_item",
"apply_tds",
"grant_commission",
"allow_zero_valuation_rate",
"section_break_21",
"net_rate",
"net_amount",
@@ -88,7 +89,6 @@
"serial_and_batch_bundle",
"use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
"incoming_rate",
"item_tax_rate",
"actual_batch_qty",
@@ -580,6 +580,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
"depends_on": "eval:parent.update_stock",
"fieldname": "warehouse_and_reference",
"fieldtype": "Section Break",
"label": "Stock Details"
@@ -595,7 +596,7 @@
"print_hide": 1
},
{
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
"depends_on": "eval: parent.is_internal_customer",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"hidden": 1,
@@ -613,7 +614,6 @@
"options": "Quality Inspection"
},
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
@@ -626,6 +626,7 @@
},
{
"default": "0",
"depends_on": "eval:parent.update_stock",
"fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check",
"label": "Allow Zero Valuation Rate",
@@ -633,7 +634,6 @@
"print_hide": 1
},
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No",
@@ -906,7 +906,7 @@
"read_only": 1
},
{
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -916,7 +916,7 @@
"search_index": 1
},
{
"depends_on": "eval:parent.update_stock === 1",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
@@ -1009,7 +1009,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-09-04 11:08:25.583561",
"modified": "2026-02-15 21:08:57.341638",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -26,7 +26,7 @@
},
{
"default": "0",
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -85,14 +85,15 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:36.427565",
"modified": "2026-02-16 20:46:34.592604",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Payment",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -3,7 +3,8 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.query_builder import Criterion
from frappe.utils import cstr, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -33,11 +34,19 @@ def execute(filters=None):
def get_accounts_data(based_on, company):
if based_on == "Cost Center":
return frappe.db.sql(
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
from `tabCost Center` where company=%s order by name""",
company,
as_dict=True,
cc = qb.DocType("Cost Center")
return (
qb.from_(cc)
.select(
cc.name,
cc.parent_cost_center.as_("parent_account"),
cc.cost_center_name.as_("account_name"),
cc.lft,
cc.rgt,
)
.where(cc.company.eq(company))
.orderby(cc.name)
.run(as_dict=True)
)
elif based_on == "Project":
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
):
"""Returns a dict like { "account": [gl entries], ... }"""
additional_conditions = []
gl = qb.DocType("GL Entry")
acc = qb.DocType("Account")
conditions = []
conditions.append(gl.company.eq(company))
conditions.append(gl[based_on].notnull())
conditions.append(gl.is_cancelled.eq(0))
if from_date and to_date:
conditions.append(gl.posting_date.between(from_date, to_date))
elif from_date and not to_date:
conditions.append(gl.posting_date.gte(from_date))
elif not from_date and to_date:
conditions.append(gl.posting_date.lte(to_date))
if ignore_closing_entries:
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
if from_date:
additional_conditions.append("and posting_date >= %(from_date)s")
gl_entries = frappe.db.sql(
"""select posting_date, {based_on} as based_on, debit, credit,
is_opening, (select root_type from `tabAccount` where name = account) as type
from `tabGL Entry` where company=%(company)s
{additional_conditions}
and posting_date <= %(to_date)s
and {based_on} is not null
and is_cancelled = 0
order by {based_on}, posting_date""".format(
additional_conditions="\n".join(additional_conditions), based_on=based_on
),
{"company": company, "from_date": from_date, "to_date": to_date},
as_dict=True,
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
gl_entries = (
qb.from_(gl)
.select(
gl.posting_date,
gl[based_on].as_("based_on"),
gl.debit,
gl.credit,
gl.is_opening,
root_subquery.as_("type"),
)
.where(Criterion.all(conditions))
.orderby(gl[based_on], gl.posting_date)
.run(as_dict=True)
)
for entry in gl_entries:

View File

@@ -4079,6 +4079,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor
)
if child_item.get("total_weight") and child_item.get("weight_per_unit"):
child_item.total_weight = flt(
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
child_item.precision("total_weight"),
)
if d.get("delivery_date") and parent_doctype == "Sales Order":
child_item.delivery_date = d.get("delivery_date")

View File

@@ -783,7 +783,9 @@ class BuyingController(SubcontractingController):
or self.is_return
or (self.is_internal_transfer() and self.docstatus == 2)
else self.get_package_for_target_warehouse(
d, type_of_transaction=type_of_transaction
d,
type_of_transaction=type_of_transaction,
via_landed_cost_voucher=via_landed_cost_voucher,
)
),
},
@@ -871,7 +873,22 @@ class BuyingController(SubcontractingController):
via_landed_cost_voucher=via_landed_cost_voucher,
)
def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
def get_package_for_target_warehouse(
self, item, warehouse=None, type_of_transaction=None, via_landed_cost_voucher=None
) -> str:
if via_landed_cost_voucher and item.get("warehouse"):
if sabb := frappe.db.get_value(
"Serial and Batch Bundle",
{
"voucher_detail_no": item.name,
"warehouse": item.get("warehouse"),
"docstatus": 1,
"is_cancelled": 0,
},
"name",
):
return sabb
if not item.serial_and_batch_bundle:
return ""

View File

@@ -2017,7 +2017,7 @@ def get_gl_entries_for_preview(doctype, docname, fields):
def get_columns(raw_columns, fields):
return [
{"name": d.get("label"), "editable": False, "width": 110}
{"name": d.get("label"), "editable": False, "width": 110, "fieldtype": d.get("fieldtype")}
for d in raw_columns
if not d.get("hidden") and d.get("fieldname") in fields
]

File diff suppressed because it is too large Load Diff

View File

@@ -146,7 +146,7 @@
"label": "Batch Size"
},
{
"depends_on": "eval:doc.parenttype == \"Routing\"",
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
"description": "If you want to run operations in parallel, keep the same sequence ID for them.",
"fieldname": "sequence_id",
"fieldtype": "Int",
@@ -297,7 +297,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-01 17:15:59.806874",
"modified": "2026-02-17 15:33:28.495850",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -49,7 +49,8 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"set_only_once": 1
},
{
"depends_on": "eval:!doc.__islocal && doc.warehouse",
@@ -66,13 +67,14 @@
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
"options": "Company",
"set_only_once": 1
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-25 10:27:41.139634",
"modified": "2026-02-17 11:53:17.940039",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Plant Floor",
@@ -92,7 +94,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -24,8 +24,8 @@ frappe.ui.form.on("Production Plan", {
"Material Request": "Material Request",
};
frm.set_df_property("sub_assembly_items", "cannot_delete_rows", true);
frm.set_df_property("mr_items", "cannot_delete_rows", true);
frm.set_df_property("sub_assembly_items", "cannot_add_rows", true);
frm.set_df_property("mr_items", "cannot_add_rows", true);
},
setup_queries(frm) {

View File

@@ -694,8 +694,8 @@ class ProductionPlan(Document):
self.status = "Completed"
if self.status != "Completed":
self.update_ordered_status()
self.update_requested_status()
self.update_ordered_status()
if close is not None:
self.db_set("status", self.status)
@@ -704,25 +704,17 @@ class ProductionPlan(Document):
self.update_bin_qty()
def update_ordered_status(self):
update_status = False
for d in self.po_items:
if d.planned_qty == d.ordered_qty:
update_status = True
if update_status and self.status != "Completed":
self.status = "In Process"
for child_table in ["po_items", "sub_assembly_items"]:
for item in self.get(child_table):
if item.ordered_qty:
self.status = "In Process"
return
def update_requested_status(self):
if not self.mr_items:
return
update_status = True
for d in self.mr_items:
if d.quantity != d.requested_qty:
update_status = False
if update_status:
self.status = "Material Requested"
if d.requested_qty:
self.status = "Material Requested"
break
def get_production_items(self):
item_dict = {}
@@ -844,6 +836,8 @@ class ProductionPlan(Document):
"stock_uom",
"bom_level",
"schedule_date",
"sales_order",
"sales_order_item",
]:
if row.get(field):
wo_data[field] = row.get(field)
@@ -898,6 +892,8 @@ class ProductionPlan(Document):
"qty",
"description",
"production_plan_item",
"sales_order",
"sales_order_item",
]:
po_data[field] = row.get(field)
@@ -1122,6 +1118,10 @@ class ProductionPlan(Document):
if not is_group_warehouse:
data.fg_warehouse = self.sub_assembly_warehouse
if not self.combine_sub_items:
data.sales_order = row.sales_order
data.sales_order_item = row.sales_order_item
def set_default_supplier_for_subcontracting_order(self):
items = [
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"

View File

@@ -626,6 +626,90 @@ class TestProductionPlan(IntegrationTestCase):
frappe.db.count("Purchase Order Item", {"production_plan": plan.name, "docstatus": 1}), 2
) # 2 since we have already created and submitted 2 POs
def test_sales_order_references_for_sub_assembly_items(self):
"""
Test that Sales Order and Sales Order Item references in Work Order and Purchase Order
are correctly propagated from the Production Plan.
"""
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
# Setup Test Items & BOM
fg_item = "Test FG Good Item"
sub_assembly_item1 = "Test Sub Assembly Item 1"
sub_assembly_item2 = "Test Sub Assembly Item 2"
bom_tree = {
fg_item: {
sub_assembly_item1: {"Test Raw Material 1": {}},
sub_assembly_item2: {"Test Raw Material 2": {}},
}
}
create_nested_bom(bom_tree, prefix="")
# Create Sales Order
so = make_sales_order(item_code=fg_item, qty=10)
so_item_row = so.items[0].name
# Create Production Plan from Sales Order
production_plan = frappe.new_doc("Production Plan")
production_plan.company = so.company
production_plan.get_items_from = "Sales Order"
production_plan.item_code = fg_item
production_plan.get_open_sales_orders()
self.assertEqual(production_plan.sales_orders[0].sales_order, so.name)
production_plan.get_so_items()
production_plan.skip_available_sub_assembly_item = 0
production_plan.get_sub_assembly_items()
self.assertEqual(len(production_plan.sub_assembly_items), 2)
# Validate Sales Order references in Sub Assembly Items
for row in production_plan.sub_assembly_items:
if row.production_item == sub_assembly_item1:
row.supplier = "_Test Supplier"
row.type_of_manufacturing = "Subcontract"
self.assertEqual(row.sales_order, so.name)
self.assertEqual(row.sales_order_item, so_item_row)
# Submit Production Plan
production_plan.save()
production_plan.submit()
production_plan.make_work_order()
# Validate Purchase Order (Subcontracted Item)
po_items = frappe.get_all(
"Purchase Order Item",
{
"production_plan": production_plan.name,
"fg_item": sub_assembly_item1,
},
["sales_order", "sales_order_item"],
)
self.assertTrue(po_items)
self.assertEqual(po_items[0].sales_order, so.name)
self.assertEqual(po_items[0].sales_order_item, so_item_row)
# Validate Work Order (In-house Item)
work_orders = frappe.get_all(
"Work Order",
{
"production_plan": production_plan.name,
"production_item": sub_assembly_item2,
},
["sales_order", "sales_order_item"],
)
self.assertTrue(work_orders)
self.assertEqual(work_orders[0].sales_order, so.name)
self.assertEqual(work_orders[0].sales_order_item, so_item_row)
def test_production_plan_for_mr_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom

View File

@@ -21,6 +21,9 @@
"subcontracting_section",
"supplier",
"purchase_order",
"column_break_oqry",
"sales_order",
"sales_order_item",
"work_order_details_section",
"production_plan_item",
"wo_produced_qty",
@@ -240,13 +243,32 @@
"label": "Ordered Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_oqry",
"fieldtype": "Column Break"
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"options": "Sales Order",
"read_only": 1
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Sales Order Item",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-03 14:33:50.677717",
"modified": "2026-02-11 13:00:09.092676",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",

View File

@@ -34,6 +34,8 @@ class ProductionPlanSubAssemblyItem(Document):
qty: DF.Float
received_qty: DF.Float
required_qty: DF.Float
sales_order: DF.Link | None
sales_order_item: DF.Data | None
schedule_date: DF.Datetime | None
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None

View File

@@ -464,3 +464,4 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
erpnext.patches.v16_0.migrate_transaction_deletion_task_flags_to_status # 2
erpnext.patches.v16_0.update_company_custom_field_in_bin
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v15_0.delete_quotation_lost_record_detail

View File

@@ -0,0 +1,11 @@
import frappe
from frappe.query_builder import DocType
def execute():
qlr = DocType("Quotation Lost Reason Detail")
quotation = DocType("Quotation")
sub_query = frappe.qb.from_(quotation).select(quotation.name)
query = frappe.qb.from_(qlr).delete().where(qlr.parent.notin(sub_query))
query.run()

View File

@@ -2,7 +2,7 @@ import click
import frappe
from frappe.query_builder.functions import Coalesce
from erpnext.setup.install import create_marketgin_campagin_custom_fields
from erpnext.setup.install import create_marketing_campaign_custom_fields
def execute():
@@ -31,7 +31,7 @@ def execute():
frappe.delete_doc("DocType", "Lead Source", ignore_missing=True)
campaign = frappe.qb.DocType("Campaign")
create_marketgin_campagin_custom_fields()
create_marketing_campaign_custom_fields()
marketing_campaign = frappe.qb.DocType("UTM Campaign")
# Fetch all Campaigns

View File

@@ -646,7 +646,11 @@ erpnext.utils.update_child_items = function (opts) {
get_query: function () {
let filters;
if (frm.doc.doctype == "Sales Order") {
filters = { is_sales_item: 1, is_stock_item: !frm.doc.is_subcontracted };
if (frm.doc.is_subcontracted) {
filters = { is_sales_item: 1, is_stock_item: 0 };
} else {
filters = { is_sales_item: 1 };
}
} else if (frm.doc.doctype == "Purchase Order") {
if (frm.doc.is_subcontracted) {
if (frm.doc.is_old_subcontracting_flow) {

View File

@@ -84,6 +84,14 @@ erpnext.accounts.ledger_preview = {
},
get_datatable(columns, data, wrapper) {
columns.forEach((col) => {
if (col.fieldtype === "Currency") {
col.format = (value) => {
return format_currency(value);
};
}
});
const datatable_options = {
columns: columns,
data: data,

View File

@@ -708,7 +708,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
render_data() {
if (this.bundle || this.frm.doc.is_return) {
if (this.bundle || (this.frm.doc.is_return && this.frm.doc.return_against)) {
frappe
.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers",

View File

@@ -1,4 +1,4 @@
.funnel-wrapper {
margin: 15px;
width: 100%;
width: calc(100% - 30px);
margin-left: 30px;
}

View File

@@ -8,6 +8,9 @@ frappe.pages["sales-funnel"].on_page_load = function (wrapper) {
single_column: true,
});
$(wrapper).find(".layout-main").addClass("row");
$(wrapper).find(".layout-main-section-wrapper").addClass("col-md-12");
wrapper.sales_funnel = new erpnext.SalesFunnel(wrapper);
frappe.breadcrumbs.add("Selling");

View File

@@ -3,7 +3,8 @@
import frappe
from frappe import _, msgprint
from frappe import _, msgprint, qb
from frappe.query_builder import Criterion
def execute(filters=None):
@@ -97,45 +98,53 @@ def get_columns(filters):
def get_entries(filters):
date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date"
dt = qb.DocType(filters["doc_type"])
st = qb.DocType("Sales Team")
date_field = dt["transaction_date"] if filters["doc_type"] == "Sales Order" else dt["posting_date"]
conditions, values = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
select
dt.name, dt.customer, dt.territory, dt.{} as posting_date,dt.base_net_total as base_net_amount,
st.commission_rate,st.sales_person, st.allocated_percentage, st.allocated_amount, st.incentives
from
`tab{}` dt, `tabSales Team` st
where
st.parent = dt.name and st.parenttype = {}
and dt.docstatus = 1 {} order by dt.name desc,st.sales_person
""".format(date_field, filters["doc_type"], "%s", conditions),
tuple([filters["doc_type"], *values]),
as_dict=1,
conditions = get_conditions(dt, st, filters, date_field)
entries = (
qb.from_(dt)
.join(st)
.on(st.parent.eq(dt.name) & st.parenttype.eq(filters["doc_type"]))
.select(
dt.name,
dt.customer,
dt.territory,
date_field.as_("posting_date"),
dt.base_net_total.as_("base_net_amount"),
st.commission_rate,
st.sales_person,
st.allocated_percentage,
st.allocated_amount,
st.incentives,
)
.where(Criterion.all(conditions))
.orderby(dt.name, st.sales_person)
.run(as_dict=True)
)
return entries
def get_conditions(filters, date_field):
conditions = [""]
values = []
def get_conditions(dt, st, filters, date_field):
conditions = []
conditions.append(dt.docstatus.eq(1))
from_dt = filters.get("from_date")
to_dt = filters.get("to_date")
if from_dt and to_dt:
conditions.append(date_field.between(from_dt, to_dt))
elif from_dt and not to_dt:
conditions.append(date_field.gte(from_dt))
elif not from_dt and to_dt:
conditions.append(date_field.lte(to_dt))
for field in ["company", "customer", "territory"]:
if filters.get(field):
conditions.append(f"dt.{field}=%s")
values.append(filters[field])
conditions.append(dt[field].eq(filters.get(field)))
if filters.get("sales_person"):
conditions.append("st.sales_person = '{}'".format(filters.get("sales_person")))
conditions.append(st["sales_person"].eq(filters.get("sales_person")))
if filters.get("from_date"):
conditions.append(f"dt.{date_field}>=%s")
values.append(filters["from_date"])
if filters.get("to_date"):
conditions.append(f"dt.{date_field}<=%s")
values.append(filters["to_date"])
return " and ".join(conditions), values
return conditions

View File

@@ -317,7 +317,9 @@ class TransactionDeletionRecord(Document):
list: List of child table DocType names (Table field options)
"""
return frappe.get_all(
"DocField", filters={"parent": doctype_name, "fieldtype": "Table"}, pluck="options"
"DocField",
filters={"parent": doctype_name, "fieldtype": ["in", ["Table", "Table MultiSelect"]]},
pluck="options",
)
def _get_to_delete_row_infos(self, doctype_name, company_field=None, company=None):

View File

@@ -23,7 +23,7 @@ def after_install():
set_single_defaults()
create_print_setting_custom_fields()
create_marketgin_campagin_custom_fields()
create_marketing_campaign_custom_fields()
create_custom_company_links()
add_all_roles_to("Administrator")
create_default_success_action()
@@ -119,16 +119,16 @@ def create_print_setting_custom_fields():
)
def create_marketgin_campagin_custom_fields():
def create_marketing_campaign_custom_fields():
create_custom_fields(
{
"UTM Campaign": [
{
"label": _("Messaging CRM Campagin"),
"label": _("Messaging CRM Campaign"),
"fieldname": "crm_campaign",
"fieldtype": "Link",
"options": "Campaign",
"insert_after": "campaign_decription",
"insert_after": "campaign_description",
},
]
}

View File

@@ -1017,6 +1017,102 @@ class TestPurchaseReceipt(IntegrationTestCase):
pr.cancel()
def test_lcv_for_internal_transfer(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
make_landed_cost_voucher,
)
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
item_code = make_item(
"Test Item For LCV in Internal Transfer",
{"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "TEST-SBATCH.###"},
).name
pr1 = make_purchase_receipt(
item_code=item_code,
qty=10,
rate=100,
warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory",
)
dn1 = create_delivery_note(
item_code=pr1.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=500,
warehouse="Stores - TCP1",
target_warehouse="Work In Progress - TCP1",
)
pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "Work In Progress - TCP1"
pr.items[0].warehouse = "Stores - TCP1"
pr.submit()
sle_entries = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
fields=["serial_and_batch_bundle", "actual_qty"],
)
self.assertEqual(len(sle_entries), 2)
inward_sabb = frappe.get_all(
"Serial and Batch Bundle",
filters={
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"total_qty": (">", 0),
"docstatus": 1,
},
pluck="name",
)
self.assertEqual(len(inward_sabb), 1)
original_cost = frappe.db.get_value("Serial and Batch Bundle", inward_sabb[0], "total_amount")
make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=100,
distribute_charges_based_on="Qty",
expense_account="Expenses Included In Valuation - TCP1",
)
sle_entries = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
fields=["serial_and_batch_bundle", "actual_qty"],
)
self.assertEqual(len(sle_entries), 2)
new_inward_sabb = frappe.get_all(
"Serial and Batch Bundle",
filters={
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"total_qty": (">", 0),
"docstatus": 1,
},
pluck="name",
)
self.assertEqual(len(new_inward_sabb), 1)
new_cost = frappe.db.get_value("Serial and Batch Bundle", new_inward_sabb[0], "total_amount")
self.assertEqual(new_cost, original_cost + 100)
self.assertTrue(new_inward_sabb[0] == inward_sabb[0])
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note

View File

@@ -17,6 +17,7 @@ from frappe.utils import (
cint,
cstr,
flt,
format_datetime,
get_datetime,
get_link_to_form,
getdate,
@@ -1459,22 +1460,33 @@ class SerialandBatchBundle(Document):
if flt(available_qty, precision) < 0:
self.throw_negative_batch(d.batch_no, available_qty, precision)
def throw_negative_batch(self, batch_no, available_qty, precision):
def throw_negative_batch(self, batch_no, available_qty, precision, posting_datetime=None):
from erpnext.stock.stock_ledger import NegativeStockError
if frappe.db.get_single_value("Stock Settings", "allow_negative_stock_for_batch"):
return
date_msg = ""
if posting_datetime:
date_msg = " " + _("as of {0}").format(format_datetime(posting_datetime))
msg = _(
"""
The Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.
Please add a stock quantity of {4} to proceed with this entry.
If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in Stock Settings to proceed.
However, enabling this setting may lead to negative stock in the system.
So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate."""
).format(
bold(batch_no),
bold(self.item_code),
bold(self.warehouse),
date_msg,
bold(abs(flt(available_qty, precision))),
)
frappe.throw(
_(
"""
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
).format(
bold(batch_no),
bold(self.item_code),
bold(self.warehouse),
bold(abs(flt(available_qty, precision))),
),
msg,
title=_("Negative Stock Error"),
exc=NegativeStockError,
)
@@ -1497,7 +1509,9 @@ class SerialandBatchBundle(Document):
available_qty[row.batch_no] = flt(row.qty)
if flt(available_qty[row.batch_no], precision) < 0:
self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision)
self.throw_negative_batch(
row.batch_no, available_qty[row.batch_no], precision, row.posting_datetime
)
return available_qty
@@ -1531,10 +1545,13 @@ class SerialandBatchBundle(Document):
def get_available_qty_from_sabb(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
sle = frappe.qb.DocType("Stock Ledger Entry")
child = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(child)
.inner_join(sle)
.on(child.parent == sle.serial_and_batch_bundle)
.select(
child.batch_no,
child.qty,
@@ -1543,6 +1560,7 @@ class SerialandBatchBundle(Document):
)
.where(
(child.item_code == self.item_code)
& (sle.is_cancelled == 0)
& (child.warehouse == self.warehouse)
& (child.is_cancelled == 0)
& (child.batch_no.isin(batches))
@@ -1556,6 +1574,9 @@ class SerialandBatchBundle(Document):
return query.run(as_dict=True)
def validate_voucher_no_docstatus(self):
if self.is_cancelled:
return
if self.voucher_type == "POS Invoice":
return

View File

@@ -550,7 +550,7 @@
},
{
"default": "0",
"description": "If enabled, the system will allow negative stock entries for the batch, but this could calculate the valuation rate incorrectly, so avoid using this option.",
"description": "If enabled, the system will allow negative stock entries for the batch. But, this may lead to incorrect valuation rates, so it is recommended to avoid using this option. The system will permit negative stock only when it is caused by backdated entries and will validate and block negative stock in all other cases.",
"fieldname": "allow_negative_stock_for_batch",
"fieldtype": "Check",
"label": "Allow Negative Stock for Batch"
@@ -562,7 +562,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-09 15:01:12.466175",
"modified": "2026-02-16 10:36:59.921491",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -9,7 +9,7 @@
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"letter_head": "Test",
"letter_head": null,
"letterhead": null,
"modified": "2025-02-03 15:39:47.613040",
"modified_by": "Administrator",

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Negative Batch Report"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_default("company"),
},
{
fieldname: "item_code",
label: __("Item Code"),
fieldtype: "Link",
options: "Item",
get_query: function () {
return {
filters: {
has_batch_no: 1,
},
};
},
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
get_query: function () {
return {
filters: {
is_group: 0,
company: frappe.query_report.get_filter_value("company"),
},
};
},
},
],
};

View File

@@ -0,0 +1,53 @@
{
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2026-02-17 11:34:21.549485",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "",
"letter_head": null,
"modified": "2026-02-17 11:34:59.106045",
"modified_by": "Administrator",
"module": "Stock",
"name": "Negative Batch Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial and Batch Bundle",
"report_name": "Negative Batch Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Purchase User"
},
{
"role": "Purchase Manager"
},
{
"role": "Stock User"
},
{
"role": "Stock Manager"
},
{
"role": "Delivery User"
},
{
"role": "Delivery Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
}
],
"timeout": 0
}

View File

@@ -0,0 +1,145 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_to_date, flt, today
from erpnext.stock.report.stock_ledger.stock_ledger import execute as stock_ledger_execute
def execute(filters: dict | None = None):
"""Return columns and data for the report.
This is the main entry point for the report. It accepts the filters as a
dictionary and should return columns and data. It is called by the framework
every time the report is refreshed or a filter is updated.
"""
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns() -> list[dict]:
return [
{
"label": _("Posting Datetime"),
"fieldname": "posting_date",
"fieldtype": "Datetime",
"width": 160,
},
{
"label": _("Batch No"),
"fieldname": "batch_no",
"fieldtype": "Link",
"options": "Batch",
"width": 120,
},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 150,
},
{
"label": _("Warehouse"),
"fieldname": "warehouse",
"fieldtype": "Link",
"options": "Warehouse",
"width": 160,
},
{
"label": _("Previous Qty"),
"fieldname": "previous_qty",
"fieldtype": "Float",
"width": 130,
},
{
"label": _("Transaction Qty"),
"fieldname": "actual_qty",
"fieldtype": "Float",
"width": 130,
},
{
"label": _("Qty After Transaction"),
"fieldname": "qty_after_transaction",
"fieldtype": "Float",
"width": 180,
},
{
"label": _("Document Type"),
"fieldname": "voucher_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Document No"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 130,
},
]
def get_data(filters) -> list[dict]:
batches = get_batches(filters)
companies = get_companies(filters)
batch_negative_data = []
flt_precision = frappe.db.get_default("float_precision") or 2
for company in companies:
for batch in batches:
_c, data = stock_ledger_execute(
frappe._dict(
{
"company": company,
"batch_no": batch,
"from_date": add_to_date(today(), years=-12),
"to_date": today(),
"segregate_serial_batch_bundle": 1,
"warehouse": filters.get("warehouse"),
"valuation_field_type": "Currency",
}
)
)
previous_qty = 0
for row in data:
if flt(row.get("qty_after_transaction"), flt_precision) < 0:
batch_negative_data.append(
{
"posting_date": row.get("date"),
"batch_no": row.get("batch_no"),
"item_code": row.get("item_code"),
"item_name": row.get("item_name"),
"warehouse": row.get("warehouse"),
"actual_qty": row.get("actual_qty"),
"qty_after_transaction": row.get("qty_after_transaction"),
"previous_qty": previous_qty,
"voucher_type": row.get("voucher_type"),
"voucher_no": row.get("voucher_no"),
}
)
previous_qty = row.get("qty_after_transaction")
return batch_negative_data
def get_batches(filters):
batch_filters = {}
if filters.get("item_code"):
batch_filters["item"] = filters["item_code"]
return frappe.get_all("Batch", pluck="name", filters=batch_filters)
def get_companies(filters):
company_filters = {}
if filters.get("company"):
company_filters["name"] = filters["company"]
return frappe.get_all("Company", pluck="name", filters=company_filters)

View File

@@ -314,7 +314,7 @@
"type": "Link"
}
],
"modified": "2026-01-27 21:23:15.665712",
"modified": "2026-01-26 21:23:15.665712",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Invoicing",

View File

@@ -89,6 +89,17 @@
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "POS Profile",
"link_to": "POS Profile",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
@@ -122,17 +133,6 @@
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "POS Profile",
"link_to": "POS Profile",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
@@ -687,7 +687,7 @@
"type": "Link"
}
],
"modified": "2026-01-10 00:06:13.103140",
"modified": "2026-02-16 23:48:24.611112",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",