mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-25 09:38:31 +00:00
Merge pull request #52731 from frappe/version-16-hotfix
chore: release v16
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
11
erpnext/patches/v15_0/delete_quotation_lost_record_detail.py
Normal file
11
erpnext/patches/v15_0/delete_quotation_lost_record_detail.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.funnel-wrapper {
|
||||
margin: 15px;
|
||||
width: 100%;
|
||||
width: calc(100% - 30px);
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user