mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-21 14:09:19 +00:00
Merge pull request #53700 from frappe/version-15-hotfix
This commit is contained in:
@@ -243,8 +243,10 @@ def get_other_conditions(conditions, values, args):
|
|||||||
if group_condition:
|
if group_condition:
|
||||||
conditions += " and " + group_condition
|
conditions += " and " + group_condition
|
||||||
|
|
||||||
date = args.get("transaction_date") or frappe.get_value(
|
date = (
|
||||||
args.get("doctype"), args.get("name"), "posting_date", ignore=True
|
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:
|
if date:
|
||||||
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
|
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
|
||||||
|
|||||||
@@ -382,7 +382,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "bill_no",
|
"collapsible_depends_on": "posting_date",
|
||||||
"fieldname": "supplier_invoice_details",
|
"fieldname": "supplier_invoice_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Supplier Invoice"
|
"label": "Supplier Invoice"
|
||||||
@@ -1660,7 +1660,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-05 20:45:16.964500",
|
"modified": "2026-03-17 20:44:00.221219",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -2516,7 +2516,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
|||||||
"doctype": target_doctype,
|
"doctype": target_doctype,
|
||||||
"postprocess": update_details,
|
"postprocess": update_details,
|
||||||
"set_target_warehouse": "set_from_warehouse",
|
"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,
|
doctype + " Item": item_field_map,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4835,6 +4835,33 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
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):
|
def make_item_for_si(item_code, properties=None):
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|||||||
@@ -152,7 +152,9 @@ class ShippingRule(Document):
|
|||||||
frappe.throw(_("Shipping rule only applicable for Buying"))
|
frappe.throw(_("Shipping rule only applicable for Buying"))
|
||||||
|
|
||||||
shipping_charge["doctype"] = "Purchase Taxes and Charges"
|
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"
|
shipping_charge["add_deduct_tax"] = "Add"
|
||||||
|
|
||||||
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)
|
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt, formatdate
|
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
|
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ def execute(filters=None):
|
|||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
|
validate_filters(filters)
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
if filters.get("budget_against_filter"):
|
if filters.get("budget_against_filter"):
|
||||||
dimensions = filters.get("budget_against_filter")
|
dimensions = filters.get("budget_against_filter")
|
||||||
@@ -35,6 +38,21 @@ def execute(filters=None):
|
|||||||
return columns, data, None, chart
|
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):
|
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
|
||||||
for account, monthwise_data in dimension_items.items():
|
for account, monthwise_data in dimension_items.items():
|
||||||
row = [dimension, account]
|
row = [dimension, account]
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
|
|
||||||
item_list = get_items(filters, additional_table_columns)
|
item_list = get_items(filters, additional_table_columns)
|
||||||
aii_account_map = get_aii_accounts()
|
aii_account_map = get_aii_accounts()
|
||||||
|
default_taxes = {}
|
||||||
if item_list:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(
|
itemised_tax, tax_columns = get_tax_accounts(
|
||||||
item_list,
|
item_list,
|
||||||
@@ -39,6 +40,9 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
doctype="Purchase Invoice",
|
doctype="Purchase Invoice",
|
||||||
tax_doctype="Purchase Taxes and Charges",
|
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)
|
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
|
total_tax = 0
|
||||||
|
row.update(default_taxes.copy())
|
||||||
|
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||||
row.update(
|
row.update(
|
||||||
|
|||||||
@@ -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")
|
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||||
|
|
||||||
item_list = get_items(filters, additional_table_columns, additional_conditions)
|
item_list = get_items(filters, additional_table_columns, additional_conditions)
|
||||||
|
default_taxes = {}
|
||||||
if item_list:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
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))
|
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)
|
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_tax = 0
|
||||||
total_other_charges = 0
|
total_other_charges = 0
|
||||||
|
row.update(default_taxes.copy())
|
||||||
|
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||||
row.update(
|
row.update(
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
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(
|
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,
|
company=self.company,
|
||||||
customer=self.customer,
|
customer=self.customer,
|
||||||
debit_to=self.debit_to,
|
debit_to=self.debit_to,
|
||||||
@@ -30,6 +32,19 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
|||||||
price_list_rate=100,
|
price_list_rate=100,
|
||||||
do_not_save=1,
|
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()
|
si = si.save()
|
||||||
if not do_not_submit:
|
if not do_not_submit:
|
||||||
si = si.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}
|
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||||
self.assertDictEqual(report_output, 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)
|
||||||
|
|||||||
@@ -912,6 +912,8 @@ def get_list_context(context=None):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_status(status, name):
|
def update_status(status, name):
|
||||||
|
frappe.has_permission("Purchase Order", "submit", name, throw=True)
|
||||||
|
|
||||||
po = frappe.get_doc("Purchase Order", name)
|
po = frappe.get_doc("Purchase Order", name)
|
||||||
po.update_status(status)
|
po.update_status(status)
|
||||||
po.update_delivered_qty_in_sales_order()
|
po.update_delivered_qty_in_sales_order()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ def get_columns(filters):
|
|||||||
"fieldname": "transferred_qty",
|
"fieldname": "transferred_qty",
|
||||||
"width": 200,
|
"width": 200,
|
||||||
},
|
},
|
||||||
|
{"label": _("Returned Quantity"), "fieldtype": "Float", "fieldname": "returned_qty", "width": 150},
|
||||||
{"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150},
|
{"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ def get_data(filters):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for row in order_rm_item_details:
|
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):
|
if transferred_qty < row.get("reqd_qty", 0):
|
||||||
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
||||||
row.p_qty = pending_qty if pending_qty > 0 else 0
|
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}`.rm_item_code as rm_item_code",
|
||||||
f"`tab{supplied_items_table}`.required_qty as reqd_qty",
|
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}`.supplied_qty as transferred_qty",
|
||||||
|
f"`tab{supplied_items_table}`.returned_qty as returned_qty",
|
||||||
],
|
],
|
||||||
filters=record_filters,
|
filters=record_filters,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2297,6 +2297,16 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
return stock_items
|
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):
|
def calculate_total_advance_from_ledger(self):
|
||||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ class StatusUpdater(Document):
|
|||||||
self.global_amount_allowance = None
|
self.global_amount_allowance = None
|
||||||
|
|
||||||
for args in self.status_updater:
|
for args in self.status_updater:
|
||||||
if "target_ref_field" not in args:
|
if "target_ref_field" not in args or args.get("validate_qty") is False:
|
||||||
# if target_ref_field is not specified, the programmer does not want to validate qty / amount
|
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# get unique transactions to update
|
# get unique transactions to update
|
||||||
|
|||||||
@@ -989,6 +989,12 @@ class SubcontractingController(StockController):
|
|||||||
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||||
return
|
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):
|
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))
|
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):
|
if not self.__transferred_items or not self.__transferred_items.get(key):
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ def validate_filters(filters):
|
|||||||
if filters.get("based_on") == filters.get("group_by"):
|
if filters.get("based_on") == filters.get("group_by"):
|
||||||
frappe.throw(_("'Based On' and 'Group By' can not be same"))
|
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):
|
def get_data(filters, conditions):
|
||||||
data = []
|
data = []
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
bom_no: frm.doc.name,
|
bom_no: frm.doc.name,
|
||||||
item: item,
|
item: item,
|
||||||
qty: data.qty || 0.0,
|
qty: data.qty || 0.0,
|
||||||
|
company: frm.doc.company,
|
||||||
project: frm.doc.project,
|
project: frm.doc.project,
|
||||||
variant_items: variant_items,
|
variant_items: variant_items,
|
||||||
use_multi_level_bom: use_multi_level_bom,
|
use_multi_level_bom: use_multi_level_bom,
|
||||||
|
|||||||
@@ -1019,6 +1019,12 @@ class BOM(WebsiteGenerator):
|
|||||||
"Row {0}: Workstation or Workstation Type is mandatory for an operation {1}"
|
"Row {0}: Workstation or Workstation Type is mandatory for an operation {1}"
|
||||||
).format(d.idx, d.operation)
|
).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:
|
def get_tree_representation(self) -> BOMTree:
|
||||||
"""Get a complete tree representation preserving order of child items."""
|
"""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)
|
bom = frappe.get_doc("BOM", work_order.bom_no)
|
||||||
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
|
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
|
||||||
|
|
||||||
items = {}
|
items = frappe._dict()
|
||||||
for d in bom.get(table):
|
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(
|
non_stock_items = frappe.get_all(
|
||||||
"Item",
|
"Item",
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_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
|
@timeout
|
||||||
def test_bom_cost_with_batch_size(self):
|
def test_bom_cost_with_batch_size(self):
|
||||||
bom = frappe.copy_doc(test_records[2])
|
bom = frappe.copy_doc(test_records[2])
|
||||||
|
|||||||
@@ -1372,7 +1372,11 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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"):
|
if not frappe.has_permission("Work Order", "write"):
|
||||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
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 = frappe.new_doc("Work Order")
|
||||||
wo_doc.production_item = item
|
wo_doc.production_item = item
|
||||||
|
wo_doc.company = company or get_default_company()
|
||||||
wo_doc.update(item_details)
|
wo_doc.update(item_details)
|
||||||
wo_doc.bom_no = bom_no
|
wo_doc.bom_no = bom_no
|
||||||
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)
|
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)
|
||||||
|
|||||||
@@ -47,21 +47,8 @@ def create_customer_or_supplier():
|
|||||||
if party_exists(doctype, user):
|
if party_exists(doctype, user):
|
||||||
return
|
return
|
||||||
|
|
||||||
party = frappe.new_doc(doctype)
|
|
||||||
fullname = frappe.utils.get_fullname(user)
|
fullname = frappe.utils.get_fullname(user)
|
||||||
|
party = create_party(doctype, fullname)
|
||||||
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)
|
|
||||||
|
|
||||||
alternate_doctype = "Customer" if doctype == "Supplier" else "Supplier"
|
alternate_doctype = "Customer" if doctype == "Supplier" else "Supplier"
|
||||||
|
|
||||||
if party_exists(alternate_doctype, user):
|
if party_exists(alternate_doctype, user):
|
||||||
@@ -69,6 +56,22 @@ def create_customer_or_supplier():
|
|||||||
fullname += "-" + doctype
|
fullname += "-" + doctype
|
||||||
|
|
||||||
create_party_contact(doctype, fullname, user, party.name)
|
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
|
return party
|
||||||
|
|
||||||
|
|||||||
@@ -1681,6 +1681,8 @@ def make_work_orders(items, sales_order, company, project=None):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_status(status, name):
|
def update_status(status, name):
|
||||||
|
frappe.has_permission("Sales Order", "submit", name, throw=True)
|
||||||
|
|
||||||
so = frappe.get_doc("Sales Order", name)
|
so = frappe.get_doc("Sales Order", name)
|
||||||
so.update_status(status)
|
so.update_status(status)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ from erpnext.stock.serial_batch_bundle import (
|
|||||||
)
|
)
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
|
|
||||||
|
|
||||||
|
class MissingWarehouseValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IncorrectWarehouseValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# TODO: Prioritize SO or WO group warehouse
|
# TODO: Prioritize SO or WO group warehouse
|
||||||
|
|
||||||
|
|
||||||
@@ -94,6 +103,7 @@ class PickList(TransactionBase):
|
|||||||
|
|
||||||
if self.get("locations"):
|
if self.get("locations"):
|
||||||
self.validate_sales_order_percentage()
|
self.validate_sales_order_percentage()
|
||||||
|
self.validate_warehouses()
|
||||||
|
|
||||||
def validate_stock_qty(self):
|
def validate_stock_qty(self):
|
||||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
@@ -138,6 +148,31 @@ class PickList(TransactionBase):
|
|||||||
title=_("Insufficient Stock"),
|
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):
|
def check_serial_no_status(self):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
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(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
|
company,
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1049,6 +1085,7 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
|
company,
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
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(
|
def get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
|
company,
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
@@ -1138,6 +1176,7 @@ def get_available_item_locations_for_batched_item(
|
|||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"warehouse": from_warehouses,
|
"warehouse": from_warehouses,
|
||||||
"based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
"based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
||||||
|
"company": company,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
"qty": 1000,
|
"qty": 1000,
|
||||||
"stock_qty": 1000,
|
"stock_qty": 1000,
|
||||||
"conversion_factor": 1,
|
"conversion_factor": 1,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
"sales_order": so.name,
|
"sales_order": so.name,
|
||||||
"sales_order_item": so.items[0].name,
|
"sales_order_item": so.items[0].name,
|
||||||
}
|
}
|
||||||
@@ -268,6 +269,119 @@ class TestPickList(FrappeTestCase):
|
|||||||
pr1.cancel()
|
pr1.cancel()
|
||||||
pr2.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):
|
def test_pick_list_for_batched_and_serialised_item(self):
|
||||||
# check if oldest batch no and serial nos are picked
|
# check if oldest batch no and serial nos are picked
|
||||||
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Warehouse",
|
"label": "Warehouse",
|
||||||
|
"mandatory_depends_on": "eval: parent.pick_manually",
|
||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -283,7 +284,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-09-23 00:02:57.817040",
|
"modified": "2026-03-17 16:25:10.358013",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
"target_ref_field": "stock_qty",
|
"target_ref_field": "stock_qty",
|
||||||
"source_field": "stock_qty",
|
"source_field": "stock_qty",
|
||||||
"percent_join_field": "material_request",
|
"percent_join_field": "material_request",
|
||||||
|
"validate_qty": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source_dt": "Purchase Receipt Item",
|
"source_dt": "Purchase Receipt Item",
|
||||||
@@ -327,7 +328,10 @@ class PurchaseReceipt(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def po_required(self):
|
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"):
|
for d in self.get("items"):
|
||||||
if not d.purchase_order:
|
if not d.purchase_order:
|
||||||
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
|
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()
|
@frappe.whitelist()
|
||||||
def update_purchase_receipt_status(docname, status):
|
def update_purchase_receipt_status(docname, status):
|
||||||
|
frappe.has_permission("Purchase Receipt", "submit", docname, throw=True)
|
||||||
|
|
||||||
pr = frappe.get_doc("Purchase Receipt", docname)
|
pr = frappe.get_doc("Purchase Receipt", docname)
|
||||||
pr.update_status(status)
|
pr.update_status(status)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from erpnext.controllers.accounts_controller import InvalidQtyError
|
|||||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
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.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
SerialNoDuplicateError,
|
SerialNoDuplicateError,
|
||||||
@@ -33,6 +34,40 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)
|
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):
|
def test_purchase_receipt_qty(self):
|
||||||
pr = make_purchase_receipt(qty=0, rejected_qty=0, do_not_save=True)
|
pr = make_purchase_receipt(qty=0, rejected_qty=0, do_not_save=True)
|
||||||
with self.assertRaises(InvalidQtyError):
|
with self.assertRaises(InvalidQtyError):
|
||||||
|
|||||||
@@ -748,11 +748,16 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
|
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
|
||||||
for d in self.entries:
|
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:
|
if self.is_rejected and not set_valuation_rate_for_rejected_materials:
|
||||||
rate = 0.0
|
rate = 0.0
|
||||||
elif (
|
elif (
|
||||||
(flt(d.incoming_rate, precision) == flt(rate, precision))
|
(flt(d.incoming_rate, precision) == flt(rate, precision))
|
||||||
and not stock_queue
|
and not stock_queue
|
||||||
|
and fifo_batch_wise_val
|
||||||
and d.qty
|
and d.qty
|
||||||
and d.stock_value_difference
|
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."""
|
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
|
||||||
|
|
||||||
pos_batches = frappe._dict()
|
pos_batches = frappe._dict()
|
||||||
pos_invoices = frappe.get_all(
|
POS_Invoice = frappe.qb.DocType("POS Invoice")
|
||||||
"POS Invoice",
|
POS_Invoice_Item = frappe.qb.DocType("POS Invoice Item")
|
||||||
fields=[
|
|
||||||
"`tabPOS Invoice Item`.batch_no",
|
pos_invoices = (
|
||||||
"`tabPOS Invoice Item`.qty",
|
frappe.qb.from_(POS_Invoice)
|
||||||
"`tabPOS Invoice`.is_return",
|
.inner_join(POS_Invoice_Item)
|
||||||
"`tabPOS Invoice Item`.warehouse",
|
.on(POS_Invoice.name == POS_Invoice_Item.parent)
|
||||||
"`tabPOS Invoice Item`.name as child_docname",
|
.select(
|
||||||
"`tabPOS Invoice`.name as parent_docname",
|
POS_Invoice_Item.batch_no,
|
||||||
"`tabPOS Invoice Item`.use_serial_batch_fields",
|
POS_Invoice_Item.qty,
|
||||||
"`tabPOS Invoice Item`.serial_and_batch_bundle",
|
POS_Invoice.is_return,
|
||||||
],
|
POS_Invoice_Item.warehouse,
|
||||||
filters=[
|
POS_Invoice_Item.name.as_("child_docname"),
|
||||||
["POS Invoice", "consolidated_invoice", "is", "not set"],
|
POS_Invoice.name.as_("parent_docname"),
|
||||||
["POS Invoice", "docstatus", "=", 1],
|
POS_Invoice_Item.use_serial_batch_fields,
|
||||||
["POS Invoice Item", "item_code", "=", kwargs.item_code],
|
POS_Invoice_Item.serial_and_batch_bundle,
|
||||||
["POS Invoice", "name", "not in", kwargs.ignore_voucher_nos],
|
)
|
||||||
],
|
.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 = [
|
ids = [
|
||||||
pos_invoice.serial_and_batch_bundle
|
pos_invoice.serial_and_batch_bundle
|
||||||
for pos_invoice in pos_invoices
|
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)
|
.groupby(sb_entry.batch_no, sre.warehouse)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get("company"):
|
||||||
|
query = query.where(sre.company == kwargs.get("company"))
|
||||||
|
|
||||||
if kwargs.batch_no:
|
if kwargs.batch_no:
|
||||||
if isinstance(kwargs.batch_no, list):
|
if isinstance(kwargs.batch_no, list):
|
||||||
query = query.where(sb_entry.batch_no.isin(kwargs.batch_no))
|
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)
|
.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"):
|
if not kwargs.get("for_stock_levels"):
|
||||||
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
|
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"):
|
if kwargs.get("item_code"):
|
||||||
query = query.where(table.item_code == 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)
|
.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"]:
|
for field in ["warehouse", "item_code", "batch_no"]:
|
||||||
if not kwargs.get(field):
|
if not kwargs.get(field):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -514,7 +514,9 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
frm.fields_dict.items.grid.refresh();
|
frm.fields_dict.items.grid.refresh();
|
||||||
frm.cscript.toggle_related_fields(frm.doc);
|
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) {
|
validate_purpose_consumption: function (frm) {
|
||||||
frappe
|
frappe
|
||||||
.call({
|
.call({
|
||||||
|
|||||||
@@ -70,6 +70,8 @@
|
|||||||
"address_display",
|
"address_display",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"project",
|
"project",
|
||||||
|
"column_break_wgvc",
|
||||||
|
"cost_center",
|
||||||
"other_info_tab",
|
"other_info_tab",
|
||||||
"printing_settings",
|
"printing_settings",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
@@ -699,6 +701,16 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"label": "Last Scanned Warehouse"
|
"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",
|
"icon": "fa fa-file-text",
|
||||||
@@ -706,7 +718,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-04 19:21:03.338958",
|
"modified": "2026-03-04 19:03:23.426082",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry",
|
"name": "Stock Entry",
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class StockEntry(StockController):
|
|||||||
asset_repair: DF.Link | None
|
asset_repair: DF.Link | None
|
||||||
bom_no: DF.Link | None
|
bom_no: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
|
cost_center: DF.Link | None
|
||||||
credit_note: DF.Link | None
|
credit_note: DF.Link | None
|
||||||
delivery_note_no: DF.Link | None
|
delivery_note_no: DF.Link | None
|
||||||
fg_completed_qty: DF.Float
|
fg_completed_qty: DF.Float
|
||||||
@@ -589,9 +590,6 @@ class StockEntry(StockController):
|
|||||||
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
|
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):
|
def validate_fg_completed_qty(self):
|
||||||
item_wise_qty = {}
|
item_wise_qty = {}
|
||||||
if self.purpose == "Manufacture" and self.work_order:
|
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 ""
|
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
|
item["original_item"] = original_item
|
||||||
|
|
||||||
self.add_to_stock_entry_detail(item_dict)
|
self.add_to_stock_entry_detail(item_dict)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from math import ceil
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
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
|
import erpnext
|
||||||
|
|
||||||
@@ -219,15 +219,6 @@ def create_material_request(material_requests):
|
|||||||
mr_list = []
|
mr_list = []
|
||||||
exceptions_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({})
|
company_wise_mr = frappe._dict({})
|
||||||
for request_type in material_requests:
|
for request_type in material_requests:
|
||||||
for company in material_requests[request_type]:
|
for company in material_requests[request_type]:
|
||||||
@@ -297,8 +288,9 @@ def create_material_request(material_requests):
|
|||||||
|
|
||||||
company_wise_mr.setdefault(company, []).append(mr)
|
company_wise_mr.setdefault(company, []).append(mr)
|
||||||
|
|
||||||
except Exception:
|
except Exception as exception:
|
||||||
_log_exception(mr)
|
exceptions_list.append(exception)
|
||||||
|
mr.log_error("Unable to create material request")
|
||||||
|
|
||||||
if company_wise_mr:
|
if company_wise_mr:
|
||||||
if getattr(frappe.local, "reorder_email_notify", None) is None:
|
if getattr(frappe.local, "reorder_email_notify", None) is None:
|
||||||
@@ -383,10 +375,7 @@ def notify_errors(exceptions_list):
|
|||||||
|
|
||||||
for exception in exceptions_list:
|
for exception in exceptions_list:
|
||||||
try:
|
try:
|
||||||
exception = json.loads(exception)
|
error_message = f"<div class='small text-muted'>{escape_html(str(exception))}</div><br>"
|
||||||
error_message = """<div class='small text-muted'>{}</div><br>""".format(
|
|
||||||
_(exception.get("message"))
|
|
||||||
)
|
|
||||||
content += error_message
|
content += error_message
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -243,10 +243,7 @@ class FIFOSlots:
|
|||||||
consumed/updated and maintained via FIFO. **
|
consumed/updated and maintained via FIFO. **
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
|
||||||
get_serial_nos_from_bundle,
|
|
||||||
)
|
|
||||||
|
|
||||||
stock_ledger_entries = self.sle
|
stock_ledger_entries = self.sle
|
||||||
|
|
||||||
@@ -271,7 +268,7 @@ class FIFOSlots:
|
|||||||
if bundle_wise_serial_nos:
|
if bundle_wise_serial_nos:
|
||||||
serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or []
|
serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or []
|
||||||
else:
|
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)
|
serial_nos = self.uppercase_serial_nos(serial_nos)
|
||||||
if d.actual_qty > 0:
|
if d.actual_qty > 0:
|
||||||
|
|||||||
@@ -2058,7 +2058,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
where
|
where
|
||||||
item_code = %(item_code)s
|
item_code = %(item_code)s
|
||||||
and warehouse = %(warehouse)s
|
and warehouse = %(warehouse)s
|
||||||
and voucher_no != %(voucher_no)s
|
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
and (
|
and (
|
||||||
posting_datetime > %(posting_datetime)s
|
posting_datetime > %(posting_datetime)s
|
||||||
|
|||||||
Reference in New Issue
Block a user