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

This commit is contained in:
diptanilsaha
2026-03-23 20:27:38 +05:30
committed by GitHub
33 changed files with 475 additions and 76 deletions

View File

@@ -243,8 +243,10 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
date = (
args.get("transaction_date")
or args.get("posting_date")
or frappe.get_value(args.get("doctype"), args.get("name"), "posting_date", ignore=True)
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')

View File

@@ -382,7 +382,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "bill_no",
"collapsible_depends_on": "posting_date",
"fieldname": "supplier_invoice_details",
"fieldtype": "Section Break",
"label": "Supplier Invoice"
@@ -1660,7 +1660,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-02-05 20:45:16.964500",
"modified": "2026-03-17 20:44:00.221219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -2516,7 +2516,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"],
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
},
doctype + " Item": item_field_map,
},

View File

@@ -4835,6 +4835,33 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
def test_inter_company_transaction_cost_center(self):
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
debit_to="Debtors - WP",
warehouse="Stores - WP",
income_account="Sales - WP",
expense_account="Cost of Goods Sold - WP",
parent_cost_center="Main - WP",
cost_center="Main - WP",
currency="USD",
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.submit()
cost_center = frappe.db.get_value("Company", "_Test Company 1", "cost_center")
frappe.db.set_value("Company", "_Test Company 1", "cost_center", None)
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
self.assertEqual(target_doc.cost_center, None)
self.assertEqual(target_doc.items[0].cost_center, None)
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -152,7 +152,9 @@ class ShippingRule(Document):
frappe.throw(_("Shipping rule only applicable for Buying"))
shipping_charge["doctype"] = "Purchase Taxes and Charges"
shipping_charge["category"] = "Valuation and Total"
shipping_charge["category"] = (
"Valuation and Total" if doc.get_stock_items() or doc.get_asset_items() else "Total"
)
shipping_charge["add_deduct_tax"] = "Add"
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _
from frappe.utils import flt, formatdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
@@ -15,6 +16,8 @@ def execute(filters=None):
if not filters:
filters = {}
validate_filters(filters)
columns = get_columns(filters)
if filters.get("budget_against_filter"):
dimensions = filters.get("budget_against_filter")
@@ -35,6 +38,21 @@ def execute(filters=None):
return columns, data, None, chart
def validate_filters(filters):
validate_budget_dimensions(filters)
def validate_budget_dimensions(filters):
dimensions = [d.get("document_type") for d in get_dimensions(with_cost_center_and_project=True)[0]]
if filters.get("budget_against") and filters.get("budget_against") not in dimensions:
frappe.throw(
title=_("Invalid Accounting Dimension"),
msg=_("{0} is not a valid Accounting Dimension.").format(
frappe.bold(filters.get("budget_against"))
),
)
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
for account, monthwise_data in dimension_items.items():
row = [dimension, account]

View File

@@ -31,6 +31,7 @@ def _execute(filters=None, additional_table_columns=None):
item_list = get_items(filters, additional_table_columns)
aii_account_map = get_aii_accounts()
default_taxes = {}
if item_list:
itemised_tax, tax_columns = get_tax_accounts(
item_list,
@@ -39,6 +40,9 @@ def _execute(filters=None, additional_table_columns=None):
doctype="Purchase Invoice",
tax_doctype="Purchase Taxes and Charges",
)
for tax in tax_columns:
default_taxes[f"{tax}_rate"] = 0
default_taxes[f"{tax}_amount"] = 0
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
@@ -85,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
}
total_tax = 0
row.update(default_taxes.copy())
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(

View File

@@ -29,8 +29,12 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
item_list = get_items(filters, additional_table_columns, additional_conditions)
default_taxes = {}
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
for tax in tax_columns:
default_taxes[f"{tax}_rate"] = 0
default_taxes[f"{tax}_amount"] = 0
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -88,6 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
total_tax = 0
total_other_charges = 0
row.update(default_taxes.copy())
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(

View File

@@ -17,9 +17,11 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, do_not_submit=False):
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
si = create_sales_invoice(
item=self.item,
item=item or self.item,
item_name=item or self.item,
description=item or self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
@@ -30,6 +32,19 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
price_list_rate=100,
do_not_save=1,
)
for tax in taxes or []:
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": tax["account_head"],
"cost_center": self.cost_center,
"description": tax["description"],
"rate": tax["rate"],
},
)
si = si.save()
if not do_not_submit:
si = si.submit()
@@ -63,3 +78,50 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_grouped_report_handles_different_tax_descriptions(self):
self.create_item(item_name="_Test Item Tax Description A")
first_item = self.item
self.create_item(item_name="_Test Item Tax Description B")
second_item = self.item
first_tax_description = "Tax Description A"
second_tax_description = "Tax Description B"
first_tax_amount_field = f"{frappe.scrub(first_tax_description)}_amount"
second_tax_amount_field = f"{frappe.scrub(second_tax_description)}_amount"
self.create_sales_invoice(
item=first_item,
taxes=[
{
"account_head": "_Test Account VAT - _TC",
"description": first_tax_description,
"rate": 5,
}
],
)
self.create_sales_invoice(
item=second_item,
taxes=[
{
"account_head": "_Test Account Service Tax - _TC",
"description": second_tax_description,
"rate": 2,
}
],
)
filters = frappe._dict(
{
"from_date": today(),
"to_date": today(),
"company": self.company,
"group_by": "Customer",
}
)
_, data, _, _, _, _ = execute(filters)
grand_total_row = next(row for row in data if row.get("bold") and row.get("item_code") == "Total")
self.assertEqual(grand_total_row[first_tax_amount_field], 5.0)
self.assertEqual(grand_total_row[second_tax_amount_field], 2.0)

View File

@@ -912,6 +912,8 @@ def get_list_context(context=None):
@frappe.whitelist()
def update_status(status, name):
frappe.has_permission("Purchase Order", "submit", name, throw=True)
po = frappe.get_doc("Purchase Order", name)
po.update_status(status)
po.update_delivered_qty_in_sales_order()

View File

@@ -41,6 +41,7 @@ def get_columns(filters):
"fieldname": "transferred_qty",
"width": 200,
},
{"label": _("Returned Quantity"), "fieldtype": "Float", "fieldname": "returned_qty", "width": 150},
{"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150},
]
@@ -50,7 +51,7 @@ def get_data(filters):
data = []
for row in order_rm_item_details:
transferred_qty = row.get("transferred_qty") or 0
transferred_qty = (row.get("transferred_qty") or 0) - (row.get("returned_qty") or 0)
if transferred_qty < row.get("reqd_qty", 0):
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
row.p_qty = pending_qty if pending_qty > 0 else 0
@@ -86,6 +87,7 @@ def get_order_items_to_supply(filters):
f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
f"`tab{supplied_items_table}`.required_qty as reqd_qty",
f"`tab{supplied_items_table}`.supplied_qty as transferred_qty",
f"`tab{supplied_items_table}`.returned_qty as returned_qty",
],
filters=record_filters,
)

View File

@@ -2297,6 +2297,16 @@ class AccountsController(TransactionBase):
return stock_items
def get_asset_items(self):
asset_items = []
item_codes = list(set(item.item_code for item in self.get("items")))
if item_codes:
asset_items = frappe.db.get_values(
"Item", {"name": ["in", item_codes], "is_fixed_asset": 1}, pluck="name", cache=True
)
return asset_items
def calculate_total_advance_from_ledger(self):
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
return (

View File

@@ -234,8 +234,8 @@ class StatusUpdater(Document):
self.global_amount_allowance = None
for args in self.status_updater:
if "target_ref_field" not in args:
# if target_ref_field is not specified, the programmer does not want to validate qty / amount
if "target_ref_field" not in args or args.get("validate_qty") is False:
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
continue
# get unique transactions to update

View File

@@ -989,6 +989,12 @@ class SubcontractingController(StockController):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
return
if (
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
== "BOM"
):
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if not self.__transferred_items or not self.__transferred_items.get(key):

View File

@@ -55,6 +55,14 @@ def validate_filters(filters):
if filters.get("based_on") == filters.get("group_by"):
frappe.throw(_("'Based On' and 'Group By' can not be same"))
if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]:
frappe.throw(
msg=_("{0} can be either {1} or {2}.").format(
frappe.bold("Period based On"), frappe.bold("Posting Date"), frappe.bold("Billing Date")
),
title=_("Invalid Filter"),
)
def get_data(filters, conditions):
data = []

View File

@@ -192,6 +192,7 @@ frappe.ui.form.on("BOM", {
bom_no: frm.doc.name,
item: item,
qty: data.qty || 0.0,
company: frm.doc.company,
project: frm.doc.project,
variant_items: variant_items,
use_multi_level_bom: use_multi_level_bom,

View File

@@ -1019,6 +1019,12 @@ class BOM(WebsiteGenerator):
"Row {0}: Workstation or Workstation Type is mandatory for an operation {1}"
).format(d.idx, d.operation)
)
if not d.time_in_mins or d.time_in_mins <= 0:
frappe.throw(
_("Row {0}: Operation time should be greater than 0 for operation {1}").format(
d.idx, d.operation
)
)
def get_tree_representation(self) -> BOMTree:
"""Get a complete tree representation preserving order of child items."""
@@ -1329,9 +1335,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
bom = frappe.get_doc("BOM", work_order.bom_no)
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
items = {}
items = frappe._dict()
for d in bom.get(table):
items.setdefault(d.item_code, d.amount)
items.setdefault(d.item_code, 0)
items[d.item_code] += flt(d.amount)
non_stock_items = frappe.get_all(
"Item",

View File

@@ -132,6 +132,15 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
@timeout
def test_bom_no_operation_time_validation(self):
bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0
for op_row in bom.operations:
op_row.time_in_mins = 0
self.assertRaises(frappe.ValidationError, bom.save)
@timeout
def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2])

View File

@@ -1372,7 +1372,11 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
@frappe.whitelist()
def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_multi_level_bom=None):
def make_work_order(
bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
):
from erpnext import get_default_company
if not frappe.has_permission("Work Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
@@ -1387,6 +1391,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
wo_doc = frappe.new_doc("Work Order")
wo_doc.production_item = item
wo_doc.company = company or get_default_company()
wo_doc.update(item_details)
wo_doc.bom_no = bom_no
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)

View File

@@ -47,21 +47,8 @@ def create_customer_or_supplier():
if party_exists(doctype, user):
return
party = frappe.new_doc(doctype)
fullname = frappe.utils.get_fullname(user)
if not doctype == "Customer":
party.update(
{
"supplier_name": fullname,
"supplier_group": "All Supplier Groups",
"supplier_type": "Individual",
}
)
party.flags.ignore_mandatory = True
party.insert(ignore_permissions=True)
party = create_party(doctype, fullname)
alternate_doctype = "Customer" if doctype == "Supplier" else "Supplier"
if party_exists(alternate_doctype, user):
@@ -69,6 +56,22 @@ def create_customer_or_supplier():
fullname += "-" + doctype
create_party_contact(doctype, fullname, user, party.name)
return party
def create_party(doctype, fullname):
party = frappe.new_doc(doctype)
# Can't set parent party as group
party.update(
{
f"{doctype.lower()}_name": fullname,
f"{doctype.lower()}_type": "Individual",
}
)
party.flags.ignore_mandatory = True
party.insert(ignore_permissions=True)
return party

View File

@@ -1681,6 +1681,8 @@ def make_work_orders(items, sales_order, company, project=None):
@frappe.whitelist()
def update_status(status, name):
frappe.has_permission("Sales Order", "submit", name, throw=True)
so = frappe.get_doc("Sales Order", name)
so.update_status(status)

View File

@@ -29,6 +29,15 @@ from erpnext.stock.serial_batch_bundle import (
)
from erpnext.utilities.transaction_base import TransactionBase
class MissingWarehouseValidationError(frappe.ValidationError):
pass
class IncorrectWarehouseValidationError(frappe.ValidationError):
pass
# TODO: Prioritize SO or WO group warehouse
@@ -94,6 +103,7 @@ class PickList(TransactionBase):
if self.get("locations"):
self.validate_sales_order_percentage()
self.validate_warehouses()
def validate_stock_qty(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty
@@ -138,6 +148,31 @@ class PickList(TransactionBase):
title=_("Insufficient Stock"),
)
def validate_warehouses(self):
for location in self.locations:
if not location.warehouse:
frappe.throw(
_("Row {0}: Warehouse is required").format(location.idx),
title=_("Missing Warehouse"),
exc=MissingWarehouseValidationError,
)
company = frappe.get_cached_value("Warehouse", location.warehouse, "company")
if company != self.company:
frappe.throw(
_(
"Row {0}: Warehouse {1} is linked to company {2}. Please select a warehouse belonging to company {3}."
).format(
location.idx,
frappe.bold(location.warehouse),
frappe.bold(company),
frappe.bold(self.company),
),
title=_("Incorrect Warehouse"),
exc=IncorrectWarehouseValidationError,
)
def check_serial_no_status(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -949,6 +984,7 @@ def get_available_item_locations(
locations = get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
company,
consider_rejected_warehouses=consider_rejected_warehouses,
)
else:
@@ -1049,6 +1085,7 @@ def get_available_item_locations_for_serial_and_batched_item(
locations = get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
company,
consider_rejected_warehouses=consider_rejected_warehouses,
)
@@ -1129,6 +1166,7 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
company,
consider_rejected_warehouses=False,
):
locations = []
@@ -1138,6 +1176,7 @@ def get_available_item_locations_for_batched_item(
"item_code": item_code,
"warehouse": from_warehouses,
"based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
"company": company,
}
)
)

View File

@@ -211,6 +211,7 @@ class TestPickList(FrappeTestCase):
"qty": 1000,
"stock_qty": 1000,
"conversion_factor": 1,
"warehouse": "_Test Warehouse - _TC",
"sales_order": so.name,
"sales_order_item": so.items[0].name,
}
@@ -268,6 +269,119 @@ class TestPickList(FrappeTestCase):
pr1.cancel()
pr2.cancel()
def test_pick_list_warehouse_for_batched_item(self):
"""
Test that pick list respects company based warehouse assignment for batched items.
This test verifies that when creating a pick list for a batched item,
the system correctly identifies and assigns the appropriate warehouse
based on the company.
"""
from erpnext.stock.doctype.batch.test_batch import make_new_batch
batch_company = frappe.get_doc(
{"doctype": "Company", "company_name": "Batch Company", "default_currency": "INR"}
)
batch_company.insert()
batch_warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": "Batch Warehouse",
"company": batch_company.name,
}
)
batch_warehouse.insert()
batch_item = frappe.db.exists("Item", "Batch Warehouse Item")
if not batch_item:
batch_item = create_item("Batch Warehouse Item")
batch_item.has_batch_no = 1
batch_item.create_new_batch = 1
batch_item.save()
else:
batch_item = frappe.get_doc("Item", "Batch Warehouse Item")
batch_no = make_new_batch(item_code=batch_item.name, batch_id="B-WH-ITEM-001")
make_stock_entry(
item_code=batch_item.name,
qty=5,
company=batch_company.name,
to_warehouse=batch_warehouse.name,
batch_no=batch_no.name,
rate=100.0,
)
make_stock_entry(
item_code=batch_item.name,
qty=5,
to_warehouse="_Test Warehouse - _TC",
batch_no=batch_no.name,
rate=100.0,
)
pick_list = frappe.get_doc(
{
"doctype": "Pick List",
"company": batch_company.name,
"purpose": "Material Transfer",
"locations": [
{
"item_code": batch_item.name,
"qty": 10,
"stock_qty": 10,
"conversion_factor": 1,
}
],
}
)
pick_list.set_item_locations()
self.assertEqual(len(pick_list.locations), 1)
self.assertEqual(pick_list.locations[0].qty, 5)
self.assertEqual(pick_list.locations[0].batch_no, batch_no.name)
self.assertEqual(pick_list.locations[0].warehouse, batch_warehouse.name)
def test_pick_list_warehouse_validation(self):
"""check if the warehouse validations are triggered"""
from erpnext.stock.doctype.pick_list.pick_list import (
IncorrectWarehouseValidationError,
MissingWarehouseValidationError,
)
warehouse_item = create_item("Warehouse Item")
temp_company = frappe.get_doc(
{"doctype": "Company", "company_name": "Temp Company", "default_currency": "INR"}
).insert()
temp_warehouse = frappe.get_doc(
{"doctype": "Warehouse", "warehouse_name": "Temp Warehouse", "company": temp_company.name}
).insert()
make_stock_entry(item_code=warehouse_item.name, qty=10, rate=100.0, to_warehouse=temp_warehouse.name)
pick_list = frappe.get_doc(
{
"doctype": "Pick List",
"company": temp_company.name,
"purpose": "Material Transfer",
"pick_manually": 1,
"locations": [
{
"item_code": warehouse_item.name,
"qty": 5,
"stock_qty": 5,
"conversion_factor": 1,
}
],
}
)
self.assertRaises(MissingWarehouseValidationError, pick_list.insert)
pick_list.locations[0].warehouse = "_Test Warehouse - _TC"
self.assertRaises(IncorrectWarehouseValidationError, pick_list.insert)
pick_list.locations[0].warehouse = temp_warehouse.name
pick_list.insert()
def test_pick_list_for_batched_and_serialised_item(self):
# check if oldest batch no and serial nos are picked
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})

View File

@@ -62,6 +62,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Warehouse",
"mandatory_depends_on": "eval: parent.pick_manually",
"options": "Warehouse",
"read_only": 1
},
@@ -283,7 +284,7 @@
],
"istable": 1,
"links": [],
"modified": "2025-09-23 00:02:57.817040",
"modified": "2026-03-17 16:25:10.358013",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@@ -185,6 +185,7 @@ class PurchaseReceipt(BuyingController):
"target_ref_field": "stock_qty",
"source_field": "stock_qty",
"percent_join_field": "material_request",
"validate_qty": False,
},
{
"source_dt": "Purchase Receipt Item",
@@ -327,7 +328,10 @@ class PurchaseReceipt(BuyingController):
)
def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
if (
frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
and not self.is_internal_transfer()
):
for d in self.get("items"):
if not d.purchase_order:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
@@ -1414,6 +1418,8 @@ def make_purchase_return(source_name, target_doc=None):
@frappe.whitelist()
def update_purchase_receipt_status(docname, status):
frappe.has_permission("Purchase Receipt", "submit", docname, throw=True)
pr = frappe.get_doc("Purchase Receipt", docname)
pr.update_status(status)

View File

@@ -15,6 +15,7 @@ from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
SerialNoDuplicateError,
@@ -33,6 +34,40 @@ class TestPurchaseReceipt(FrappeTestCase):
def setUp(self):
frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)
def test_purchase_receipt_skips_validation(self):
"""
Test that validation is skipped when over delivery receipt allowance is reduced after PO submission
and PR can be submitted with higher qty than MR.
"""
item = create_item("Test item for validation")
mr = frappe.new_doc("Material Request")
mr.material_request_type = "Purchase"
mr.company = "_Test Company"
mr.price_list = "_Test Price List"
mr.append(
"items",
{
"item_code": item.name,
"item_name": item.item_name,
"item_group": item.item_group,
"schedule_date": add_days(today(), 1),
"qty": 100,
"uom": item.stock_uom,
},
)
mr.insert()
mr.submit()
frappe.db.set_value("Item", item.name, "over_delivery_receipt_allowance", 200)
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.items[0].qty = 300
po.save()
po.submit()
frappe.db.set_value("Item", item.name, "over_delivery_receipt_allowance", 20)
pr = make_purchase_receipt(qty=300, item_code=item.name, do_not_save=True)
pr.save()
pr.submit()
def test_purchase_receipt_qty(self):
pr = make_purchase_receipt(qty=0, rejected_qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):

View File

@@ -748,11 +748,16 @@ class SerialandBatchBundle(Document):
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
for d in self.entries:
fifo_batch_wise_val = True
if valuation_method == "FIFO" and d.batch_no in batches:
fifo_batch_wise_val = False
if self.is_rejected and not set_valuation_rate_for_rejected_materials:
rate = 0.0
elif (
(flt(d.incoming_rate, precision) == flt(rate, precision))
and not stock_queue
and fifo_batch_wise_val
and d.qty
and d.stock_value_difference
):
@@ -2524,26 +2529,38 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
pos_batches = frappe._dict()
pos_invoices = frappe.get_all(
"POS Invoice",
fields=[
"`tabPOS Invoice Item`.batch_no",
"`tabPOS Invoice Item`.qty",
"`tabPOS Invoice`.is_return",
"`tabPOS Invoice Item`.warehouse",
"`tabPOS Invoice Item`.name as child_docname",
"`tabPOS Invoice`.name as parent_docname",
"`tabPOS Invoice Item`.use_serial_batch_fields",
"`tabPOS Invoice Item`.serial_and_batch_bundle",
],
filters=[
["POS Invoice", "consolidated_invoice", "is", "not set"],
["POS Invoice", "docstatus", "=", 1],
["POS Invoice Item", "item_code", "=", kwargs.item_code],
["POS Invoice", "name", "not in", kwargs.ignore_voucher_nos],
],
POS_Invoice = frappe.qb.DocType("POS Invoice")
POS_Invoice_Item = frappe.qb.DocType("POS Invoice Item")
pos_invoices = (
frappe.qb.from_(POS_Invoice)
.inner_join(POS_Invoice_Item)
.on(POS_Invoice.name == POS_Invoice_Item.parent)
.select(
POS_Invoice_Item.batch_no,
POS_Invoice_Item.qty,
POS_Invoice.is_return,
POS_Invoice_Item.warehouse,
POS_Invoice_Item.name.as_("child_docname"),
POS_Invoice.name.as_("parent_docname"),
POS_Invoice_Item.use_serial_batch_fields,
POS_Invoice_Item.serial_and_batch_bundle,
)
.where(
(POS_Invoice.consolidated_invoice.isnull())
& (POS_Invoice.docstatus == 1)
& (POS_Invoice_Item.item_code == kwargs.item_code)
)
)
if kwargs.get("company"):
pos_invoices = pos_invoices.where(POS_Invoice.company == kwargs.get("company"))
if kwargs.get("ignore_voucher_nos"):
pos_invoices = pos_invoices.where(POS_Invoice.name.notin(kwargs.get("ignore_voucher_nos")))
pos_invoices = pos_invoices.run(as_dict=True)
ids = [
pos_invoice.serial_and_batch_bundle
for pos_invoice in pos_invoices
@@ -2607,6 +2624,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
.groupby(sb_entry.batch_no, sre.warehouse)
)
if kwargs.get("company"):
query = query.where(sre.company == kwargs.get("company"))
if kwargs.batch_no:
if isinstance(kwargs.batch_no, list):
query = query.where(sb_entry.batch_no.isin(kwargs.batch_no))
@@ -2769,6 +2789,9 @@ def get_available_batches(kwargs):
.groupby(batch_ledger.batch_no, batch_ledger.warehouse)
)
if kwargs.get("company"):
query = query.where(stock_ledger_entry.company == kwargs.get("company"))
if not kwargs.get("for_stock_levels"):
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
@@ -2886,6 +2909,9 @@ def get_picked_batches(kwargs) -> dict[str, dict]:
)
)
if kwargs.get("company"):
query = query.where(table.company == kwargs.get("company"))
if kwargs.get("item_code"):
query = query.where(table.item_code == kwargs.get("item_code"))
@@ -3085,6 +3111,9 @@ def get_stock_ledgers_batches(kwargs):
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
)
if kwargs.get("company"):
query = query.where(stock_ledger_entry.company == kwargs.get("company"))
for field in ["warehouse", "item_code", "batch_no"]:
if not kwargs.get(field):
continue

View File

@@ -514,7 +514,9 @@ frappe.ui.form.on("Stock Entry", {
frm.fields_dict.items.grid.refresh();
frm.cscript.toggle_related_fields(frm.doc);
},
cost_center(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "items", "cost_center");
},
validate_purpose_consumption: function (frm) {
frappe
.call({

View File

@@ -70,6 +70,8 @@
"address_display",
"accounting_dimensions_section",
"project",
"column_break_wgvc",
"cost_center",
"other_info_tab",
"printing_settings",
"select_print_heading",
@@ -699,6 +701,16 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
{
"fieldname": "column_break_wgvc",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
}
],
"icon": "fa fa-file-text",
@@ -706,7 +718,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 19:21:03.338958",
"modified": "2026-03-04 19:03:23.426082",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@@ -103,6 +103,7 @@ class StockEntry(StockController):
asset_repair: DF.Link | None
bom_no: DF.Link | None
company: DF.Link
cost_center: DF.Link | None
credit_note: DF.Link | None
delivery_note_no: DF.Link | None
fg_completed_qty: DF.Float
@@ -589,9 +590,6 @@ class StockEntry(StockController):
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
)
if self.purpose == "Manufacture":
item.set("expense_account", item_details.get("expense_account"))
def validate_fg_completed_qty(self):
item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order:
@@ -2136,7 +2134,7 @@ class StockEntry(StockController):
self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
)
if original_item != item.get("item_code"):
if isinstance(original_item, str) and original_item != item.get("item_code"):
item["original_item"] = original_item
self.add_to_stock_entry_detail(item_dict)

View File

@@ -7,7 +7,7 @@ from math import ceil
import frappe
from frappe import _
from frappe.utils import add_days, cint, flt, nowdate
from frappe.utils import add_days, cint, escape_html, flt, nowdate
import erpnext
@@ -219,15 +219,6 @@ def create_material_request(material_requests):
mr_list = []
exceptions_list = []
def _log_exception(mr):
if frappe.local.message_log:
exceptions_list.extend(frappe.local.message_log)
frappe.local.message_log = []
else:
exceptions_list.append(frappe.get_traceback(with_context=True))
mr.log_error("Unable to create material request")
company_wise_mr = frappe._dict({})
for request_type in material_requests:
for company in material_requests[request_type]:
@@ -297,8 +288,9 @@ def create_material_request(material_requests):
company_wise_mr.setdefault(company, []).append(mr)
except Exception:
_log_exception(mr)
except Exception as exception:
exceptions_list.append(exception)
mr.log_error("Unable to create material request")
if company_wise_mr:
if getattr(frappe.local, "reorder_email_notify", None) is None:
@@ -383,10 +375,7 @@ def notify_errors(exceptions_list):
for exception in exceptions_list:
try:
exception = json.loads(exception)
error_message = """<div class='small text-muted'>{}</div><br>""".format(
_(exception.get("message"))
)
error_message = f"<div class='small text-muted'>{escape_html(str(exception))}</div><br>"
content += error_message
except Exception:
pass

View File

@@ -243,10 +243,7 @@ class FIFOSlots:
consumed/updated and maintained via FIFO. **
}
"""
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle,
)
from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle
stock_ledger_entries = self.sle
@@ -271,7 +268,7 @@ class FIFOSlots:
if bundle_wise_serial_nos:
serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or []
else:
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or []
serial_nos = sorted(get_serial_nos_from_bundle(d.serial_and_batch_bundle)) or []
serial_nos = self.uppercase_serial_nos(serial_nos)
if d.actual_qty > 0:

View File

@@ -2058,7 +2058,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (
posting_datetime > %(posting_datetime)s