mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-04 20:59:11 +00:00
refactor(treewide): formatting and ruff fixes, + manually enabled F401
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
This commit is contained in:
@@ -17,9 +17,9 @@ install_docs = [
|
||||
|
||||
|
||||
def get_warehouse_account_map(company=None):
|
||||
company_warehouse_account_map = company and frappe.flags.setdefault(
|
||||
"warehouse_account_map", {}
|
||||
).get(company)
|
||||
company_warehouse_account_map = company and frappe.flags.setdefault("warehouse_account_map", {}).get(
|
||||
company
|
||||
)
|
||||
warehouse_account_map = frappe.flags.warehouse_account_map
|
||||
|
||||
if not warehouse_account_map or not company_warehouse_account_map or frappe.flags.in_test:
|
||||
|
||||
@@ -27,9 +27,7 @@ def get(
|
||||
if filters and filters.get("company"):
|
||||
warehouse_filters.append(["company", "=", filters.get("company")])
|
||||
|
||||
warehouses = frappe.get_list(
|
||||
"Warehouse", pluck="name", filters=warehouse_filters, order_by="name"
|
||||
)
|
||||
warehouses = frappe.get_list("Warehouse", pluck="name", filters=warehouse_filters, order_by="name")
|
||||
|
||||
warehouses = frappe.get_list(
|
||||
"Bin",
|
||||
|
||||
@@ -165,9 +165,7 @@ class Batch(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_qty(
|
||||
batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None
|
||||
):
|
||||
def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None):
|
||||
"""Returns batch actual qty if warehouse is passed,
|
||||
or returns dict of qty by warehouse if warehouse is None
|
||||
|
||||
@@ -221,9 +219,7 @@ def get_batch_qty(
|
||||
def get_batches_by_oldest(item_code, warehouse):
|
||||
"""Returns the oldest batch and qty for the given item_code and warehouse"""
|
||||
batches = get_batch_qty(item_code=item_code, warehouse=warehouse)
|
||||
batches_dates = [
|
||||
[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches
|
||||
]
|
||||
batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches]
|
||||
batches_dates.sort(key=lambda tup: tup[1])
|
||||
return batches_dates
|
||||
|
||||
|
||||
@@ -81,9 +81,7 @@ class TestBatch(FrappeTestCase):
|
||||
stock_entry.submit()
|
||||
|
||||
self.assertTrue(stock_entry.items[0].batch_no)
|
||||
self.assertEqual(
|
||||
get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90
|
||||
)
|
||||
self.assertEqual(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90)
|
||||
|
||||
def test_delivery_note(self):
|
||||
"""Test automatic batch selection for outgoing items"""
|
||||
@@ -159,9 +157,7 @@ class TestBatch(FrappeTestCase):
|
||||
receipt = self.test_purchase_receipt()
|
||||
from erpnext.stock.doctype.batch.batch import split_batch
|
||||
|
||||
new_batch = split_batch(
|
||||
receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22
|
||||
)
|
||||
new_batch = split_batch(receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22)
|
||||
|
||||
self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78)
|
||||
self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22)
|
||||
@@ -359,9 +355,7 @@ class TestBatch(FrappeTestCase):
|
||||
self.make_batch_item(item_code)
|
||||
|
||||
def assertValuation(expected):
|
||||
actual = get_valuation_rate(
|
||||
item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no
|
||||
)
|
||||
actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no)
|
||||
self.assertAlmostEqual(actual, expected)
|
||||
|
||||
se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
|
||||
|
||||
@@ -58,9 +58,7 @@ class Bin(Document):
|
||||
get_reserved_qty_for_sub_assembly,
|
||||
)
|
||||
|
||||
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
|
||||
self.item_code, self.warehouse
|
||||
)
|
||||
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(self.item_code, self.warehouse)
|
||||
|
||||
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
|
||||
return
|
||||
@@ -81,9 +79,7 @@ class Bin(Document):
|
||||
in open work orders"""
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
|
||||
|
||||
self.reserved_qty_for_production = get_reserved_qty_for_production(
|
||||
self.item_code, self.warehouse
|
||||
)
|
||||
self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse)
|
||||
|
||||
self.db_set(
|
||||
"reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True
|
||||
@@ -131,9 +127,7 @@ class Bin(Document):
|
||||
se_item = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
if frappe.db.field_exists("Stock Entry", "is_return"):
|
||||
qty_field = (
|
||||
Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)
|
||||
)
|
||||
qty_field = Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)
|
||||
else:
|
||||
qty_field = se_item.transfer_qty
|
||||
|
||||
|
||||
@@ -31,4 +31,4 @@ class TestBin(FrappeTestCase):
|
||||
def test_index_exists(self):
|
||||
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
|
||||
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
|
||||
self.fail(f"Expected unique index on item-warehouse")
|
||||
self.fail("Expected unique index on item-warehouse")
|
||||
|
||||
@@ -126,6 +126,6 @@ def prepare_closing_stock_balance(name):
|
||||
try:
|
||||
doc.create_closing_stock_balance_entries()
|
||||
doc.db_set("status", "Completed")
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
doc.db_set("status", "Failed")
|
||||
doc.log_error(title="Closing Stock Balance Failed")
|
||||
|
||||
@@ -20,7 +20,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
class DeliveryNote(SellingController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DeliveryNote, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.status_updater = [
|
||||
{
|
||||
"source_dt": "Delivery Note Item",
|
||||
@@ -108,7 +108,7 @@ class DeliveryNote(SellingController):
|
||||
for f in fieldname:
|
||||
toggle_print_hide(self.meta if key == "parent" else item_meta, f)
|
||||
|
||||
super(DeliveryNote, self).before_print(settings)
|
||||
super().before_print(settings)
|
||||
|
||||
def set_actual_qty(self):
|
||||
for d in self.get("items"):
|
||||
@@ -129,7 +129,7 @@ class DeliveryNote(SellingController):
|
||||
|
||||
def validate(self):
|
||||
self.validate_posting_time()
|
||||
super(DeliveryNote, self).validate()
|
||||
super().validate()
|
||||
self.validate_references()
|
||||
self.set_status()
|
||||
self.so_required()
|
||||
@@ -159,11 +159,16 @@ class DeliveryNote(SellingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super(DeliveryNote, self).validate_with_previous_doc(
|
||||
super().validate_with_previous_doc(
|
||||
{
|
||||
"Sales Order": {
|
||||
"ref_dn_field": "against_sales_order",
|
||||
"compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]],
|
||||
"compare_fields": [
|
||||
["customer", "="],
|
||||
["company", "="],
|
||||
["project", "="],
|
||||
["currency", "="],
|
||||
],
|
||||
},
|
||||
"Sales Order Item": {
|
||||
"ref_dn_field": "so_detail",
|
||||
@@ -173,7 +178,12 @@ class DeliveryNote(SellingController):
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"ref_dn_field": "against_sales_invoice",
|
||||
"compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]],
|
||||
"compare_fields": [
|
||||
["customer", "="],
|
||||
["company", "="],
|
||||
["project", "="],
|
||||
["currency", "="],
|
||||
],
|
||||
},
|
||||
"Sales Invoice Item": {
|
||||
"ref_dn_field": "si_detail",
|
||||
@@ -263,7 +273,7 @@ class DeliveryNote(SellingController):
|
||||
)
|
||||
|
||||
def validate_warehouse(self):
|
||||
super(DeliveryNote, self).validate_warehouse()
|
||||
super().validate_warehouse()
|
||||
|
||||
for d in self.get_item_list():
|
||||
if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1:
|
||||
@@ -311,7 +321,7 @@ class DeliveryNote(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
def on_cancel(self):
|
||||
super(DeliveryNote, self).on_cancel()
|
||||
super().on_cancel()
|
||||
|
||||
self.check_sales_order_on_hold_or_close("against_sales_order")
|
||||
self.check_next_docstatus()
|
||||
@@ -851,7 +861,7 @@ def make_shipment(source_name, target_doc=None):
|
||||
"User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1
|
||||
)
|
||||
target.pickup_contact_email = user.email
|
||||
pickup_contact_display = "{}".format(user.full_name)
|
||||
pickup_contact_display = f"{user.full_name}"
|
||||
if user:
|
||||
if user.email:
|
||||
pickup_contact_display += "<br>" + user.email
|
||||
@@ -867,7 +877,7 @@ def make_shipment(source_name, target_doc=None):
|
||||
contact = frappe.db.get_value(
|
||||
"Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1
|
||||
)
|
||||
delivery_contact_display = "{}".format(source.contact_display)
|
||||
delivery_contact_display = f"{source.contact_display}"
|
||||
if contact:
|
||||
if contact.email_id:
|
||||
delivery_contact_display += "<br>" + contact.email_id
|
||||
@@ -1043,8 +1053,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"postprocess": update_details,
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse"],
|
||||
},
|
||||
doctype
|
||||
+ " Item": {
|
||||
doctype + " Item": {
|
||||
"doctype": target_doctype + " Item",
|
||||
"field_map": {
|
||||
source_document_warehouse_field: target_document_warehouse_field,
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(si).insert)
|
||||
|
||||
def test_delivery_note_no_gl_entry(self):
|
||||
company = frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company")
|
||||
frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company")
|
||||
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
|
||||
|
||||
stock_queue = json.loads(
|
||||
@@ -71,16 +71,14 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
dn = create_delivery_note()
|
||||
|
||||
sle = frappe.get_doc(
|
||||
"Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}
|
||||
)
|
||||
sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name})
|
||||
|
||||
self.assertEqual(sle.stock_value_difference, flt(-1 * stock_queue[0][1], 2))
|
||||
|
||||
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
|
||||
|
||||
def test_delivery_note_gl_entry_packing_item(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=10, basic_rate=100)
|
||||
make_stock_entry(
|
||||
@@ -127,7 +125,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
stock_in_hand_account: [0.0, stock_value_diff],
|
||||
"Cost of Goods Sold - TCP1": [stock_value_diff, 0.0],
|
||||
}
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
|
||||
|
||||
# check stock in hand balance
|
||||
@@ -159,9 +157,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)
|
||||
serial_no = "\n".join(serial_no)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no
|
||||
)
|
||||
dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no)
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
si.items[0].qty = 1
|
||||
@@ -694,7 +690,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
"Stock In Hand - TCP1": [0.0, stock_value_difference],
|
||||
target_warehouse: [stock_value_difference, 0.0],
|
||||
}
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
|
||||
|
||||
# tear down
|
||||
@@ -902,7 +898,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
"Cost of Goods Sold - TCP1": {"cost_center": cost_center},
|
||||
stock_in_hand_account: {"cost_center": cost_center},
|
||||
}
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
def test_delivery_note_cost_center_with_balance_sheet_account(self):
|
||||
@@ -931,7 +927,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
"Cost of Goods Sold - TCP1": {"cost_center": cost_center},
|
||||
stock_in_hand_account: {"cost_center": cost_center},
|
||||
}
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
def test_make_sales_invoice_from_dn_for_returned_qty(self):
|
||||
@@ -997,9 +993,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
},
|
||||
)
|
||||
make_product_bundle(parent=batched_bundle.name, items=[batched_item.name])
|
||||
make_stock_entry(
|
||||
item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42
|
||||
)
|
||||
make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42)
|
||||
|
||||
try:
|
||||
dn = create_delivery_note(item_code=batched_bundle.name, qty=1)
|
||||
@@ -1008,9 +1002,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.fail("Batch numbers not getting added to bundled items in DN.")
|
||||
raise e
|
||||
|
||||
self.assertTrue(
|
||||
"TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item"
|
||||
)
|
||||
self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item")
|
||||
|
||||
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
@@ -1185,9 +1177,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
warehouse=warehouse,
|
||||
target_warehouse=target,
|
||||
)
|
||||
self.assertFalse(
|
||||
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
|
||||
)
|
||||
self.assertFalse(frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}))
|
||||
|
||||
def test_batch_expiry_for_delivery_note(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
@@ -1262,11 +1252,9 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
).name
|
||||
|
||||
# Step - 2: Inward Stock
|
||||
se1 = make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3)
|
||||
make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3)
|
||||
serial_nos = (
|
||||
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3)
|
||||
.items[0]
|
||||
.serial_no
|
||||
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3).items[0].serial_no
|
||||
)
|
||||
|
||||
# Step - 3: Create a Product Bundle
|
||||
@@ -1353,9 +1341,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
basic_rate=100.0,
|
||||
posting_date=add_days(nowdate(), -5),
|
||||
)
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
dn = create_delivery_note(item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4))
|
||||
self.assertEqual(dn.items[0].incoming_rate, 100.0)
|
||||
|
||||
make_stock_entry(
|
||||
|
||||
@@ -13,7 +13,7 @@ from frappe.utils import cint, get_datetime, get_link_to_form
|
||||
|
||||
class DeliveryTrip(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DeliveryTrip, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Google Maps returns distances in meters by default
|
||||
self.default_distance_uom = (
|
||||
@@ -67,9 +67,7 @@ class DeliveryTrip(Document):
|
||||
delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`.
|
||||
"""
|
||||
|
||||
delivery_notes = list(
|
||||
set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)
|
||||
)
|
||||
delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note))
|
||||
|
||||
update_fields = {
|
||||
"driver": self.driver,
|
||||
@@ -315,14 +313,11 @@ def get_contact_display(contact):
|
||||
"Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1
|
||||
)
|
||||
|
||||
contact_info.html = (
|
||||
""" <b>%(first_name)s %(last_name)s</b> <br> %(phone)s <br> %(mobile_no)s"""
|
||||
% {
|
||||
"first_name": contact_info.first_name,
|
||||
"last_name": contact_info.last_name or "",
|
||||
"phone": contact_info.phone or "",
|
||||
"mobile_no": contact_info.mobile_no or "",
|
||||
}
|
||||
contact_info.html = """ <b>{first_name} {last_name}</b> <br> {phone} <br> {mobile_no}""".format(
|
||||
first_name=contact_info.first_name,
|
||||
last_name=contact_info.last_name or "",
|
||||
phone=contact_info.phone or "",
|
||||
mobile_no=contact_info.mobile_no or "",
|
||||
)
|
||||
|
||||
return contact_info.html
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
@@ -360,9 +360,7 @@ def delete_dimension(dimension):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_parent_fields(child_doctype, dimension_name):
|
||||
parent_doctypes = frappe.get_all(
|
||||
"DocField", fields=["parent"], filters={"options": child_doctype}
|
||||
)
|
||||
parent_doctypes = frappe.get_all("DocField", fields=["parent"], filters={"options": child_doctype})
|
||||
|
||||
fields = []
|
||||
|
||||
|
||||
@@ -210,9 +210,7 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
frappe.db.get_value(
|
||||
"Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name"
|
||||
)
|
||||
frappe.db.get_value("Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name")
|
||||
)
|
||||
|
||||
def test_check_mandatory_dimensions(self):
|
||||
@@ -296,9 +294,7 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
se_doc.save()
|
||||
se_doc.submit()
|
||||
|
||||
entries = get_voucher_sl_entries(
|
||||
se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"]
|
||||
)
|
||||
entries = get_voucher_sl_entries(se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"])
|
||||
|
||||
for entry in entries:
|
||||
self.assertEqual(entry.warehouse, warehouse)
|
||||
@@ -488,7 +484,14 @@ def create_store_dimension():
|
||||
"autoname": "field:store_name",
|
||||
"fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||
{
|
||||
"role": "System Manager",
|
||||
"permlevel": 0,
|
||||
"read": 1,
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -510,7 +513,14 @@ def prepare_test_data():
|
||||
"autoname": "field:shelf_name",
|
||||
"fields": [{"label": "Shelf Name", "fieldname": "shelf_name", "fieldtype": "Data"}],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||
{
|
||||
"role": "System Manager",
|
||||
"permlevel": 0,
|
||||
"read": 1,
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -532,7 +542,14 @@ def prepare_test_data():
|
||||
"autoname": "field:rack_name",
|
||||
"fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||
{
|
||||
"role": "System Manager",
|
||||
"permlevel": 0,
|
||||
"read": 1,
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -554,7 +571,14 @@ def prepare_test_data():
|
||||
"autoname": "field:pallet_name",
|
||||
"fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||
{
|
||||
"role": "System Manager",
|
||||
"permlevel": 0,
|
||||
"read": 1,
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -570,7 +594,14 @@ def prepare_test_data():
|
||||
"autoname": "field:site_name",
|
||||
"fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}],
|
||||
"permissions": [
|
||||
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
|
||||
{
|
||||
"role": "System Manager",
|
||||
"permlevel": 0,
|
||||
"read": 1,
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -623,9 +654,7 @@ def prepare_data_for_internal_transfer():
|
||||
|
||||
to_warehouse = create_warehouse("_Test Internal Warehouse GIT A", company=company)
|
||||
|
||||
pr_doc = make_purchase_receipt(
|
||||
company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True
|
||||
)
|
||||
pr_doc = make_purchase_receipt(company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True)
|
||||
pr_doc.items[0].store = "Inter Transfer Store 1"
|
||||
pr_doc.submit()
|
||||
|
||||
@@ -651,9 +680,7 @@ def prepare_data_for_internal_transfer():
|
||||
|
||||
expense_account = frappe.db.get_value(
|
||||
"Company", company, "stock_adjustment_account"
|
||||
) or frappe.db.get_value(
|
||||
"Account", {"company": company, "account_type": "Expense Account"}, "name"
|
||||
)
|
||||
) or frappe.db.get_value("Account", {"company": company, "account_type": "Expense Account"}, "name")
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -300,9 +299,9 @@ class Item(Document):
|
||||
for d in self.get("uoms"):
|
||||
if cstr(d.uom) in check_list:
|
||||
frappe.throw(
|
||||
_("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(
|
||||
d.uom
|
||||
)
|
||||
_(
|
||||
"Unit of Measure {0} has been entered more than once in Conversion Factor Table"
|
||||
).format(d.uom)
|
||||
)
|
||||
else:
|
||||
check_list.append(cstr(d.uom))
|
||||
@@ -354,7 +353,7 @@ class Item(Document):
|
||||
frappe.throw(
|
||||
_("{0} entered twice {1} in Item Taxes").format(
|
||||
frappe.bold(d.item_tax_template),
|
||||
"for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "",
|
||||
f"for tax category {frappe.bold(d.tax_category)}" if d.tax_category else "",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -373,7 +372,9 @@ class Item(Document):
|
||||
)
|
||||
if duplicate:
|
||||
frappe.throw(
|
||||
_("Barcode {0} already used in Item {1}").format(item_barcode.barcode, duplicate[0][0])
|
||||
_("Barcode {0} already used in Item {1}").format(
|
||||
item_barcode.barcode, duplicate[0][0]
|
||||
)
|
||||
)
|
||||
|
||||
item_barcode.barcode_type = (
|
||||
@@ -403,9 +404,9 @@ class Item(Document):
|
||||
warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))]
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format(
|
||||
d.idx, d.warehouse, d.material_request_type
|
||||
),
|
||||
_(
|
||||
"Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}."
|
||||
).format(d.idx, d.warehouse, d.material_request_type),
|
||||
DuplicateReorderRows,
|
||||
)
|
||||
|
||||
@@ -477,20 +478,21 @@ class Item(Document):
|
||||
|
||||
for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"):
|
||||
for d in frappe.db.sql(
|
||||
"""select name, item_wise_tax_detail from `tab{0}`
|
||||
where ifnull(item_wise_tax_detail, '') != ''""".format(
|
||||
dt
|
||||
),
|
||||
f"""select name, item_wise_tax_detail from `tab{dt}`
|
||||
where ifnull(item_wise_tax_detail, '') != ''""",
|
||||
as_dict=1,
|
||||
):
|
||||
|
||||
item_wise_tax_detail = json.loads(d.item_wise_tax_detail)
|
||||
if isinstance(item_wise_tax_detail, dict) and old_name in item_wise_tax_detail:
|
||||
item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name]
|
||||
item_wise_tax_detail.pop(old_name)
|
||||
|
||||
frappe.db.set_value(
|
||||
dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False
|
||||
dt,
|
||||
d.name,
|
||||
"item_wise_tax_detail",
|
||||
json.dumps(item_wise_tax_detail),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
def delete_old_bins(self, old_name):
|
||||
@@ -517,9 +519,7 @@ class Item(Document):
|
||||
)
|
||||
|
||||
msg += " <br>"
|
||||
msg += (
|
||||
", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
|
||||
)
|
||||
msg += ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
|
||||
|
||||
msg += _(
|
||||
"Note: To merge the items, create a separate Stock Reconciliation for the old item {0}"
|
||||
@@ -542,12 +542,8 @@ class Item(Document):
|
||||
|
||||
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
|
||||
"Block merge if both old and new items have product bundles."
|
||||
old_bundle = frappe.get_value(
|
||||
"Product Bundle", filters={"new_item_code": old_name, "disabled": 0}
|
||||
)
|
||||
new_bundle = frappe.get_value(
|
||||
"Product Bundle", filters={"new_item_code": new_name, "disabled": 0}
|
||||
)
|
||||
old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name, "disabled": 0})
|
||||
new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name, "disabled": 0})
|
||||
|
||||
if old_bundle and new_bundle:
|
||||
bundle_link = get_link_to_form("Product Bundle", old_bundle)
|
||||
@@ -572,7 +568,7 @@ class Item(Document):
|
||||
if len(web_items) <= 1:
|
||||
return
|
||||
|
||||
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
|
||||
old_web_item = next(d.get("name") for d in web_items if d.get("item_code") == old_name)
|
||||
web_item_link = get_link_to_form("Website Item", old_web_item)
|
||||
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
|
||||
|
||||
@@ -586,9 +582,7 @@ class Item(Document):
|
||||
def recalculate_bin_qty(self, new_name):
|
||||
from erpnext.stock.stock_balance import repost_stock
|
||||
|
||||
existing_allow_negative_stock = frappe.db.get_value(
|
||||
"Stock Settings", None, "allow_negative_stock"
|
||||
)
|
||||
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
|
||||
repost_stock_for_warehouses = frappe.get_all(
|
||||
@@ -605,9 +599,7 @@ class Item(Document):
|
||||
for warehouse in repost_stock_for_warehouses:
|
||||
repost_stock(new_name, warehouse)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock
|
||||
)
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
|
||||
|
||||
def update_bom_item_desc(self):
|
||||
if self.is_new():
|
||||
@@ -768,16 +760,14 @@ class Item(Document):
|
||||
return "<br>".join(docnames)
|
||||
|
||||
def table_row(title, body):
|
||||
return """<tr>
|
||||
<td>{0}</td>
|
||||
<td>{1}</td>
|
||||
</tr>""".format(
|
||||
title, body
|
||||
)
|
||||
return f"""<tr>
|
||||
<td>{title}</td>
|
||||
<td>{body}</td>
|
||||
</tr>"""
|
||||
|
||||
rows = ""
|
||||
for docname, attr_list in not_included.items():
|
||||
link = "<a href='/app/Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
|
||||
link = f"<a href='/app/Form/Item/{frappe.bold(_(docname))}'>{frappe.bold(_(docname))}</a>"
|
||||
rows += table_row(link, body(attr_list))
|
||||
|
||||
error_description = _(
|
||||
@@ -785,17 +775,15 @@ class Item(Document):
|
||||
)
|
||||
|
||||
message = """
|
||||
<div>{0}</div><br>
|
||||
<div>{}</div><br>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<td>{1}</td>
|
||||
<td>{2}</td>
|
||||
<td>{}</td>
|
||||
<td>{}</td>
|
||||
</thead>
|
||||
{3}
|
||||
{}
|
||||
</table>
|
||||
""".format(
|
||||
error_description, _("Variant Items"), _("Attributes"), rows
|
||||
)
|
||||
""".format(error_description, _("Variant Items"), _("Attributes"), rows)
|
||||
|
||||
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True)
|
||||
|
||||
@@ -925,7 +913,7 @@ class Item(Document):
|
||||
|
||||
frappe.throw(msg, title=_("Linked with submitted documents"))
|
||||
|
||||
def _get_linked_submitted_documents(self, changed_fields: List[str]) -> Optional[Dict[str, str]]:
|
||||
def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None:
|
||||
linked_doctypes = [
|
||||
"Delivery Note Item",
|
||||
"Sales Invoice Item",
|
||||
@@ -1088,17 +1076,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
||||
last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01"
|
||||
)
|
||||
|
||||
if last_purchase_order and (
|
||||
purchase_order_date >= purchase_receipt_date or not last_purchase_receipt
|
||||
):
|
||||
if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt):
|
||||
# use purchase order
|
||||
|
||||
last_purchase = last_purchase_order[0]
|
||||
purchase_date = purchase_order_date
|
||||
|
||||
elif last_purchase_receipt and (
|
||||
purchase_receipt_date > purchase_order_date or not last_purchase_order
|
||||
):
|
||||
elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order):
|
||||
# use purchase receipt
|
||||
last_purchase = last_purchase_receipt[0]
|
||||
purchase_date = purchase_receipt_date
|
||||
@@ -1305,7 +1289,7 @@ def set_item_tax_from_hsn_code(item):
|
||||
pass
|
||||
|
||||
|
||||
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
|
||||
def validate_item_default_company_links(item_defaults: list[ItemDefault]) -> None:
|
||||
for item_default in item_defaults:
|
||||
for doctype, field in [
|
||||
["Warehouse", "default_warehouse"],
|
||||
|
||||
@@ -315,7 +315,6 @@ class TestItem(FrappeTestCase):
|
||||
self.assertEqual(value, purchase_item_details.get(key))
|
||||
|
||||
def test_item_default_validations(self):
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as ve:
|
||||
make_item(
|
||||
"Bad Item defaults",
|
||||
@@ -469,9 +468,7 @@ class TestItem(FrappeTestCase):
|
||||
|
||||
self.assertFalse(frappe.db.exists("Item", old))
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
self.assertTrue(frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"}))
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})
|
||||
)
|
||||
@@ -715,9 +712,7 @@ class TestItem(FrappeTestCase):
|
||||
|
||||
@change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"})
|
||||
def test_retain_sample(self):
|
||||
item = make_item(
|
||||
"_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1}
|
||||
)
|
||||
item = make_item("_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1})
|
||||
|
||||
self.assertEqual(item.has_batch_no, 1)
|
||||
self.assertEqual(item.retain_sample, 1)
|
||||
@@ -790,7 +785,7 @@ class TestItem(FrappeTestCase):
|
||||
def test_customer_codes_length(self):
|
||||
"""Check if item code with special characters are allowed."""
|
||||
item = make_item(properties={"item_code": "Test Item Code With Special Characters"})
|
||||
for row in range(3):
|
||||
for _row in range(3):
|
||||
item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)})
|
||||
item.save()
|
||||
self.assertTrue(len(item.customer_code) > 140)
|
||||
@@ -831,9 +826,7 @@ class TestItem(FrappeTestCase):
|
||||
make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype")
|
||||
|
||||
item = make_item(properties={"item_name": "Test Item", "description": "Test Description"})
|
||||
data = item_query(
|
||||
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
|
||||
)
|
||||
data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True)
|
||||
self.assertEqual(data[0].name, item.name)
|
||||
self.assertEqual(data[0].item_name, item.item_name)
|
||||
self.assertTrue("description" not in data[0])
|
||||
@@ -841,9 +834,7 @@ class TestItem(FrappeTestCase):
|
||||
make_property_setter(
|
||||
"Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype"
|
||||
)
|
||||
data = item_query(
|
||||
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
|
||||
)
|
||||
data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True)
|
||||
self.assertEqual(data[0].name, item.name)
|
||||
self.assertEqual(data[0].item_name, item.item_name)
|
||||
self.assertEqual(data[0].description, item.description)
|
||||
|
||||
@@ -30,9 +30,7 @@ class ItemAlternative(Document):
|
||||
"allow_alternative_item",
|
||||
]
|
||||
item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1)
|
||||
alternative_item_data = frappe.db.get_value(
|
||||
"Item", self.alternative_item_code, fields, as_dict=1
|
||||
)
|
||||
alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1)
|
||||
|
||||
for field in fields:
|
||||
if item_data.get(field) != alternative_item_data.get(field):
|
||||
@@ -72,14 +70,12 @@ class ItemAlternative(Document):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_alternative_items(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
""" (select alternative_item_code from `tabItem Alternative`
|
||||
f""" (select alternative_item_code from `tabItem Alternative`
|
||||
where item_code = %(item_code)s and alternative_item_code like %(txt)s)
|
||||
union
|
||||
(select item_code from `tabItem Alternative`
|
||||
where alternative_item_code = %(item_code)s and item_code like %(txt)s
|
||||
and two_way = 1) limit {1} offset {0}
|
||||
""".format(
|
||||
start, page_len
|
||||
),
|
||||
and two_way = 1) limit {page_len} offset {start}
|
||||
""",
|
||||
{"item_code": filters.get("item_code"), "txt": "%" + txt + "%"},
|
||||
)
|
||||
|
||||
@@ -54,9 +54,7 @@ class TestItemAlternative(FrappeTestCase):
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
service_items=service_items, supplier_warehouse=supplier_warehouse
|
||||
)
|
||||
sco = get_subcontracting_order(service_items=service_items, supplier_warehouse=supplier_warehouse)
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": "Test Finished Goods - A",
|
||||
@@ -106,9 +104,7 @@ class TestItemAlternative(FrappeTestCase):
|
||||
"reserved_qty_for_sub_contract",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)
|
||||
)
|
||||
self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5))
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
@@ -159,9 +155,7 @@ class TestItemAlternative(FrappeTestCase):
|
||||
"reserved_qty_for_production",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)
|
||||
)
|
||||
self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5))
|
||||
ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5))
|
||||
|
||||
status = False
|
||||
|
||||
@@ -75,7 +75,9 @@ class ItemAttribute(Document):
|
||||
values, abbrs = [], []
|
||||
for d in self.item_attribute_values:
|
||||
if d.attribute_value.lower() in map(str.lower, values):
|
||||
frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title()))
|
||||
frappe.throw(
|
||||
_("Attribute value: {0} must appear only once").format(d.attribute_value.title())
|
||||
)
|
||||
values.append(d.attribute_value)
|
||||
|
||||
if d.abbr.lower() in map(str.lower, abbrs):
|
||||
|
||||
@@ -41,7 +41,9 @@ class ItemManufacturer(Document):
|
||||
# if unchecked and default in Item master, clear it.
|
||||
if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no:
|
||||
frappe.db.set_value(
|
||||
"Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None}
|
||||
"Item",
|
||||
item.name,
|
||||
{"default_item_manufacturer": None, "default_manufacturer_part_no": None},
|
||||
)
|
||||
|
||||
elif self.is_default:
|
||||
|
||||
@@ -40,7 +40,7 @@ class ItemPrice(Document):
|
||||
|
||||
if not price_list_details:
|
||||
link = frappe.utils.get_link_to_form("Price List", self.price_list)
|
||||
frappe.throw("The price list {0} does not exist or is disabled".format(link))
|
||||
frappe.throw(f"The price list {link} does not exist or is disabled")
|
||||
|
||||
self.buying, self.selling, self.currency = price_list_details
|
||||
|
||||
@@ -57,7 +57,6 @@ class ItemPrice(Document):
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def check_duplicates(self):
|
||||
|
||||
item_price = frappe.qb.DocType("Item Price")
|
||||
|
||||
query = (
|
||||
|
||||
@@ -24,7 +24,6 @@ class ItemVariantSettings(Document):
|
||||
"description",
|
||||
"variant_of",
|
||||
"valuation_rate",
|
||||
"description",
|
||||
"barcodes",
|
||||
"has_variants",
|
||||
"attributes",
|
||||
@@ -50,4 +49,6 @@ class ItemVariantSettings(Document):
|
||||
def validate(self):
|
||||
for d in self.fields:
|
||||
if d.field_name in self.invalid_fields_for_copy_fields_in_variants:
|
||||
frappe.throw(_("Cannot set the field <b>{0}</b> for copying in variants").format(d.field_name))
|
||||
frappe.throw(
|
||||
_("Cannot set the field <b>{0}</b> for copying in variants").format(d.field_name)
|
||||
)
|
||||
|
||||
@@ -76,13 +76,13 @@ class LandedCostVoucher(Document):
|
||||
for d in self.get("purchase_receipts"):
|
||||
docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
|
||||
if docstatus != 1:
|
||||
msg = (
|
||||
f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
|
||||
)
|
||||
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
|
||||
frappe.throw(_(msg), title=_("Invalid Document"))
|
||||
|
||||
if d.receipt_document_type == "Purchase Invoice":
|
||||
update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock")
|
||||
update_stock = frappe.db.get_value(
|
||||
d.receipt_document_type, d.receipt_document, "update_stock"
|
||||
)
|
||||
if not update_stock:
|
||||
msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(
|
||||
d.idx, frappe.bold(d.receipt_document)
|
||||
@@ -132,7 +132,8 @@ class LandedCostVoucher(Document):
|
||||
)
|
||||
|
||||
item.applicable_charges = flt(
|
||||
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
|
||||
flt(item.get(based_on_field))
|
||||
* (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
|
||||
item.precision("applicable_charges"),
|
||||
)
|
||||
total_charges += item.applicable_charges
|
||||
@@ -230,7 +231,9 @@ class LandedCostVoucher(Document):
|
||||
for item in self.get("items"):
|
||||
if item.is_fixed_asset:
|
||||
receipt_document_type = (
|
||||
"purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt"
|
||||
"purchase_invoice"
|
||||
if item.receipt_document_type == "Purchase Invoice"
|
||||
else "purchase_receipt"
|
||||
)
|
||||
docs = frappe.db.get_all(
|
||||
"Asset",
|
||||
@@ -249,9 +252,7 @@ class LandedCostVoucher(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} <b>{1}</b> has submitted Assets. Remove Item <b>{2}</b> from table to continue."
|
||||
).format(
|
||||
item.receipt_document_type, item.receipt_document, item.item_code
|
||||
)
|
||||
).format(item.receipt_document_type, item.receipt_document, item.item_code)
|
||||
)
|
||||
|
||||
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
|
||||
@@ -260,10 +261,10 @@ class LandedCostVoucher(Document):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
if serial_nos:
|
||||
frappe.db.sql(
|
||||
"update `tabSerial No` set purchase_rate=%s where name in ({0})".format(
|
||||
"update `tabSerial No` set purchase_rate=%s where name in ({})".format(
|
||||
", ".join(["%s"] * len(serial_nos))
|
||||
),
|
||||
tuple([item.valuation_rate] + serial_nos),
|
||||
tuple([item.valuation_rate, *serial_nos]),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
|
||||
)
|
||||
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
|
||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0)
|
||||
|
||||
# assert after submit
|
||||
@@ -87,7 +85,6 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
self.assertPurchaseReceiptLCVGLEntries(pr)
|
||||
|
||||
def assertPurchaseReceiptLCVGLEntries(self, pr):
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
@@ -170,9 +167,7 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
|
||||
)
|
||||
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
|
||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||
|
||||
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
||||
@@ -229,7 +224,6 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
)
|
||||
|
||||
def test_landed_cost_voucher_against_purchase_invoice(self):
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
update_stock=1,
|
||||
posting_date=frappe.utils.nowdate(),
|
||||
@@ -274,9 +268,7 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
|
||||
)
|
||||
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
|
||||
|
||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||
|
||||
@@ -365,9 +357,7 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
|
||||
new_purchase_rate = serial_no_rate + charges
|
||||
|
||||
serial_no = frappe.db.get_value(
|
||||
"Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1
|
||||
)
|
||||
serial_no = frappe.db.get_value("Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1)
|
||||
|
||||
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
|
||||
|
||||
@@ -392,7 +382,7 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
do_not_save=True,
|
||||
)
|
||||
pr.items[0].cost_center = "Main - TCP1"
|
||||
for x in range(2):
|
||||
for _x in range(2):
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
|
||||
@@ -33,10 +33,10 @@ class MaterialRequest(BuyingController):
|
||||
so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}}
|
||||
for d in self.get("items"):
|
||||
if d.sales_order:
|
||||
if not d.sales_order in so_items:
|
||||
if d.sales_order not in so_items:
|
||||
so_items[d.sales_order] = {d.item_code: flt(d.qty)}
|
||||
else:
|
||||
if not d.item_code in so_items[d.sales_order]:
|
||||
if d.item_code not in so_items[d.sales_order]:
|
||||
so_items[d.sales_order][d.item_code] = flt(d.qty)
|
||||
else:
|
||||
so_items[d.sales_order][d.item_code] += flt(d.qty)
|
||||
@@ -61,13 +61,13 @@ class MaterialRequest(BuyingController):
|
||||
|
||||
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
|
||||
frappe.throw(
|
||||
_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(
|
||||
actual_so_qty - already_indented, item, so_no
|
||||
)
|
||||
_(
|
||||
"Material Request of maximum {0} can be made for Item {1} against Sales Order {2}"
|
||||
).format(actual_so_qty - already_indented, item, so_no)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
super(MaterialRequest, self).validate()
|
||||
super().validate()
|
||||
|
||||
self.validate_schedule_date()
|
||||
self.check_for_on_hold_or_closed_status("Sales Order", "sales_order")
|
||||
@@ -141,12 +141,8 @@ class MaterialRequest(BuyingController):
|
||||
self.set_status(update=True, status="Cancelled")
|
||||
|
||||
def check_modified_date(self):
|
||||
mod_db = frappe.db.sql(
|
||||
"""select modified from `tabMaterial Request` where name = %s""", self.name
|
||||
)
|
||||
date_diff = frappe.db.sql(
|
||||
"""select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified))
|
||||
)
|
||||
mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", self.name)
|
||||
date_diff = frappe.db.sql(f"""select TIMEDIFF('{mod_db[0][0]}', '{cstr(self.modified)}')""")
|
||||
|
||||
if date_diff and date_diff[0][0]:
|
||||
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name))
|
||||
@@ -326,9 +322,7 @@ def update_completed_and_requested_qty(stock_entry, method):
|
||||
|
||||
|
||||
def set_missing_values(source, target_doc):
|
||||
if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(
|
||||
nowdate()
|
||||
):
|
||||
if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()):
|
||||
target_doc.schedule_date = None
|
||||
target_doc.run_method("set_missing_values")
|
||||
target_doc.run_method("calculate_taxes_and_totals")
|
||||
@@ -458,9 +452,7 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non
|
||||
target_doc.schedule_date = None
|
||||
target_doc.set(
|
||||
"items",
|
||||
[
|
||||
d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0
|
||||
],
|
||||
[d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0],
|
||||
)
|
||||
|
||||
set_missing_values(source, target_doc)
|
||||
@@ -512,7 +504,7 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
date = filters.get("transaction_date")[1]
|
||||
conditions += "and mr.transaction_date between '{0}' and '{1}' ".format(date[0], date[1])
|
||||
conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' "
|
||||
|
||||
supplier = filters.get("supplier")
|
||||
supplier_items = get_items_based_on_default_supplier(supplier)
|
||||
@@ -524,18 +516,18 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
|
||||
"""select distinct mr.name, transaction_date,company
|
||||
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
||||
where mr.name = mr_item.parent
|
||||
and mr_item.item_code in ({0})
|
||||
and mr_item.item_code in ({})
|
||||
and mr.material_request_type = 'Purchase'
|
||||
and mr.per_ordered < 99.99
|
||||
and mr.docstatus = 1
|
||||
and mr.status != 'Stopped'
|
||||
and mr.company = %s
|
||||
{1}
|
||||
{}
|
||||
order by mr_item.item_code ASC
|
||||
limit {2} offset {3} """.format(
|
||||
limit {} offset {} """.format(
|
||||
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
|
||||
),
|
||||
tuple(supplier_items) + (filters.get("company"),),
|
||||
(*tuple(supplier_items), filters.get("company")),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -663,7 +655,10 @@ def make_stock_entry(source_name, target_doc=None):
|
||||
"doctype": "Stock Entry",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
"material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]],
|
||||
"material_request_type": [
|
||||
"in",
|
||||
["Material Transfer", "Material Issue", "Customer Provided"],
|
||||
],
|
||||
},
|
||||
},
|
||||
"Material Request Item": {
|
||||
@@ -693,9 +688,7 @@ def raise_work_orders(material_request):
|
||||
mr = frappe.get_doc("Material Request", material_request)
|
||||
errors = []
|
||||
work_orders = []
|
||||
default_wip_warehouse = frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "default_wip_warehouse"
|
||||
)
|
||||
default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
|
||||
|
||||
for d in mr.items:
|
||||
if (d.stock_qty - d.ordered_qty) > 0:
|
||||
|
||||
@@ -744,9 +744,7 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
self.assertEqual(mr.per_ordered, 100)
|
||||
|
||||
def test_customer_provided_parts_mr(self):
|
||||
create_item(
|
||||
"CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
|
||||
)
|
||||
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
||||
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
|
||||
|
||||
mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided")
|
||||
@@ -810,7 +808,7 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
)
|
||||
comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name)
|
||||
|
||||
for company, mr_list in comapnywise_mr_list.items():
|
||||
for company, _mr_list in comapnywise_mr_list.items():
|
||||
emails = get_email_list(company)
|
||||
|
||||
self.assertTrue(comapnywise_users[company] in emails)
|
||||
@@ -828,9 +826,7 @@ def get_in_transit_warehouse(company):
|
||||
}
|
||||
).insert()
|
||||
|
||||
in_transit_warehouse = frappe.db.exists(
|
||||
"Warehouse", {"warehouse_type": "Transit", "company": company}
|
||||
)
|
||||
in_transit_warehouse = frappe.db.exists("Warehouse", {"warehouse_type": "Transit", "company": company})
|
||||
|
||||
if not in_transit_warehouse:
|
||||
in_transit_warehouse = (
|
||||
|
||||
@@ -23,9 +23,7 @@ def make_packing_list(doc):
|
||||
return
|
||||
|
||||
parent_items_price, reset = {}, False
|
||||
set_price_from_children = frappe.db.get_single_value(
|
||||
"Selling Settings", "editable_bundle_item_rates"
|
||||
)
|
||||
set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates")
|
||||
|
||||
stale_packed_items_table = get_indexed_packed_items_table(doc)
|
||||
|
||||
@@ -244,9 +242,7 @@ def get_packed_item_bin_qty(item, warehouse):
|
||||
def get_cancelled_doc_packed_item_details(old_packed_items):
|
||||
prev_doc_packed_items_map = {}
|
||||
for items in old_packed_items:
|
||||
prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append(
|
||||
items.as_dict()
|
||||
)
|
||||
prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append(items.as_dict())
|
||||
return prev_doc_packed_items_map
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
@@ -15,8 +14,8 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
def create_product_bundle(
|
||||
quantities: Optional[List[int]] = None, warehouse: Optional[str] = None
|
||||
) -> Tuple[str, List[str]]:
|
||||
quantities: list[int] | None = None, warehouse: str | None = None
|
||||
) -> tuple[str, list[str]]:
|
||||
"""Get a new product_bundle for use in tests.
|
||||
|
||||
Create 10x required stock if warehouse is specified.
|
||||
@@ -169,9 +168,7 @@ class TestPackedItem(FrappeTestCase):
|
||||
|
||||
# backdated stock entry
|
||||
for item in self.bundle_items:
|
||||
make_stock_entry(
|
||||
item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday
|
||||
)
|
||||
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday)
|
||||
|
||||
# assert correct reposting
|
||||
gles = get_gl_entries(dn.doctype, dn.name)
|
||||
@@ -182,10 +179,11 @@ class TestPackedItem(FrappeTestCase):
|
||||
def assertReturns(self, original, returned):
|
||||
self.assertEqual(len(original), len(returned))
|
||||
|
||||
sort_function = lambda p: (p.parent_item, p.item_code, p.qty)
|
||||
def sort_function(p):
|
||||
return p.parent_item, p.item_code, p.qty
|
||||
|
||||
for sent, returned in zip(
|
||||
sorted(original, key=sort_function), sorted(returned, key=sort_function)
|
||||
sorted(original, key=sort_function), sorted(returned, key=sort_function), strict=False
|
||||
):
|
||||
self.assertEqual(sent.item_code, returned.item_code)
|
||||
self.assertEqual(sent.parent_item, returned.parent_item)
|
||||
|
||||
@@ -11,7 +11,7 @@ from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
class PackingSlip(StatusUpdater):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super(PackingSlip, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.status_updater = [
|
||||
{
|
||||
"target_dt": "Delivery Note Item",
|
||||
@@ -64,9 +64,7 @@ class PackingSlip(StatusUpdater):
|
||||
"""Validate if case nos overlap. If they do, recommend next case no."""
|
||||
|
||||
if cint(self.from_case_no) <= 0:
|
||||
frappe.throw(
|
||||
_("The 'From Package No.' field must neither be empty nor it's value less than 1.")
|
||||
)
|
||||
frappe.throw(_("The 'From Package No.' field must neither be empty nor it's value less than 1."))
|
||||
elif not self.to_case_no:
|
||||
self.to_case_no = self.from_case_no
|
||||
elif cint(self.to_case_no) < cint(self.from_case_no):
|
||||
@@ -189,9 +187,8 @@ def item_details(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select name, item_name, description from `tabItem`
|
||||
where name in ( select item_code FROM `tabDelivery Note Item`
|
||||
where parent= %s)
|
||||
and %s like "%s" %s
|
||||
limit %s offset %s """
|
||||
% ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
|
||||
where parent= {})
|
||||
and {} like "{}" {}
|
||||
limit {} offset {} """.format("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
|
||||
((filters or {}).get("delivery_note"), "%%%s%%" % txt, page_len, start),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import json
|
||||
from collections import OrderedDict, defaultdict
|
||||
from itertools import groupby
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -213,9 +212,7 @@ class PickList(Document):
|
||||
),
|
||||
)
|
||||
|
||||
locations = get_items_with_location_and_quantity(
|
||||
item_doc, self.item_location_map, self.docstatus
|
||||
)
|
||||
locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus)
|
||||
|
||||
item_doc.idx = None
|
||||
item_doc.name = None
|
||||
@@ -269,12 +266,10 @@ class PickList(Document):
|
||||
item_map = OrderedDict()
|
||||
for item in locations:
|
||||
if not item.item_code:
|
||||
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
|
||||
frappe.throw(f"Row #{item.idx}: Item Code is Mandatory")
|
||||
if not cint(
|
||||
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
|
||||
) and not frappe.db.exists(
|
||||
"Product Bundle", {"new_item_code": item.item_code, "disabled": 0}
|
||||
):
|
||||
) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}):
|
||||
continue
|
||||
item_code = item.item_code
|
||||
reference = item.sales_order_item or item.material_request_item
|
||||
@@ -395,7 +390,7 @@ class PickList(Document):
|
||||
|
||||
return picked_items
|
||||
|
||||
def _get_product_bundles(self) -> Dict[str, str]:
|
||||
def _get_product_bundles(self) -> dict[str, str]:
|
||||
# Dict[so_item_row: item_code]
|
||||
product_bundles = {}
|
||||
for item in self.locations:
|
||||
@@ -408,13 +403,11 @@ class PickList(Document):
|
||||
)
|
||||
return product_bundles
|
||||
|
||||
def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
|
||||
def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]:
|
||||
# bundle_item_code: Dict[component, qty]
|
||||
product_bundle_qty_map = {}
|
||||
for bundle_item_code in bundles:
|
||||
bundle = frappe.get_last_doc(
|
||||
"Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}
|
||||
)
|
||||
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0})
|
||||
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
|
||||
return product_bundle_qty_map
|
||||
|
||||
@@ -440,7 +433,7 @@ def update_pick_list_status(pick_list):
|
||||
doc.run_method("update_status")
|
||||
|
||||
|
||||
def get_picked_items_qty(items) -> List[Dict]:
|
||||
def get_picked_items_qty(items) -> list[dict]:
|
||||
pi_item = frappe.qb.DocType("Pick List Item")
|
||||
return (
|
||||
frappe.qb.from_(pi_item)
|
||||
@@ -470,17 +463,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
||||
locations = []
|
||||
|
||||
# if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock.
|
||||
remaining_stock_qty = (
|
||||
item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
|
||||
)
|
||||
remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
|
||||
|
||||
while flt(remaining_stock_qty) > 0 and available_locations:
|
||||
item_location = available_locations.pop(0)
|
||||
item_location = frappe._dict(item_location)
|
||||
|
||||
stock_qty = (
|
||||
remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
|
||||
)
|
||||
stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
|
||||
qty = stock_qty / (item_doc.conversion_factor or 1)
|
||||
|
||||
uom_must_be_whole_number = frappe.get_cached_value("UOM", item_doc.uom, "must_be_whole_number")
|
||||
@@ -515,7 +504,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
||||
if item_location.serial_no:
|
||||
# set remaining serial numbers
|
||||
item_location.serial_no = item_location.serial_no[-int(qty_diff) :]
|
||||
available_locations = [item_location] + available_locations
|
||||
available_locations = [item_location, *available_locations]
|
||||
|
||||
# update available locations for the item
|
||||
item_location_map[item_doc.item_code] = available_locations
|
||||
@@ -830,8 +819,7 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
"name": "so_detail",
|
||||
"parent": "against_sales_order",
|
||||
},
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
|
||||
}
|
||||
|
||||
for customer in sales_dict:
|
||||
@@ -852,7 +840,6 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
|
||||
|
||||
def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
|
||||
for location in pick_list.locations:
|
||||
if location.sales_order != sales_order or location.product_bundle_item:
|
||||
continue
|
||||
@@ -883,9 +870,7 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
|
||||
|
||||
def add_product_bundles_to_delivery_note(
|
||||
pick_list: "PickList", delivery_note, item_mapper
|
||||
) -> None:
|
||||
def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None:
|
||||
"""Add product bundles found in pick list to delivery note.
|
||||
|
||||
When mapping pick list items, the bundle item itself isn't part of the
|
||||
@@ -963,7 +948,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
|
||||
& (wo.qty > wo.material_transferred_for_manufacturing)
|
||||
& (wo.docstatus == 1)
|
||||
& (wo.company == filters.get("company"))
|
||||
& (wo.name.like("%{0}%".format(txt)))
|
||||
& (wo.name.like(f"%{txt}%"))
|
||||
)
|
||||
.orderby(Case().when(Locate(txt, wo.name) > 0, Locate(txt, wo.name)).else_(99999))
|
||||
.orderby(wo.name)
|
||||
@@ -1030,9 +1015,7 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry):
|
||||
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
|
||||
stock_entry.fg_completed_qty = pick_list.for_qty
|
||||
if work_order.bom_no:
|
||||
stock_entry.inspection_required = frappe.db.get_value(
|
||||
"BOM", work_order.bom_no, "inspection_required"
|
||||
)
|
||||
stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")
|
||||
|
||||
is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group")
|
||||
if not (is_wip_warehouse_group and work_order.skip_transfer):
|
||||
|
||||
@@ -139,7 +139,6 @@ class TestPickList(FrappeTestCase):
|
||||
self.assertEqual(pick_list.locations[1].qty, 10)
|
||||
|
||||
def test_pick_list_shows_serial_no_for_serialized_item(self):
|
||||
|
||||
stock_reconciliation = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Reconciliation",
|
||||
@@ -274,7 +273,6 @@ class TestPickList(FrappeTestCase):
|
||||
pr2.cancel()
|
||||
|
||||
def test_pick_list_for_items_from_multiple_sales_orders(self):
|
||||
|
||||
item_code = make_item().name
|
||||
try:
|
||||
frappe.get_doc(
|
||||
@@ -418,9 +416,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
|
||||
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
|
||||
self.assertEqual(
|
||||
sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor
|
||||
)
|
||||
self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
@@ -469,7 +465,7 @@ class TestPickList(FrappeTestCase):
|
||||
_dict(item_code="A", warehouse="X", qty=8, picked_qty=3),
|
||||
_dict(item_code="B", warehouse="Y", qty=6, picked_qty=4),
|
||||
]
|
||||
for expected_item, created_item in zip(expected_items, pl.locations):
|
||||
for expected_item, created_item in zip(expected_items, pl.locations, strict=False):
|
||||
_compare_dicts(expected_item, created_item)
|
||||
|
||||
def test_multiple_dn_creation(self):
|
||||
@@ -579,9 +575,7 @@ class TestPickList(FrappeTestCase):
|
||||
pick_list_1.set_item_locations()
|
||||
pick_list_1.submit()
|
||||
create_delivery_note(pick_list_1.name)
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}
|
||||
):
|
||||
for dn in frappe.get_all("Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
if dn_item.item_code == "_Test Item":
|
||||
self.assertEqual(dn_item.qty, 1)
|
||||
@@ -609,7 +603,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
quantities = [5, 2]
|
||||
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
|
||||
bundle_items = dict(zip(components, quantities))
|
||||
bundle_items = dict(zip(components, quantities, strict=False))
|
||||
|
||||
so = make_sales_order(item_code=bundle, qty=3, rate=42)
|
||||
|
||||
@@ -715,7 +709,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
for item in items:
|
||||
for warehouse in warehouses:
|
||||
se = make_stock_entry(
|
||||
make_stock_entry(
|
||||
item=item.get("item_code"),
|
||||
to_warehouse=warehouse,
|
||||
qty=5,
|
||||
|
||||
@@ -23,7 +23,7 @@ form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
class PurchaseReceipt(BuyingController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PurchaseReceipt, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.status_updater = [
|
||||
{
|
||||
"target_dt": "Purchase Order Item",
|
||||
@@ -116,7 +116,7 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
def validate(self):
|
||||
self.validate_posting_time()
|
||||
super(PurchaseReceipt, self).validate()
|
||||
super().validate()
|
||||
|
||||
if self._action == "submit":
|
||||
self.make_batches("warehouse")
|
||||
@@ -148,15 +148,15 @@ class PurchaseReceipt(BuyingController):
|
||||
# Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account
|
||||
self.get_company_default("asset_received_but_not_billed")
|
||||
get_asset_account(
|
||||
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=item.asset_category,
|
||||
company=self.company,
|
||||
)
|
||||
break
|
||||
|
||||
def validate_provisional_expense_account(self):
|
||||
provisional_accounting_for_non_stock_items = cint(
|
||||
frappe.db.get_value(
|
||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||
)
|
||||
frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
|
||||
)
|
||||
|
||||
if not provisional_accounting_for_non_stock_items:
|
||||
@@ -168,7 +168,7 @@ class PurchaseReceipt(BuyingController):
|
||||
item.provisional_expense_account = default_provisional_account
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super(PurchaseReceipt, self).validate_with_previous_doc(
|
||||
super().validate_with_previous_doc(
|
||||
{
|
||||
"Purchase Order": {
|
||||
"ref_dn_field": "purchase_order",
|
||||
@@ -229,24 +229,20 @@ class PurchaseReceipt(BuyingController):
|
||||
return qty and flt(qty[0][0]) or 0.0
|
||||
|
||||
def get_po_qty_and_warehouse(self, po_detail):
|
||||
po_qty, po_warehouse = frappe.db.get_value(
|
||||
"Purchase Order Item", po_detail, ["qty", "warehouse"]
|
||||
)
|
||||
po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, ["qty", "warehouse"])
|
||||
return po_qty, po_warehouse
|
||||
|
||||
# Check for Closed status
|
||||
def check_on_hold_or_closed_status(self):
|
||||
check_list = []
|
||||
for d in self.get("items"):
|
||||
if (
|
||||
d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list
|
||||
):
|
||||
if d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list:
|
||||
check_list.append(d.purchase_order)
|
||||
check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
|
||||
|
||||
# on submit
|
||||
def on_submit(self):
|
||||
super(PurchaseReceipt, self).on_submit()
|
||||
super().on_submit()
|
||||
|
||||
# Check for Approving Authority
|
||||
frappe.get_doc("Authorization Control").validate_approving_authority(
|
||||
@@ -283,7 +279,7 @@ class PurchaseReceipt(BuyingController):
|
||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0]))
|
||||
|
||||
def on_cancel(self):
|
||||
super(PurchaseReceipt, self).on_cancel()
|
||||
super().on_cancel()
|
||||
|
||||
self.check_on_hold_or_closed_status()
|
||||
# Check if Purchase Invoice has been submitted against current Purchase Order
|
||||
@@ -325,9 +321,7 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
provisional_accounting_for_non_stock_items = cint(
|
||||
frappe.db.get_value(
|
||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||
)
|
||||
frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
|
||||
)
|
||||
|
||||
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
||||
@@ -396,7 +390,6 @@ class PurchaseReceipt(BuyingController):
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_invoice]
|
||||
and item.net_rate == net_rate_map[item.purchase_invoice_item]
|
||||
):
|
||||
|
||||
discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
|
||||
exchange_rate_map[item.purchase_invoice] - self.conversion_rate
|
||||
)
|
||||
@@ -578,10 +571,12 @@ class PurchaseReceipt(BuyingController):
|
||||
elif warehouse_account.get(d.warehouse):
|
||||
stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
|
||||
stock_asset_account_name = warehouse_account[d.warehouse]["account"]
|
||||
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
|
||||
supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get(
|
||||
"account_currency"
|
||||
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
|
||||
"account"
|
||||
)
|
||||
supplier_warehouse_account_currency = warehouse_account.get(
|
||||
self.supplier_warehouse, {}
|
||||
).get("account_currency")
|
||||
|
||||
# If PR is sub-contracted and fg item rate is zero
|
||||
# in that case if account for source and target warehouse are same,
|
||||
@@ -988,9 +983,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
frappe.throw(_("All items have already been Invoiced/Returned"))
|
||||
|
||||
doc = frappe.get_doc(target)
|
||||
doc.payment_terms_template = get_payment_terms_template(
|
||||
source.supplier, "Supplier", source.company
|
||||
)
|
||||
doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company)
|
||||
doc.run_method("onload")
|
||||
doc.run_method("set_missing_values")
|
||||
|
||||
@@ -1002,9 +995,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty, returned_qty = get_pending_qty(source_doc)
|
||||
if frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
):
|
||||
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
|
||||
target_doc.rejected_qty = 0
|
||||
target_doc.stock_qty = flt(target_doc.qty) * flt(
|
||||
target_doc.conversion_factor, target_doc.precision("conversion_factor")
|
||||
@@ -1013,9 +1004,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
|
||||
def get_pending_qty(item_row):
|
||||
qty = item_row.qty
|
||||
if frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
):
|
||||
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
|
||||
qty = item_row.received_qty
|
||||
pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
|
||||
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
|
||||
@@ -1193,15 +1182,11 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
if total_item_cost > 0:
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["amount"] += (
|
||||
account.amount * item.get(based_on_field) / total_item_cost
|
||||
)
|
||||
]["amount"] += account.amount * item.get(based_on_field) / total_item_cost
|
||||
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["base_amount"] += (
|
||||
account.base_amount * item.get(based_on_field) / total_item_cost
|
||||
)
|
||||
]["base_amount"] += account.base_amount * item.get(based_on_field) / total_item_cost
|
||||
else:
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
|
||||
@@ -11,7 +11,7 @@ from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.delete()
|
||||
|
||||
def test_reverse_purchase_receipt_sle(self):
|
||||
|
||||
pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200")
|
||||
|
||||
sl_entry = frappe.db.get_all(
|
||||
@@ -124,9 +123,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pi.delete() # draft PI
|
||||
pr.cancel()
|
||||
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier)
|
||||
frappe.get_doc(
|
||||
"Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
|
||||
).delete()
|
||||
frappe.get_doc("Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice").delete()
|
||||
|
||||
def test_purchase_receipt_no_gl_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
@@ -273,7 +270,8 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
self.assertEqual(len(rejected_serial_nos), 2)
|
||||
for serial_no in rejected_serial_nos:
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse
|
||||
frappe.db.get_value("Serial No", serial_no, "warehouse"),
|
||||
pr.get("items")[0].rejected_warehouse,
|
||||
)
|
||||
|
||||
pr.cancel()
|
||||
@@ -613,7 +611,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
item_code = "Test Manual Created Serial No"
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
item = make_item(item_code, dict(has_serial_no=1))
|
||||
make_item(item_code, dict(has_serial_no=1))
|
||||
|
||||
serial_no = "12903812901"
|
||||
pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
|
||||
@@ -759,7 +757,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
"Stock Received But Not Billed - TCP1": {"cost_center": cost_center},
|
||||
stock_in_hand_account: {"cost_center": cost_center},
|
||||
}
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
pr.cancel()
|
||||
@@ -784,7 +782,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
"Stock Received But Not Billed - TCP1": {"cost_center": cost_center},
|
||||
stock_in_hand_account: {"cost_center": cost_center},
|
||||
}
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
pr.cancel()
|
||||
@@ -1045,9 +1043,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.submit()
|
||||
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value(
|
||||
"Company", pr.company, "exchange_gain_loss_account"
|
||||
)
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", pr.company, "exchange_gain_loss_account")
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value(
|
||||
@@ -1104,9 +1100,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
account = "Stock Received But Not Billed - TCP1"
|
||||
|
||||
make_item(item_code)
|
||||
se = make_stock_entry(
|
||||
item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0
|
||||
)
|
||||
se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0)
|
||||
se.items[0].allow_zero_valuation_rate = 1
|
||||
se.save()
|
||||
se.submit()
|
||||
@@ -1221,9 +1215,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
|
||||
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
|
||||
rejected_warehouse = create_warehouse(
|
||||
"_Test Rejected Internal To Warehouse New", company=company
|
||||
)
|
||||
rejected_warehouse = create_warehouse("_Test Rejected Internal To Warehouse New", company=company)
|
||||
item_doc = make_item(
|
||||
"Test Internal Transfer Item DS",
|
||||
{
|
||||
@@ -1616,7 +1608,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
)
|
||||
|
||||
# Step 4: Create Internal Purchase Receipt
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
@@ -1766,7 +1757,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
)
|
||||
|
||||
# Step 4: Create Internal Purchase Receipt
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
@@ -2072,9 +2062,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
)
|
||||
|
||||
# Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched])
|
||||
make_stock_entry(
|
||||
purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100
|
||||
)
|
||||
make_stock_entry(purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100)
|
||||
|
||||
# Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched])
|
||||
return_pr = make_purchase_receipt(
|
||||
@@ -2146,9 +2134,9 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
gl_entries = get_gl_entries(pr_return.doctype, pr_return.name)
|
||||
|
||||
# Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate
|
||||
average_rate = (
|
||||
(se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate)
|
||||
) / (se.items[0].qty + pr.items[0].qty)
|
||||
average_rate = ((se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate)) / (
|
||||
se.items[0].qty + pr.items[0].qty
|
||||
)
|
||||
expected_stock_value_difference = pr_return.items[0].qty * average_rate
|
||||
self.assertEqual(
|
||||
flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2)
|
||||
@@ -2243,7 +2231,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
for i in range(9):
|
||||
for _i in range(9):
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
@@ -2283,7 +2271,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
for i in range(9):
|
||||
for _i in range(9):
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
@@ -2370,7 +2358,6 @@ def get_gl_entries(voucher_type, voucher_no):
|
||||
|
||||
|
||||
def get_taxes(**args):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
return [
|
||||
@@ -2490,15 +2477,14 @@ def make_purchase_receipt(**args):
|
||||
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC"
|
||||
if rejected_qty != 0
|
||||
else "",
|
||||
"rate": args.rate if args.rate != None else 50,
|
||||
"rate": args.rate if args.rate is not None else 50,
|
||||
"conversion_factor": args.conversion_factor or 1.0,
|
||||
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no,
|
||||
"stock_uom": args.stock_uom or "_Test UOM",
|
||||
"uom": uom,
|
||||
"cost_center": args.cost_center
|
||||
or frappe.get_cached_value("Company", pr.company, "cost_center"),
|
||||
"cost_center": args.cost_center or frappe.get_cached_value("Company", pr.company, "cost_center"),
|
||||
"asset_location": args.location or "Test Location",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -109,9 +109,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
|
||||
updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table)
|
||||
continue
|
||||
|
||||
at_capacity, rules = get_ordered_putaway_rules(
|
||||
item_code, company, source_warehouse=source_warehouse
|
||||
)
|
||||
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
|
||||
|
||||
if not rules:
|
||||
warehouse = source_warehouse or item.get("warehouse")
|
||||
@@ -204,7 +202,7 @@ def _items_changed(old, new, doctype: str) -> bool:
|
||||
new_sorted = sorted(new, key=sort_key)
|
||||
|
||||
# Once sorted by all relevant keys both tables should align if they are same.
|
||||
for old_item, new_item in zip(old_sorted, new_sorted):
|
||||
for old_item, new_item in zip(old_sorted, new_sorted, strict=False):
|
||||
for key in compare_keys:
|
||||
if old_item.get(key) != new_item.get(key):
|
||||
return True
|
||||
@@ -253,9 +251,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
|
||||
|
||||
if item.doctype == "Stock Entry Detail":
|
||||
new_updated_table_row.t_warehouse = warehouse
|
||||
new_updated_table_row.transfer_qty = flt(to_allocate) * flt(
|
||||
new_updated_table_row.conversion_factor
|
||||
)
|
||||
new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
|
||||
else:
|
||||
new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
|
||||
new_updated_table_row.warehouse = warehouse
|
||||
@@ -277,24 +273,20 @@ def show_unassigned_items_message(items_not_accomodated):
|
||||
|
||||
for entry in items_not_accomodated:
|
||||
item_link = frappe.utils.get_link_to_form("Item", entry[0])
|
||||
formatted_item_rows += """
|
||||
<td>{0}</td>
|
||||
<td>{1}</td>
|
||||
</tr>""".format(
|
||||
item_link, frappe.bold(entry[1])
|
||||
)
|
||||
formatted_item_rows += f"""
|
||||
<td>{item_link}</td>
|
||||
<td>{frappe.bold(entry[1])}</td>
|
||||
</tr>"""
|
||||
|
||||
msg += """
|
||||
<table class="table">
|
||||
<thead>
|
||||
<td>{0}</td>
|
||||
<td>{1}</td>
|
||||
<td>{}</td>
|
||||
<td>{}</td>
|
||||
</thead>
|
||||
{2}
|
||||
{}
|
||||
</table>
|
||||
""".format(
|
||||
_("Item"), _("Unassigned Qty"), formatted_item_rows
|
||||
)
|
||||
""".format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
|
||||
|
||||
frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
|
||||
|
||||
|
||||
@@ -46,9 +46,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
def test_putaway_rules_priority(self):
|
||||
"""Test if rule is applied by priority, irrespective of free space."""
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg")
|
||||
rule_2 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2
|
||||
)
|
||||
@@ -69,17 +67,11 @@ class TestPutawayRule(FrappeTestCase):
|
||||
def test_putaway_rules_with_same_priority(self):
|
||||
"""Test if rule with more free space is applied,
|
||||
among two rules with same priority and capacity."""
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg"
|
||||
)
|
||||
rule_2 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg")
|
||||
rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg")
|
||||
|
||||
# out of 500 kg capacity, occupy 100 kg in warehouse_1
|
||||
stock_receipt = make_stock_entry(
|
||||
item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50
|
||||
)
|
||||
stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50)
|
||||
|
||||
pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1)
|
||||
self.assertEqual(len(pr.items), 2)
|
||||
@@ -97,12 +89,8 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
def test_putaway_rules_with_insufficient_capacity(self):
|
||||
"""Test if qty exceeding capacity, is handled."""
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg"
|
||||
)
|
||||
rule_2 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg")
|
||||
rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg")
|
||||
|
||||
pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1)
|
||||
self.assertEqual(len(pr.items), 2)
|
||||
@@ -123,19 +111,13 @@ class TestPutawayRule(FrappeTestCase):
|
||||
item.append("uoms", {"uom": "Bag", "conversion_factor": 1000})
|
||||
item.save()
|
||||
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag")
|
||||
self.assertEqual(rule_1.stock_capacity, 3000)
|
||||
rule_2 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag"
|
||||
)
|
||||
rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag")
|
||||
self.assertEqual(rule_2.stock_capacity, 4000)
|
||||
|
||||
# populate 'Rack 1' with 1 Bag, making the free space 2 Bags
|
||||
stock_receipt = make_stock_entry(
|
||||
item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50
|
||||
)
|
||||
stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Rice",
|
||||
@@ -167,9 +149,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1)
|
||||
|
||||
# Putaway Rule in different UOM
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag")
|
||||
self.assertEqual(rule_1.stock_capacity, 1000)
|
||||
# Putaway Rule in Stock UOM
|
||||
rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500)
|
||||
@@ -199,9 +179,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
def test_putaway_rules_with_reoccurring_item(self):
|
||||
"""Test rules on same item entered multiple times with different rate."""
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg")
|
||||
# total capacity is 200 Kg
|
||||
|
||||
pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, do_not_submit=1)
|
||||
@@ -237,9 +215,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
def test_validate_over_receipt_in_warehouse(self):
|
||||
"""Test if overreceipt is blocked in the presence of putaway rules."""
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg")
|
||||
|
||||
pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1)
|
||||
self.assertEqual(len(pr.items), 1)
|
||||
@@ -291,9 +267,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self):
|
||||
"""Test if reoccuring item is correctly considered."""
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg"
|
||||
)
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg")
|
||||
rule_2 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_2, capacity=600, uom="Kg", priority=2
|
||||
)
|
||||
@@ -428,9 +402,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
rule_1 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg"
|
||||
) # more capacity
|
||||
rule_2 = create_putaway_rule(
|
||||
item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg"
|
||||
)
|
||||
rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg")
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code="_Rice",
|
||||
@@ -480,9 +452,7 @@ def create_putaway_rule(**args):
|
||||
putaway.capacity = args.capacity or 1
|
||||
putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom")
|
||||
putaway.uom = args.uom or putaway.stock_uom
|
||||
putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)[
|
||||
"conversion_factor"
|
||||
]
|
||||
putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)["conversion_factor"]
|
||||
|
||||
if not args.do_not_save:
|
||||
putaway.save()
|
||||
|
||||
@@ -82,13 +82,11 @@ class QualityInspection(Document):
|
||||
if self.reference_type == "Job Card":
|
||||
if self.reference_name:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tab{doctype}`
|
||||
f"""
|
||||
UPDATE `tab{self.reference_type}`
|
||||
SET quality_inspection = %s, modified = %s
|
||||
WHERE name = %s and production_item = %s
|
||||
""".format(
|
||||
doctype=self.reference_type
|
||||
),
|
||||
""",
|
||||
(quality_inspection, self.modified, self.reference_name, self.item_code),
|
||||
)
|
||||
|
||||
@@ -110,9 +108,9 @@ class QualityInspection(Document):
|
||||
args.append(self.name)
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
UPDATE
|
||||
`tab{child_doc}` t1, `tab{parent_doc}` t2
|
||||
`tab{doctype}` t1, `tab{self.reference_type}` t2
|
||||
SET
|
||||
t1.quality_inspection = %s, t2.modified = %s
|
||||
WHERE
|
||||
@@ -120,9 +118,7 @@ class QualityInspection(Document):
|
||||
and t1.item_code = %s
|
||||
and t1.parent = t2.name
|
||||
{conditions}
|
||||
""".format(
|
||||
parent_doc=self.reference_type, child_doc=doctype, conditions=conditions
|
||||
),
|
||||
""",
|
||||
args,
|
||||
)
|
||||
|
||||
@@ -180,9 +176,9 @@ class QualityInspection(Document):
|
||||
except NameError as e:
|
||||
field = frappe.bold(e.args[0].split()[1])
|
||||
frappe.throw(
|
||||
_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.").format(
|
||||
reading.idx, field
|
||||
),
|
||||
_(
|
||||
"Row #{0}: {1} is not a valid reading field. Please refer to the field description."
|
||||
).format(reading.idx, field),
|
||||
title=_("Invalid Formula"),
|
||||
)
|
||||
except Exception:
|
||||
@@ -251,40 +247,26 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
qi_condition = ""
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT item_code
|
||||
FROM `tab{doc}`
|
||||
FROM `tab{from_doctype}`
|
||||
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY item_code limit {page_len} offset {start}
|
||||
""".format(
|
||||
doc=from_doctype,
|
||||
cond=cond,
|
||||
mcond=mcond,
|
||||
start=cint(start),
|
||||
page_len=cint(page_len),
|
||||
qi_condition=qi_condition,
|
||||
),
|
||||
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
|
||||
""",
|
||||
{"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
elif filters.get("reference_name"):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT production_item
|
||||
FROM `tab{doc}`
|
||||
FROM `tab{from_doctype}`
|
||||
WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY production_item
|
||||
limit {page_len} offset {start}
|
||||
""".format(
|
||||
doc=from_doctype,
|
||||
cond=cond,
|
||||
mcond=mcond,
|
||||
start=cint(start),
|
||||
page_len=cint(page_len),
|
||||
qi_condition=qi_condition,
|
||||
),
|
||||
limit {cint(page_len)} offset {cint(start)}
|
||||
""",
|
||||
{"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
@@ -159,9 +159,7 @@ class TestQualityInspection(FrappeTestCase):
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
readings = [
|
||||
{"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"}
|
||||
]
|
||||
readings = [{"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"}]
|
||||
|
||||
qa = create_quality_inspection(
|
||||
reference_type="Stock Entry", reference_name=se.name, readings=readings, status="Rejected"
|
||||
|
||||
@@ -17,17 +17,13 @@ class QuickStockBalance(Document):
|
||||
def get_stock_item_details(warehouse, date, item=None, barcode=None):
|
||||
out = {}
|
||||
if barcode:
|
||||
out["item"] = frappe.db.get_value(
|
||||
"Item Barcode", filters={"barcode": barcode}, fieldname=["parent"]
|
||||
)
|
||||
out["item"] = frappe.db.get_value("Item Barcode", filters={"barcode": barcode}, fieldname=["parent"])
|
||||
if not out["item"]:
|
||||
frappe.throw(_("Invalid Barcode. There is no Item attached to this barcode."))
|
||||
else:
|
||||
out["item"] = item
|
||||
|
||||
barcodes = frappe.db.get_values(
|
||||
"Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"]
|
||||
)
|
||||
barcodes = frappe.db.get_values("Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"])
|
||||
|
||||
out["barcodes"] = [x[0] for x in barcodes]
|
||||
out["qty"] = get_stock_balance(out["item"], warehouse, date)
|
||||
|
||||
@@ -117,15 +117,12 @@ class RepostItemValuation(Document):
|
||||
if not acc_settings.acc_frozen_upto:
|
||||
return
|
||||
if getdate(self.posting_date) <= getdate(acc_settings.acc_frozen_upto):
|
||||
if (
|
||||
if acc_settings.frozen_accounts_modifier and frappe.session.user in get_users_with_role(
|
||||
acc_settings.frozen_accounts_modifier
|
||||
and frappe.session.user in get_users_with_role(acc_settings.frozen_accounts_modifier)
|
||||
):
|
||||
frappe.msgprint(_("Caution: This might alter frozen accounts."))
|
||||
return
|
||||
frappe.throw(
|
||||
_("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto)
|
||||
)
|
||||
frappe.throw(_("You cannot repost item valuation before {}").format(acc_settings.acc_frozen_upto))
|
||||
|
||||
def reset_field_values(self):
|
||||
if self.based_on == "Transaction":
|
||||
|
||||
@@ -179,7 +179,6 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
riv3.set_status("Skipped")
|
||||
|
||||
def test_stock_freeze_validation(self):
|
||||
|
||||
today = nowdate()
|
||||
|
||||
riv = frappe.get_doc(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
|
||||
import json
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _
|
||||
@@ -66,7 +65,7 @@ class SerialNoDuplicateError(ValidationError):
|
||||
|
||||
class SerialNo(StockController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SerialNo, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.via_stock_ledger = False
|
||||
|
||||
def validate(self):
|
||||
@@ -123,9 +122,7 @@ class SerialNo(StockController):
|
||||
"""
|
||||
item = frappe.get_cached_doc("Item", self.item_code)
|
||||
if item.has_serial_no != 1:
|
||||
frappe.throw(
|
||||
_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)
|
||||
)
|
||||
frappe.throw(_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code))
|
||||
|
||||
self.item_group = item.item_group
|
||||
self.description = item.description
|
||||
@@ -301,7 +298,8 @@ def validate_serial_no(sle, item_det):
|
||||
|
||||
if len(serial_nos) != len(set(serial_nos)):
|
||||
frappe.throw(
|
||||
_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError
|
||||
_("Duplicate Serial No entered for Item {0}").format(sle.item_code),
|
||||
SerialNoDuplicateError,
|
||||
)
|
||||
|
||||
allow_existing_serial_no = cint(
|
||||
@@ -337,7 +335,9 @@ def validate_serial_no(sle, item_det):
|
||||
if sr.item_code != sle.item_code:
|
||||
if not allow_serial_nos_with_different_item(serial_no, sle):
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code),
|
||||
_("Serial No {0} does not belong to Item {1}").format(
|
||||
serial_no, sle.item_code
|
||||
),
|
||||
SerialNoItemError,
|
||||
)
|
||||
|
||||
@@ -362,7 +362,9 @@ def validate_serial_no(sle, item_det):
|
||||
frappe.throw(_(msg), SerialNoDuplicateError)
|
||||
|
||||
if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
|
||||
doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
|
||||
doc_name = frappe.bold(
|
||||
get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)
|
||||
)
|
||||
frappe.throw(
|
||||
_("Serial No {0} has already been received in the {1} #{2}").format(
|
||||
frappe.bold(serial_no), sr.purchase_document_type, doc_name
|
||||
@@ -375,25 +377,32 @@ def validate_serial_no(sle, item_det):
|
||||
and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"]
|
||||
and sle.voucher_type == sr.delivery_document_type
|
||||
):
|
||||
return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against")
|
||||
return_against = frappe.db.get_value(
|
||||
sle.voucher_type, sle.voucher_no, "return_against"
|
||||
)
|
||||
if return_against and return_against != sr.delivery_document_no:
|
||||
frappe.throw(_("Serial no {0} has been already returned").format(sr.name))
|
||||
|
||||
if cint(sle.actual_qty) < 0:
|
||||
if sr.warehouse != sle.warehouse:
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse),
|
||||
_("Serial No {0} does not belong to Warehouse {1}").format(
|
||||
serial_no, sle.warehouse
|
||||
),
|
||||
SerialNoWarehouseError,
|
||||
)
|
||||
|
||||
if not sr.purchase_document_no:
|
||||
frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
|
||||
frappe.throw(
|
||||
_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError
|
||||
)
|
||||
|
||||
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||
|
||||
if sr.batch_no and sr.batch_no != sle.batch_no:
|
||||
frappe.throw(
|
||||
_("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no),
|
||||
_("Serial No {0} does not belong to Batch {1}").format(
|
||||
serial_no, sle.batch_no
|
||||
),
|
||||
SerialNoBatchError,
|
||||
)
|
||||
|
||||
@@ -408,7 +417,11 @@ def validate_serial_no(sle, item_det):
|
||||
if sle.voucher_type == "Sales Invoice":
|
||||
if not frappe.db.exists(
|
||||
"Sales Invoice Item",
|
||||
{"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order},
|
||||
{
|
||||
"parent": sle.voucher_no,
|
||||
"item_code": sle.item_code,
|
||||
"sales_order": sr.sales_order,
|
||||
},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -431,7 +444,11 @@ def validate_serial_no(sle, item_det):
|
||||
)
|
||||
if not invoice or frappe.db.exists(
|
||||
"Sales Invoice Item",
|
||||
{"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order},
|
||||
{
|
||||
"parent": invoice,
|
||||
"item_code": sle.item_code,
|
||||
"sales_order": sr.sales_order,
|
||||
},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -467,7 +484,9 @@ def validate_serial_no(sle, item_det):
|
||||
{"parent": sales_invoice, "item_code": sle.item_code},
|
||||
"sales_order",
|
||||
)
|
||||
if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
|
||||
if sales_order and get_reserved_qty_for_so(
|
||||
sales_order, sle.item_code
|
||||
):
|
||||
validate_so_serial_no(sr, sales_order)
|
||||
elif cint(sle.actual_qty) < 0:
|
||||
# transfer out
|
||||
@@ -483,9 +502,7 @@ def validate_serial_no(sle, item_det):
|
||||
|
||||
|
||||
def check_serial_no_validity_on_cancel(serial_no, sle):
|
||||
sr = frappe.db.get_value(
|
||||
"Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1
|
||||
)
|
||||
sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1)
|
||||
sr_link = frappe.utils.get_link_to_form("Serial No", serial_no)
|
||||
doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no)
|
||||
actual_qty = cint(sle.actual_qty)
|
||||
@@ -538,9 +555,7 @@ def validate_so_serial_no(sr, sales_order):
|
||||
|
||||
|
||||
def has_serial_no_exists(sn, sle):
|
||||
if (
|
||||
sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation"
|
||||
):
|
||||
if sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation":
|
||||
return True
|
||||
|
||||
if sn.company != sle.company:
|
||||
@@ -584,7 +599,7 @@ def update_serial_nos(sle, item_det):
|
||||
|
||||
def get_auto_serial_nos(serial_no_series, qty):
|
||||
serial_nos = []
|
||||
for i in range(cint(qty)):
|
||||
for _i in range(cint(qty)):
|
||||
serial_nos.append(get_new_serial_number(serial_no_series))
|
||||
|
||||
return "\n".join(serial_nos)
|
||||
@@ -634,13 +649,11 @@ def auto_make_serial_nos(args):
|
||||
|
||||
def get_items_html(serial_nos, item_code):
|
||||
body = ", ".join(serial_nos)
|
||||
return """<details><summary>
|
||||
<b>{0}:</b> {1} Serial Numbers <span class="caret"></span>
|
||||
return f"""<details><summary>
|
||||
<b>{item_code}:</b> {len(serial_nos)} Serial Numbers <span class="caret"></span>
|
||||
</summary>
|
||||
<div class="small">{2}</div></details>
|
||||
""".format(
|
||||
item_code, len(serial_nos), body
|
||||
)
|
||||
<div class="small">{body}</div></details>
|
||||
"""
|
||||
|
||||
|
||||
def get_item_details(item_code):
|
||||
@@ -657,9 +670,7 @@ def get_serial_nos(serial_no):
|
||||
if isinstance(serial_no, list):
|
||||
return serial_no
|
||||
|
||||
return [
|
||||
s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
|
||||
]
|
||||
return [s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()]
|
||||
|
||||
|
||||
def clean_serial_no_string(serial_no: str) -> str:
|
||||
@@ -779,11 +790,9 @@ def update_maintenance_status():
|
||||
def get_delivery_note_serial_no(item_code, qty, delivery_note):
|
||||
serial_nos = ""
|
||||
dn_serial_nos = frappe.db.sql_list(
|
||||
""" select name from `tabSerial No`
|
||||
f""" select name from `tabSerial No`
|
||||
where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s
|
||||
and sales_invoice is null limit {0}""".format(
|
||||
cint(qty)
|
||||
),
|
||||
and sales_invoice is null limit {cint(qty)}""",
|
||||
{"item_code": item_code, "delivery_note": delivery_note},
|
||||
)
|
||||
|
||||
@@ -798,12 +807,11 @@ def auto_fetch_serial_number(
|
||||
qty: int,
|
||||
item_code: str,
|
||||
warehouse: str,
|
||||
posting_date: Optional[str] = None,
|
||||
batch_nos: Optional[Union[str, List[str]]] = None,
|
||||
for_doctype: Optional[str] = None,
|
||||
posting_date: str | None = None,
|
||||
batch_nos: str | list[str] | None = None,
|
||||
for_doctype: str | None = None,
|
||||
exclude_sr_nos=None,
|
||||
) -> List[str]:
|
||||
|
||||
) -> list[str]:
|
||||
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
|
||||
|
||||
if exclude_sr_nos is None:
|
||||
|
||||
@@ -248,16 +248,12 @@ class TestSerialNo(FrappeTestCase):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
serial_nos = ["LOWVALUATION", "HIGHVALUATION"]
|
||||
|
||||
in1 = make_stock_entry(
|
||||
item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0]
|
||||
)
|
||||
in2 = make_stock_entry(
|
||||
make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0])
|
||||
make_stock_entry(
|
||||
item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1]
|
||||
)
|
||||
|
||||
out = create_delivery_note(
|
||||
item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True
|
||||
)
|
||||
out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True)
|
||||
|
||||
# change serial no
|
||||
out.items[0].serial_no = serial_nos[1]
|
||||
|
||||
@@ -76,9 +76,7 @@ def create_test_shipment(delivery_notes=None):
|
||||
shipment.description_of_content = "unit test entry"
|
||||
for delivery_note in delivery_notes:
|
||||
shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name})
|
||||
shipment.append(
|
||||
"shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5}
|
||||
)
|
||||
shipment.append("shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5})
|
||||
shipment.insert()
|
||||
return shipment
|
||||
|
||||
@@ -96,9 +94,7 @@ def get_shipment_customer_contact(customer_name):
|
||||
|
||||
def get_shipment_customer_address(customer_name):
|
||||
address_title = customer_name + " address 123"
|
||||
customer_address = frappe.get_all(
|
||||
"Address", fields=["name"], filters={"address_title": address_title}
|
||||
)
|
||||
customer_address = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title})
|
||||
if len(customer_address):
|
||||
return customer_address[0]
|
||||
else:
|
||||
@@ -160,9 +156,7 @@ def create_customer_contact(fname, lname):
|
||||
customer.is_primary_contact = 1
|
||||
customer.is_billing_contact = 1
|
||||
customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1})
|
||||
customer.append(
|
||||
"phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1}
|
||||
)
|
||||
customer.append("phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1})
|
||||
customer.status = "Passive"
|
||||
customer.insert()
|
||||
return customer
|
||||
|
||||
@@ -68,7 +68,7 @@ form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
|
||||
|
||||
class StockEntry(StockController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StockEntry, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.purchase_order:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
@@ -98,9 +98,7 @@ class StockEntry(StockController):
|
||||
def before_validate(self):
|
||||
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
|
||||
|
||||
apply_rule = self.apply_putaway_rule and (
|
||||
self.purpose in ["Material Transfer", "Material Receipt"]
|
||||
)
|
||||
apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"])
|
||||
|
||||
if self.get("items") and apply_rule:
|
||||
apply_putaway_rule(self.doctype, self.get("items"), self.company, purpose=self.purpose)
|
||||
@@ -268,7 +266,11 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Send to Warehouse":
|
||||
for d in frappe.get_all(
|
||||
"Stock Entry",
|
||||
filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"},
|
||||
filters={
|
||||
"docstatus": 0,
|
||||
"outgoing_stock_entry": self.name,
|
||||
"purpose": "Receive at Warehouse",
|
||||
},
|
||||
):
|
||||
frappe.delete_doc("Stock Entry", d.name)
|
||||
|
||||
@@ -422,7 +424,7 @@ class StockEntry(StockController):
|
||||
return
|
||||
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
fg_item = list(fg_qty.keys())[0]
|
||||
fg_item = next(iter(fg_qty.keys()))
|
||||
fg_item_qty = flt(fg_qty[fg_item], precision)
|
||||
fg_completed_qty = flt(self.fg_completed_qty, precision)
|
||||
|
||||
@@ -605,14 +607,13 @@ class StockEntry(StockController):
|
||||
production_item, qty = frappe.db.get_value(
|
||||
"Work Order", self.work_order, ["production_item", "qty"]
|
||||
)
|
||||
args = other_ste + [production_item]
|
||||
args = [*other_ste, production_item]
|
||||
fg_qty_already_entered = frappe.db.sql(
|
||||
"""select sum(transfer_qty)
|
||||
from `tabStock Entry Detail`
|
||||
where parent in (%s)
|
||||
and item_code = %s
|
||||
and ifnull(s_warehouse,'')='' """
|
||||
% (", ".join(["%s" * len(other_ste)]), "%s"),
|
||||
where parent in ({})
|
||||
and item_code = {}
|
||||
and ifnull(s_warehouse,'')='' """.format(", ".join(["%s" * len(other_ste)]), "%s"),
|
||||
args,
|
||||
)[0][0]
|
||||
if fg_qty_already_entered and fg_qty_already_entered >= qty:
|
||||
@@ -690,9 +691,7 @@ class StockEntry(StockController):
|
||||
Set rate for outgoing, scrapped and finished items
|
||||
"""
|
||||
# Set rate for outgoing items
|
||||
outgoing_items_cost = self.set_rate_for_outgoing_items(
|
||||
reset_outgoing_rate, raise_error_if_no_rate
|
||||
)
|
||||
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
|
||||
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
|
||||
|
||||
items = []
|
||||
@@ -856,8 +855,6 @@ class StockEntry(StockController):
|
||||
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
|
||||
|
||||
def validate_duplicate_serial_no(self):
|
||||
warehouse_wise_serial_nos = {}
|
||||
|
||||
# In case of repack the source and target serial nos could be same
|
||||
for warehouse in ["s_warehouse", "t_warehouse"]:
|
||||
serial_nos = []
|
||||
@@ -895,13 +892,17 @@ class StockEntry(StockController):
|
||||
item_code = se_item.original_item or se_item.item_code
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||
required_qty = sum(
|
||||
[flt(d.required_qty) for d in subcontract_order.supplied_items if d.rm_item_code == item_code]
|
||||
[
|
||||
flt(d.required_qty)
|
||||
for d in subcontract_order.supplied_items
|
||||
if d.rm_item_code == item_code
|
||||
]
|
||||
)
|
||||
|
||||
total_allowed = required_qty + (required_qty * (qty_allowance / 100))
|
||||
|
||||
if not required_qty:
|
||||
bom_no = frappe.db.get_value(
|
||||
frappe.db.get_value(
|
||||
f"{self.subcontract_data.order_doctype} Item",
|
||||
{
|
||||
"parent": self.get(self.subcontract_data.order_field),
|
||||
@@ -947,7 +948,10 @@ class StockEntry(StockController):
|
||||
& (se.docstatus == 1)
|
||||
& (se_detail.item_code == se_item.item_code)
|
||||
& (
|
||||
((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail))
|
||||
(
|
||||
(se.purchase_order == self.purchase_order)
|
||||
& (se_detail.po_detail == se_item.po_detail)
|
||||
)
|
||||
if self.subcontract_data.order_doctype == "Purchase Order"
|
||||
else (
|
||||
(se.subcontracting_order == self.subcontracting_order)
|
||||
@@ -1000,7 +1004,9 @@ class StockEntry(StockController):
|
||||
else:
|
||||
if not se_item.allow_alternative_item:
|
||||
frappe.throw(
|
||||
_("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format(
|
||||
_(
|
||||
"Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}"
|
||||
).format(
|
||||
se_item.idx,
|
||||
se_item.item_code,
|
||||
self.subcontract_data.order_doctype,
|
||||
@@ -1187,12 +1193,18 @@ class StockEntry(StockController):
|
||||
for d in self.get("items"):
|
||||
if cstr(d.s_warehouse):
|
||||
sle = self.get_sl_entries(
|
||||
d, {"warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0}
|
||||
d,
|
||||
{
|
||||
"warehouse": cstr(d.s_warehouse),
|
||||
"actual_qty": -flt(d.transfer_qty),
|
||||
"incoming_rate": 0,
|
||||
},
|
||||
)
|
||||
if cstr(d.t_warehouse):
|
||||
sle.dependant_sle_voucher_detail_no = d.name
|
||||
elif finished_item_row and (
|
||||
finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse
|
||||
finished_item_row.item_code != d.item_code
|
||||
or finished_item_row.t_warehouse != d.s_warehouse
|
||||
):
|
||||
sle.dependant_sle_voucher_detail_no = finished_item_row.name
|
||||
|
||||
@@ -1215,7 +1227,7 @@ class StockEntry(StockController):
|
||||
sl_entries.append(sle)
|
||||
|
||||
def get_gl_entries(self, warehouse_account):
|
||||
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
|
||||
gl_entries = super().get_gl_entries(warehouse_account)
|
||||
|
||||
if self.purpose in ("Repack", "Manufacture"):
|
||||
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
|
||||
@@ -1248,9 +1260,9 @@ class StockEntry(StockController):
|
||||
flt(t.amount * multiply_based_on) / divide_based_on
|
||||
)
|
||||
|
||||
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += (
|
||||
flt(t.base_amount * multiply_based_on) / divide_based_on
|
||||
)
|
||||
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account][
|
||||
"base_amount"
|
||||
] += flt(t.base_amount * multiply_based_on) / divide_based_on
|
||||
|
||||
if item_account_wise_additional_cost:
|
||||
for d in self.get("items"):
|
||||
@@ -1282,7 +1294,9 @@ class StockEntry(StockController):
|
||||
"cost_center": d.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": -1
|
||||
* amount["base_amount"], # put it as negative credit instead of debit purposefully
|
||||
* amount[
|
||||
"base_amount"
|
||||
], # put it as negative credit instead of debit purposefully
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
@@ -1415,7 +1429,10 @@ class StockEntry(StockController):
|
||||
):
|
||||
subcontract_items = frappe.get_all(
|
||||
self.subcontract_data.order_supplied_items_field,
|
||||
{"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")},
|
||||
{
|
||||
"parent": self.get(self.subcontract_data.order_field),
|
||||
"rm_item_code": args.get("item_code"),
|
||||
},
|
||||
"main_item_code",
|
||||
)
|
||||
|
||||
@@ -1465,7 +1482,6 @@ class StockEntry(StockController):
|
||||
)
|
||||
|
||||
if self.bom_no:
|
||||
|
||||
backflush_based_on = frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "backflush_raw_materials_based_on"
|
||||
)
|
||||
@@ -1479,7 +1495,6 @@ class StockEntry(StockController):
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
]:
|
||||
|
||||
if self.work_order and self.purpose == "Material Transfer for Manufacture":
|
||||
item_dict = self.get_pending_raw_materials(backflush_based_on)
|
||||
if self.to_warehouse and self.pro_doc:
|
||||
@@ -1489,7 +1504,10 @@ class StockEntry(StockController):
|
||||
|
||||
elif (
|
||||
self.work_order
|
||||
and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
|
||||
and (
|
||||
self.purpose == "Manufacture"
|
||||
or self.purpose == "Material Consumption for Manufacture"
|
||||
)
|
||||
and not self.pro_doc.skip_transfer
|
||||
and self.flags.backflush_based_on == "Material Transferred for Manufacture"
|
||||
):
|
||||
@@ -1497,7 +1515,10 @@ class StockEntry(StockController):
|
||||
|
||||
elif (
|
||||
self.work_order
|
||||
and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
|
||||
and (
|
||||
self.purpose == "Manufacture"
|
||||
or self.purpose == "Material Consumption for Manufacture"
|
||||
)
|
||||
and self.flags.backflush_based_on == "BOM"
|
||||
and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1
|
||||
):
|
||||
@@ -1510,7 +1531,10 @@ class StockEntry(StockController):
|
||||
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
|
||||
|
||||
# Get Subcontract Order Supplied Items Details
|
||||
if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
|
||||
if (
|
||||
self.get(self.subcontract_data.order_field)
|
||||
and self.purpose == "Send to Subcontractor"
|
||||
):
|
||||
# Get Subcontract Order Supplied Items Details
|
||||
parent = frappe.qb.DocType(self.subcontract_data.order_doctype)
|
||||
child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field)
|
||||
@@ -1529,9 +1553,14 @@ class StockEntry(StockController):
|
||||
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
|
||||
item["from_warehouse"] = self.pro_doc.wip_warehouse
|
||||
# Get Reserve Warehouse from Subcontract Order
|
||||
if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
|
||||
if (
|
||||
self.get(self.subcontract_data.order_field)
|
||||
and self.purpose == "Send to Subcontractor"
|
||||
):
|
||||
item["from_warehouse"] = item_wh.get(item.item_code)
|
||||
item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
|
||||
item["to_warehouse"] = (
|
||||
self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
|
||||
)
|
||||
|
||||
self.add_to_stock_entry_detail(item_dict)
|
||||
|
||||
@@ -1727,9 +1756,7 @@ class StockEntry(StockController):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
||||
if (
|
||||
frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
|
||||
)
|
||||
frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies")
|
||||
and self.work_order
|
||||
and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
|
||||
):
|
||||
@@ -1885,7 +1912,7 @@ class StockEntry(StockController):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for key, row in available_materials.items():
|
||||
for _key, row in available_materials.items():
|
||||
remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty)
|
||||
if remaining_qty_to_produce <= 0 and not self.is_return:
|
||||
continue
|
||||
@@ -2029,9 +2056,7 @@ class StockEntry(StockController):
|
||||
continue
|
||||
|
||||
transfer_pending = flt(d.required_qty) > flt(d.transferred_qty)
|
||||
can_transfer = transfer_pending or (
|
||||
backflush_based_on == "Material Transferred for Manufacture"
|
||||
)
|
||||
can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture")
|
||||
|
||||
if not can_transfer:
|
||||
continue
|
||||
@@ -2046,7 +2071,9 @@ class StockEntry(StockController):
|
||||
)
|
||||
item_row["job_card_item"] = job_card_item or None
|
||||
|
||||
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):
|
||||
if d.source_warehouse and not frappe.db.get_value(
|
||||
"Warehouse", d.source_warehouse, "is_group"
|
||||
):
|
||||
item_row["from_warehouse"] = d.source_warehouse
|
||||
|
||||
item_row["to_warehouse"] = wip_warehouse
|
||||
@@ -2101,9 +2128,9 @@ class StockEntry(StockController):
|
||||
if item_row.get(field):
|
||||
se_child.set(field, item_row.get(field))
|
||||
|
||||
if se_child.s_warehouse == None:
|
||||
if se_child.s_warehouse is None:
|
||||
se_child.s_warehouse = self.from_warehouse
|
||||
if se_child.t_warehouse == None:
|
||||
if se_child.t_warehouse is None:
|
||||
se_child.t_warehouse = self.to_warehouse
|
||||
|
||||
# in stock uom
|
||||
@@ -2159,15 +2186,20 @@ class StockEntry(StockController):
|
||||
expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date")
|
||||
if expiry_date:
|
||||
if getdate(self.posting_date) > getdate(expiry_date):
|
||||
frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code))
|
||||
frappe.throw(
|
||||
_("Batch {0} of Item {1} has expired.").format(
|
||||
item.batch_no, item.item_code
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code))
|
||||
frappe.throw(
|
||||
_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)
|
||||
)
|
||||
|
||||
def update_subcontract_order_supplied_items(self):
|
||||
if self.get(self.subcontract_data.order_field) and (
|
||||
self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
|
||||
):
|
||||
|
||||
# Get Subcontract Order Supplied Items Details
|
||||
order_supplied_items = frappe.db.get_all(
|
||||
self.subcontract_data.order_supplied_items_field,
|
||||
@@ -2268,8 +2300,8 @@ class StockEntry(StockController):
|
||||
|
||||
cond = ""
|
||||
for data, transferred_qty in stock_entries.items():
|
||||
cond += """ WHEN (parent = %s and name = %s) THEN %s
|
||||
""" % (
|
||||
cond += """ WHEN (parent = {} and name = {}) THEN {}
|
||||
""".format(
|
||||
frappe.db.escape(data[0]),
|
||||
frappe.db.escape(data[1]),
|
||||
transferred_qty,
|
||||
@@ -2325,7 +2357,9 @@ class StockEntry(StockController):
|
||||
material_request = item.material_request or None
|
||||
if self.purpose == "Material Transfer" and material_request not in material_requests:
|
||||
if self.outgoing_stock_entry and parent_se:
|
||||
material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request")
|
||||
material_request = frappe.get_value(
|
||||
"Stock Entry Detail", item.ste_detail, "material_request"
|
||||
)
|
||||
|
||||
if material_request and material_request not in material_requests:
|
||||
material_requests.append(material_request)
|
||||
@@ -2530,9 +2564,7 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
)
|
||||
)
|
||||
):
|
||||
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(
|
||||
work_order.produced_qty
|
||||
)
|
||||
operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
|
||||
|
||||
return operating_cost_per_unit
|
||||
|
||||
@@ -2545,24 +2577,20 @@ def get_used_alternative_items(
|
||||
if subcontract_order:
|
||||
cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'"
|
||||
elif work_order:
|
||||
cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
|
||||
work_order
|
||||
)
|
||||
cond = f"and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{work_order}'"
|
||||
|
||||
if not cond:
|
||||
return {}
|
||||
|
||||
used_alternative_items = {}
|
||||
data = frappe.db.sql(
|
||||
""" select sted.original_item, sted.uom, sted.conversion_factor,
|
||||
f""" select sted.original_item, sted.uom, sted.conversion_factor,
|
||||
sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description
|
||||
from
|
||||
`tabStock Entry` ste, `tabStock Entry Detail` sted
|
||||
where
|
||||
sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code
|
||||
{0} """.format(
|
||||
cond
|
||||
),
|
||||
{cond} """,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -2600,9 +2628,7 @@ def get_uom_details(item_code, uom, qty):
|
||||
conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor")
|
||||
|
||||
if not conversion_factor:
|
||||
frappe.msgprint(
|
||||
_("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
|
||||
)
|
||||
frappe.msgprint(_("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code))
|
||||
ret = {"uom": ""}
|
||||
else:
|
||||
ret = {
|
||||
@@ -2712,9 +2738,7 @@ def get_supplied_items(
|
||||
else:
|
||||
supplied_item.supplied_qty += row.transfer_qty
|
||||
|
||||
supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(
|
||||
supplied_item.returned_qty
|
||||
)
|
||||
supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
|
||||
|
||||
return supplied_item_details
|
||||
|
||||
@@ -2804,7 +2828,11 @@ def get_stock_entry_data(work_order):
|
||||
& (stock_entry_detail.s_warehouse.isnotnull())
|
||||
& (
|
||||
stock_entry.purpose.isin(
|
||||
["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"]
|
||||
[
|
||||
"Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Material Transfer for Manufacture",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, overload
|
||||
from typing import TYPE_CHECKING, overload
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint, flt
|
||||
@@ -18,15 +18,15 @@ def make_stock_entry(
|
||||
*,
|
||||
item_code: str,
|
||||
qty: float,
|
||||
company: Optional[str] = None,
|
||||
from_warehouse: Optional[str] = None,
|
||||
to_warehouse: Optional[str] = None,
|
||||
rate: Optional[float] = None,
|
||||
serial_no: Optional[str] = None,
|
||||
batch_no: Optional[str] = None,
|
||||
posting_date: Optional[str] = None,
|
||||
posting_time: Optional[str] = None,
|
||||
purpose: Optional[str] = None,
|
||||
company: str | None = None,
|
||||
from_warehouse: str | None = None,
|
||||
to_warehouse: str | None = None,
|
||||
rate: float | None = None,
|
||||
serial_no: str | None = None,
|
||||
batch_no: str | None = None,
|
||||
posting_date: str | None = None,
|
||||
posting_time: str | None = None,
|
||||
purpose: str | None = None,
|
||||
do_not_save: bool = False,
|
||||
do_not_submit: bool = False,
|
||||
inspection_required: bool = False,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, flt, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.item.test_item import (
|
||||
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.item.test_item import (
|
||||
make_item_variant,
|
||||
set_item_variant_settings,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
|
||||
from erpnext.stock.doctype.serial_no.serial_no import *
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
||||
FinishedGoodError,
|
||||
make_stock_in_entry,
|
||||
@@ -35,7 +35,7 @@ def get_sle(**args):
|
||||
condition, values = "", []
|
||||
for key, value in args.items():
|
||||
condition += " and " if condition else " where "
|
||||
condition += "`{0}`=%s".format(key)
|
||||
condition += f"`{key}`=%s"
|
||||
values.append(value)
|
||||
|
||||
return frappe.db.sql(
|
||||
@@ -136,8 +136,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
)
|
||||
|
||||
projected_qty = (
|
||||
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty")
|
||||
or 0
|
||||
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") or 0
|
||||
)
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "auto_indent", 1)
|
||||
@@ -664,14 +663,10 @@ class TestStockEntry(FrappeTestCase):
|
||||
se.set_stock_entry_type()
|
||||
se.insert()
|
||||
se.submit()
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC"
|
||||
)
|
||||
self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC")
|
||||
|
||||
se.cancel()
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC"
|
||||
)
|
||||
self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC")
|
||||
|
||||
def test_serial_warehouse_error(self):
|
||||
make_serialized_item(target_warehouse="_Test Warehouse 1 - _TC")
|
||||
@@ -719,9 +714,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
else:
|
||||
item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
||||
)
|
||||
se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
|
||||
batch_no = se.items[0].batch_no
|
||||
serial_no = get_serial_nos(se.items[0].serial_no)[0]
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
@@ -797,7 +790,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
||||
frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
|
||||
frappe.get_doc("User", "test2@example.com").add_roles(
|
||||
"Sales User", "Sales Manager", "Stock User", "Stock Manager"
|
||||
)
|
||||
@@ -903,12 +896,8 @@ class TestStockEntry(FrappeTestCase):
|
||||
for d in stock_entry.get("items"):
|
||||
if d.item_code != "_Test FG Item 2":
|
||||
rm_cost += flt(d.amount)
|
||||
fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items")))[
|
||||
0
|
||||
].amount
|
||||
self.assertEqual(
|
||||
fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)
|
||||
)
|
||||
fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items"))).amount
|
||||
self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
|
||||
|
||||
@change_settings("Manufacturing Settings", {"material_consumption": 1})
|
||||
def test_work_order_manufacture_with_material_consumption(self):
|
||||
@@ -951,8 +940,8 @@ class TestStockEntry(FrappeTestCase):
|
||||
for d in s.get("items"):
|
||||
if d.s_warehouse:
|
||||
rm_cost += d.amount
|
||||
fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount
|
||||
scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
|
||||
fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount
|
||||
scrap_cost = next(filter(lambda x: x.is_scrap_item, s.get("items"))).amount
|
||||
self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2))
|
||||
|
||||
# When Stock Entry has only FG + Scrap
|
||||
@@ -966,13 +955,11 @@ class TestStockEntry(FrappeTestCase):
|
||||
rm_cost += d.amount
|
||||
self.assertEqual(rm_cost, 0)
|
||||
expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
|
||||
fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount
|
||||
fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount
|
||||
self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
|
||||
|
||||
def test_variant_work_order(self):
|
||||
bom_no = frappe.db.get_value(
|
||||
"BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1}
|
||||
)
|
||||
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1})
|
||||
|
||||
make_item_variant() # make variant of _Test Variant Item if absent
|
||||
|
||||
@@ -1073,9 +1060,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
receipt_entry.insert()
|
||||
receipt_entry.submit()
|
||||
|
||||
retention_data = move_sample_to_retention_warehouse(
|
||||
receipt_entry.company, receipt_entry.get("items")
|
||||
)
|
||||
retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items"))
|
||||
retention_entry = frappe.new_doc("Stock Entry")
|
||||
retention_entry.company = retention_data.company
|
||||
retention_entry.purpose = retention_data.purpose
|
||||
@@ -1126,9 +1111,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, repack.submit)
|
||||
|
||||
def test_customer_provided_parts_se(self):
|
||||
create_item(
|
||||
"CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
|
||||
)
|
||||
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
||||
se = make_stock_entry(
|
||||
item_code="CUST-0987", purpose="Material Receipt", qty=4, to_warehouse="_Test Warehouse - _TC"
|
||||
)
|
||||
@@ -1243,9 +1226,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.check_gl_entries(
|
||||
"Stock Entry",
|
||||
se.name,
|
||||
sorted(
|
||||
[["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]
|
||||
),
|
||||
sorted([["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]),
|
||||
)
|
||||
|
||||
def test_conversion_factor_change(self):
|
||||
@@ -1409,7 +1390,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
)
|
||||
batch_nos.append(se2.items[0].batch_no)
|
||||
|
||||
with self.assertRaises(NegativeStockError) as nse:
|
||||
with self.assertRaises(NegativeStockError):
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
@@ -1455,7 +1436,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
item_code=item_code, qty=1, from_warehouse=warehouse, purpose="Material Issue", do_not_save=True
|
||||
)
|
||||
issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False))
|
||||
for row, batch_no in zip(issue.items, batch_nos):
|
||||
for row, batch_no in zip(issue.items, batch_nos, strict=False):
|
||||
row.batch_no = batch_no
|
||||
issue.save()
|
||||
issue.submit()
|
||||
@@ -1663,15 +1644,12 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertRaises(BatchExpiredError, se.save)
|
||||
|
||||
def test_negative_stock_reco(self):
|
||||
from erpnext.controllers.stock_controller import BatchExpiredError
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
|
||||
|
||||
item_code = "Test Negative Item - 001"
|
||||
item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
|
||||
create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
|
||||
|
||||
se1 = make_stock_entry(
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="00:00:00",
|
||||
@@ -1680,7 +1658,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
posting_date=today(),
|
||||
posting_time="00:00:00",
|
||||
|
||||
@@ -222,7 +222,9 @@ class StockLedgerEntry(Document):
|
||||
)
|
||||
if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles():
|
||||
frappe.throw(
|
||||
_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days),
|
||||
_("Not allowed to update stock transactions older than {0}").format(
|
||||
stock_frozen_upto_days
|
||||
),
|
||||
StockFreezeError,
|
||||
)
|
||||
|
||||
@@ -240,7 +242,9 @@ class StockLedgerEntry(Document):
|
||||
expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date")
|
||||
if expiry_date:
|
||||
if getdate(self.posting_date) > getdate(expiry_date):
|
||||
frappe.throw(_("Batch {0} of Item {1} has expired.").format(self.batch_no, self.item_code))
|
||||
frappe.throw(
|
||||
_("Batch {0} of Item {1} has expired.").format(self.batch_no, self.item_code)
|
||||
)
|
||||
|
||||
def validate_and_set_fiscal_year(self):
|
||||
if not self.fiscal_year:
|
||||
@@ -273,7 +277,7 @@ class StockLedgerEntry(Document):
|
||||
(self.item_code, self.warehouse),
|
||||
)[0][0]
|
||||
|
||||
cur_doc_posting_datetime = "%s %s" % (
|
||||
cur_doc_posting_datetime = "{} {}".format(
|
||||
self.posting_date,
|
||||
self.get("posting_time") or "00:00:00",
|
||||
)
|
||||
@@ -282,7 +286,9 @@ class StockLedgerEntry(Document):
|
||||
last_transaction_time
|
||||
):
|
||||
msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(
|
||||
frappe.bold(self.item_code), frappe.bold(self.warehouse), frappe.bold(last_transaction_time)
|
||||
frappe.bold(self.item_code),
|
||||
frappe.bold(self.warehouse),
|
||||
frappe.bold(last_transaction_time),
|
||||
)
|
||||
|
||||
msg += "<br><br>" + _(
|
||||
|
||||
@@ -8,7 +8,6 @@ from uuid import uuid4
|
||||
import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.query_builder.functions import CombineDatetime
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, add_to_date, flt, today
|
||||
|
||||
@@ -35,8 +34,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
|
||||
# delete SLE and BINs for all items
|
||||
frappe.db.sql(
|
||||
"delete from `tabStock Ledger Entry` where item_code in (%s)"
|
||||
% (", ".join(["%s"] * len(items))),
|
||||
"delete from `tabStock Ledger Entry` where item_code in (%s)" % (", ".join(["%s"] * len(items))),
|
||||
items,
|
||||
)
|
||||
frappe.db.sql(
|
||||
@@ -122,7 +120,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
|
||||
|
||||
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
|
||||
sr = create_stock_reconciliation(
|
||||
create_stock_reconciliation(
|
||||
item_code="_Test Item for Reposting",
|
||||
warehouse="Stores - _TC",
|
||||
qty=50,
|
||||
@@ -484,7 +482,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125]
|
||||
|
||||
self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
|
||||
for dn, incoming_rate in zip(dns, expected_incoming_rates):
|
||||
for dn, incoming_rate in zip(dns, expected_incoming_rates, strict=False):
|
||||
self.assertEqual(
|
||||
dn.items[0].incoming_rate,
|
||||
incoming_rate,
|
||||
@@ -556,9 +554,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
],
|
||||
)
|
||||
|
||||
reciept = make_stock_entry(
|
||||
item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10
|
||||
)
|
||||
reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10)
|
||||
self.assertSLEs(
|
||||
reciept,
|
||||
[
|
||||
@@ -680,9 +676,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
],
|
||||
)
|
||||
|
||||
consume_today = make_stock_entry(
|
||||
item_code=item_code, source=warehouse, batch_no=batches[0], qty=1
|
||||
)
|
||||
consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], qty=1)
|
||||
self.assertSLEs(
|
||||
consume_today,
|
||||
[
|
||||
@@ -715,8 +709,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
|
||||
|
||||
def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns):
|
||||
for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)):
|
||||
for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals):
|
||||
for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details, strict=False)):
|
||||
for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals, strict=False):
|
||||
if col == "stock_queue":
|
||||
sle_val = get_stock_value_from_q(sle_val)
|
||||
ex_sle_val = get_stock_value_from_q(ex_sle_val)
|
||||
@@ -746,9 +740,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
se_entry_list_mi = [
|
||||
(item, warehouses[0], None, batches[1], 1, None, "2021-01-29"),
|
||||
]
|
||||
ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
|
||||
se_entry_list_mi, "Material Issue"
|
||||
)
|
||||
ses = create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list_mi, "Material Issue")
|
||||
sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
|
||||
expected_sle_details = [(-50.0, 100.0, -1.0, 1.0, "[[1, 100.0]]")]
|
||||
details_list.append((sle_details, expected_sle_details, "Material Issue Entries", columns))
|
||||
@@ -771,9 +763,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
sle["qty_after_transaction"] = state["qty"]
|
||||
return exp_sles
|
||||
|
||||
old1 = make_stock_entry(
|
||||
item_code=item_code, target=warehouse, batch_no=batches[0], qty=10, rate=10
|
||||
)
|
||||
old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], qty=10, rate=10)
|
||||
self.assertSLEs(
|
||||
old1,
|
||||
update_invariants(
|
||||
@@ -782,20 +772,20 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
]
|
||||
),
|
||||
)
|
||||
old2 = make_stock_entry(
|
||||
item_code=item_code, target=warehouse, batch_no=batches[1], qty=10, rate=20
|
||||
)
|
||||
old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], qty=10, rate=20)
|
||||
self.assertSLEs(
|
||||
old2,
|
||||
update_invariants(
|
||||
[
|
||||
{"actual_qty": 10, "stock_value_difference": 10 * 20, "stock_queue": [[10, 10], [10, 20]]},
|
||||
{
|
||||
"actual_qty": 10,
|
||||
"stock_value_difference": 10 * 20,
|
||||
"stock_queue": [[10, 10], [10, 20]],
|
||||
},
|
||||
]
|
||||
),
|
||||
)
|
||||
old3 = make_stock_entry(
|
||||
item_code=item_code, target=warehouse, batch_no=batches[0], qty=5, rate=15
|
||||
)
|
||||
old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], qty=5, rate=15)
|
||||
|
||||
self.assertSLEs(
|
||||
old3,
|
||||
@@ -842,9 +832,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
)
|
||||
|
||||
# consume old batch as per FIFO
|
||||
consume_old1 = make_stock_entry(
|
||||
item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]
|
||||
)
|
||||
consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
|
||||
self.assertSLEs(
|
||||
consume_old1,
|
||||
update_invariants(
|
||||
@@ -859,22 +847,22 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
)
|
||||
|
||||
# consume new batch as per batch
|
||||
consume_new2 = make_stock_entry(
|
||||
item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]
|
||||
)
|
||||
consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
|
||||
self.assertSLEs(
|
||||
consume_new2,
|
||||
update_invariants(
|
||||
[
|
||||
{"actual_qty": -10, "stock_value_difference": -10 * 42, "stock_queue": [[5, 20], [5, 15]]},
|
||||
{
|
||||
"actual_qty": -10,
|
||||
"stock_value_difference": -10 * 42,
|
||||
"stock_queue": [[5, 20], [5, 15]],
|
||||
},
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# finish all old batches
|
||||
consume_old2 = make_stock_entry(
|
||||
item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]
|
||||
)
|
||||
consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
|
||||
self.assertSLEs(
|
||||
consume_old2,
|
||||
update_invariants(
|
||||
@@ -885,9 +873,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
)
|
||||
|
||||
# finish all new batches
|
||||
consume_new1 = make_stock_entry(
|
||||
item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]
|
||||
)
|
||||
consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
|
||||
self.assertSLEs(
|
||||
consume_new1,
|
||||
update_invariants(
|
||||
@@ -938,9 +924,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
|
||||
rates = [10 * i for i in range(1, 5)]
|
||||
|
||||
receipt = make_stock_entry(
|
||||
item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10
|
||||
)
|
||||
receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
@@ -970,9 +954,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
repack.submit()
|
||||
|
||||
# same exact queue should be transferred
|
||||
self.assertSLEs(
|
||||
repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name}
|
||||
)
|
||||
self.assertSLEs(repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name})
|
||||
|
||||
def test_negative_fifo_valuation(self):
|
||||
"""
|
||||
@@ -982,7 +964,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
item = make_item(properties={"allow_negative_stock": 1}).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10)
|
||||
make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10)
|
||||
consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15)
|
||||
|
||||
self.assertSLEs(consume1, [{"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}])
|
||||
@@ -1022,27 +1004,21 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(50, _get_stock_credit(depdendent_consumption))
|
||||
|
||||
# backdated receipt - should trigger GL repost of all previous stock entries
|
||||
bd_receipt = make_stock_entry(
|
||||
item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1)
|
||||
)
|
||||
bd_receipt = make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1))
|
||||
self.assertEqual(100, _get_stock_credit(depdendent_consumption))
|
||||
|
||||
# cancelling receipt should reset it back
|
||||
bd_receipt.cancel()
|
||||
self.assertEqual(50, _get_stock_credit(depdendent_consumption))
|
||||
|
||||
bd_receipt2 = make_stock_entry(
|
||||
item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2)
|
||||
)
|
||||
bd_receipt2 = make_stock_entry(item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2))
|
||||
# total as per FIFO -> 2 * 20 + 3 * 10 = 70
|
||||
self.assertEqual(70, _get_stock_credit(depdendent_consumption))
|
||||
|
||||
# transfer WIP material to final destination and consume it all
|
||||
depdendent_consumption.cancel()
|
||||
make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3))
|
||||
final_consumption = make_stock_entry(
|
||||
item_code=item, from_warehouse=C, qty=5, posting_date=_day(4)
|
||||
)
|
||||
final_consumption = make_stock_entry(item_code=item, from_warehouse=C, qty=5, posting_date=_day(4))
|
||||
# exact amount gets consumed
|
||||
self.assertEqual(70, _get_stock_credit(final_consumption))
|
||||
|
||||
@@ -1109,11 +1085,10 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual([1], ordered_qty_after_transaction())
|
||||
|
||||
def test_timestamp_clash(self):
|
||||
|
||||
item = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
reciept = make_stock_entry(
|
||||
make_stock_entry(
|
||||
item_code=item,
|
||||
to_warehouse=warehouse,
|
||||
qty=100,
|
||||
@@ -1122,7 +1097,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
posting_time="01:00:00",
|
||||
)
|
||||
|
||||
consumption = make_stock_entry(
|
||||
make_stock_entry(
|
||||
item_code=item,
|
||||
from_warehouse=warehouse,
|
||||
qty=50,
|
||||
@@ -1141,11 +1116,10 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
|
||||
try:
|
||||
backdated_receipt.cancel()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
self.fail("Double processing of qty for clashing timestamp.")
|
||||
|
||||
def test_previous_sle_with_clashed_timestamp(self):
|
||||
|
||||
item = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
@@ -1186,7 +1160,6 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(sle[0].actual_qty, 5)
|
||||
|
||||
def test_backdated_sle_with_same_timestamp(self):
|
||||
|
||||
item = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
@@ -1429,12 +1402,13 @@ def create_items(items=None, uoms=None):
|
||||
|
||||
|
||||
def setup_item_valuation_test(
|
||||
valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=["X", "Y"]
|
||||
valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=None
|
||||
):
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
if batches_list is None:
|
||||
batches_list = ["X", "Y"]
|
||||
if not suffix:
|
||||
suffix = get_unique_suffix()
|
||||
|
||||
@@ -1448,7 +1422,7 @@ def setup_item_valuation_test(
|
||||
for i, batch_id in enumerate(batches):
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
ubw = use_batchwise_valuation
|
||||
if isinstance(use_batchwise_valuation, (list, tuple)):
|
||||
if isinstance(use_batchwise_valuation, list | tuple):
|
||||
ubw = use_batchwise_valuation[i]
|
||||
batch = frappe.get_doc(
|
||||
frappe._dict(
|
||||
@@ -1479,9 +1453,7 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list
|
||||
|
||||
dns = []
|
||||
for item, warehouse, batch_no, qty, rate in dn_entry_list:
|
||||
so = make_sales_order(
|
||||
rate=rate, qty=qty, item=item, warehouse=warehouse, against_blanket_order=0
|
||||
)
|
||||
so = make_sales_order(rate=rate, qty=qty, item=item, warehouse=warehouse, against_blanket_order=0)
|
||||
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.items[0].batch_no = batch_no
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, msgprint
|
||||
@@ -27,7 +26,7 @@ class EmptyStockReconciliationItemsError(frappe.ValidationError):
|
||||
|
||||
class StockReconciliation(StockController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StockReconciliation, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
|
||||
|
||||
def validate(self):
|
||||
@@ -87,7 +86,9 @@ class StockReconciliation(StockController):
|
||||
if not item.batch_no and not item.serial_no:
|
||||
for dimension in get_inventory_dimensions():
|
||||
if item.get(dimension.get("fieldname")):
|
||||
inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname"))
|
||||
inventory_dimensions_dict[dimension.get("fieldname")] = item.get(
|
||||
dimension.get("fieldname")
|
||||
)
|
||||
|
||||
item_dict = get_stock_balance_for(
|
||||
item.item_code,
|
||||
@@ -189,19 +190,27 @@ class StockReconciliation(StockController):
|
||||
|
||||
# do not allow negative valuation
|
||||
if flt(row.valuation_rate) < 0:
|
||||
self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed")))
|
||||
self.validation_messages.append(
|
||||
_get_msg(row_num, _("Negative Valuation Rate is not allowed"))
|
||||
)
|
||||
|
||||
if row.batch_no and frappe.get_cached_value("Batch", row.batch_no, "item") != row.item_code:
|
||||
self.validation_messages.append(
|
||||
_get_msg(
|
||||
row_num,
|
||||
_("Batch {0} does not belong to item {1}").format(bold(row.batch_no), bold(row.item_code)),
|
||||
_("Batch {0} does not belong to item {1}").format(
|
||||
bold(row.batch_no), bold(row.item_code)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if row.qty and row.valuation_rate in ["", None]:
|
||||
row.valuation_rate = get_stock_balance(
|
||||
row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
with_valuation_rate=True,
|
||||
)[1]
|
||||
if not row.valuation_rate:
|
||||
# try if there is a buying price list in default currency
|
||||
@@ -263,11 +272,10 @@ class StockReconciliation(StockController):
|
||||
|
||||
sl_entries = []
|
||||
has_serial_no = False
|
||||
has_batch_no = False
|
||||
for row in self.items:
|
||||
item = frappe.get_doc("Item", row.item_code)
|
||||
if item.has_batch_no:
|
||||
has_batch_no = True
|
||||
pass
|
||||
|
||||
if not row.qty and not row.valuation_rate and not row.current_qty:
|
||||
self.make_adjustment_entry(row, sl_entries)
|
||||
@@ -551,9 +559,7 @@ class StockReconciliation(StockController):
|
||||
if not self.cost_center:
|
||||
msgprint(_("Please enter Cost Center"), raise_exception=1)
|
||||
|
||||
return super(StockReconciliation, self).get_gl_entries(
|
||||
warehouse_account, self.expense_account, self.cost_center
|
||||
)
|
||||
return super().get_gl_entries(warehouse_account, self.expense_account, self.cost_center)
|
||||
|
||||
def validate_expense_account(self):
|
||||
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||
@@ -648,7 +654,12 @@ class StockReconciliation(StockController):
|
||||
row.current_valuation_rate = item_dict.get("rate")
|
||||
else:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
row.batch_no,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
self.name,
|
||||
)
|
||||
|
||||
precesion = row.precision("current_qty")
|
||||
@@ -708,9 +719,7 @@ class StockReconciliation(StockController):
|
||||
return allow_negative_stock
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
item_code, warehouse, batch_no, posting_date, posting_time, voucher_no
|
||||
):
|
||||
def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, posting_time, voucher_no):
|
||||
ledger = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
query = (
|
||||
@@ -740,9 +749,7 @@ def get_batch_qty_for_stock_reco(
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items(
|
||||
warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False
|
||||
):
|
||||
def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
|
||||
ignore_empty_stock = cint(ignore_empty_stock)
|
||||
items = []
|
||||
if item_code and warehouse:
|
||||
@@ -913,16 +920,14 @@ def get_stock_balance_for(
|
||||
warehouse: str,
|
||||
posting_date,
|
||||
posting_time,
|
||||
batch_no: Optional[str] = None,
|
||||
batch_no: str | None = None,
|
||||
with_valuation_rate: bool = True,
|
||||
inventory_dimensions_dict=None,
|
||||
voucher_no=None,
|
||||
):
|
||||
frappe.has_permission("Stock Reconciliation", "write", throw=True)
|
||||
|
||||
item_dict = frappe.get_cached_value(
|
||||
"Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||
)
|
||||
item_dict = frappe.get_cached_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if not item_dict:
|
||||
# In cases of data upload to Items table
|
||||
@@ -956,9 +961,7 @@ def get_stock_balance_for(
|
||||
qty, rate = data
|
||||
|
||||
if item_dict.get("has_batch_no"):
|
||||
qty = (
|
||||
get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
|
||||
)
|
||||
qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
|
||||
|
||||
return {"qty": qty, "rate": rate, "serial_nos": serial_nos}
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
# no gl entries
|
||||
self.assertTrue(
|
||||
frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name}
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,7 +148,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
def test_stock_reco_for_serialized_item(self):
|
||||
to_delete_records = []
|
||||
to_delete_serial_nos = []
|
||||
|
||||
# Add new serial nos
|
||||
serial_item_code = "Stock-Reco-Serial-Item-1"
|
||||
@@ -215,7 +215,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
purpose="Opening Stock",
|
||||
)
|
||||
|
||||
for i in range(3):
|
||||
for _i in range(3):
|
||||
sr.append(
|
||||
"items",
|
||||
{
|
||||
@@ -394,9 +394,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
def test_customer_provided_items(self):
|
||||
item_code = "Stock-Reco-customer-Item-100"
|
||||
create_item(
|
||||
item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
|
||||
)
|
||||
create_item(item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
||||
|
||||
sr = create_stock_reconciliation(item_code=item_code, qty=10, rate=420)
|
||||
|
||||
@@ -579,18 +577,18 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
item_code = self.make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
create_stock_reconciliation(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10)
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
create_delivery_note(
|
||||
item_code=item_code, warehouse=warehouse, qty=5, rate=120, posting_date=add_days(nowdate(), 12)
|
||||
)
|
||||
old_bin_qty = frappe.db.get_value(
|
||||
"Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty"
|
||||
)
|
||||
|
||||
sr2 = create_stock_reconciliation(
|
||||
create_stock_reconciliation(
|
||||
item_code=item_code, warehouse=warehouse, qty=11, rate=100, posting_date=add_days(nowdate(), 11)
|
||||
)
|
||||
new_bin_qty = frappe.db.get_value(
|
||||
@@ -830,7 +828,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Stock Value => 100 * 100 = 10000
|
||||
se = make_stock_entry(
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=100,
|
||||
@@ -1118,9 +1116,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
active_serial_no = frappe.get_all(
|
||||
"Serial No", filters={"status": "Active", "item_code": item_code}
|
||||
)
|
||||
active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code})
|
||||
self.assertEqual(len(active_serial_no), 5)
|
||||
|
||||
|
||||
@@ -1213,9 +1209,7 @@ def create_stock_reconciliation(**args):
|
||||
)
|
||||
)
|
||||
if frappe.get_all("Stock Ledger Entry", {"company": sr.company})
|
||||
else frappe.get_cached_value(
|
||||
"Account", {"account_type": "Temporary", "company": sr.company}, "name"
|
||||
)
|
||||
else frappe.get_cached_value("Account", {"account_type": "Temporary", "company": sr.company}, "name")
|
||||
)
|
||||
sr.cost_center = (
|
||||
args.cost_center
|
||||
|
||||
@@ -61,9 +61,9 @@ class StockSettings(Document):
|
||||
for field in warehouse_fields:
|
||||
if frappe.db.get_value("Warehouse", self.get(field), "is_group"):
|
||||
frappe.throw(
|
||||
_("Group Warehouses cannot be used in transactions. Please change the value of {0}").format(
|
||||
frappe.bold(self.meta.get_field(field).label)
|
||||
),
|
||||
_(
|
||||
"Group Warehouses cannot be used in transactions. Please change the value of {0}"
|
||||
).format(frappe.bold(self.meta.get_field(field).label)),
|
||||
title=_("Incorrect Warehouse"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
@@ -58,7 +58,7 @@ class TestWarehouse(FrappeTestCase):
|
||||
warehouse_ids.append(warehouse_id)
|
||||
|
||||
item_names = [f"_Test Item {i} for Unlinking" for i in range(2)]
|
||||
for item, warehouse in zip(item_names, warehouse_ids):
|
||||
for item, warehouse in zip(item_names, warehouse_ids, strict=False):
|
||||
create_item(item, warehouse=warehouse, company=company)
|
||||
|
||||
# Delete warehouses
|
||||
@@ -78,7 +78,6 @@ class TestWarehouse(FrappeTestCase):
|
||||
)
|
||||
|
||||
def test_group_non_group_conversion(self):
|
||||
|
||||
warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion"))
|
||||
|
||||
convert_to_group_or_ledger(warehouse.name)
|
||||
|
||||
@@ -26,9 +26,7 @@ class Warehouse(NestedSet):
|
||||
|
||||
def onload(self):
|
||||
"""load account name for General Ledger Report"""
|
||||
if self.company and cint(
|
||||
frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")
|
||||
):
|
||||
if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")):
|
||||
account = self.account or get_warehouse_account(self)
|
||||
|
||||
if account:
|
||||
|
||||
@@ -189,19 +189,14 @@ def set_valuation_rate(out, args):
|
||||
|
||||
|
||||
def update_bin_details(args, out, doc):
|
||||
if (
|
||||
args.get("doctype") == "Material Request"
|
||||
and args.get("material_request_type") == "Material Transfer"
|
||||
):
|
||||
if args.get("doctype") == "Material Request" and args.get("material_request_type") == "Material Transfer":
|
||||
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
|
||||
|
||||
elif out.get("warehouse"):
|
||||
company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None
|
||||
|
||||
# calculate company_total_stock only for po
|
||||
bin_details = get_bin_details(
|
||||
args.item_code, out.warehouse, company, include_child_warehouses=True
|
||||
)
|
||||
bin_details = get_bin_details(args.item_code, out.warehouse, company, include_child_warehouses=True)
|
||||
|
||||
out.update(bin_details)
|
||||
|
||||
@@ -308,9 +303,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
if not item:
|
||||
item = frappe.get_doc("Item", args.get("item_code"))
|
||||
|
||||
if (
|
||||
item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of})
|
||||
):
|
||||
if item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of}):
|
||||
item.update_template_tables()
|
||||
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
@@ -355,9 +348,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
# Set stock UOM in args, so that it can be used while fetching item price
|
||||
args.stock_uom = item.stock_uom
|
||||
|
||||
if args.get("batch_no") and item.name != frappe.get_cached_value(
|
||||
"Batch", args.get("batch_no"), "item"
|
||||
):
|
||||
if args.get("batch_no") and item.name != frappe.get_cached_value("Batch", args.get("batch_no"), "item"):
|
||||
args["batch_no"] = ""
|
||||
|
||||
out = frappe._dict(
|
||||
@@ -378,9 +369,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"provisional_expense_account": get_provisional_account(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
),
|
||||
"cost_center": get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
),
|
||||
"cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults),
|
||||
"has_serial_no": item.has_serial_no,
|
||||
"has_batch_no": item.has_batch_no,
|
||||
"batch_no": args.get("batch_no"),
|
||||
@@ -406,9 +395,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
if args.get("doctype") in ["Sales Order", "Sales Invoice"]
|
||||
else 0,
|
||||
"is_fixed_asset": item.is_fixed_asset,
|
||||
"last_purchase_rate": item.last_purchase_rate
|
||||
if args.get("doctype") in ["Purchase Order"]
|
||||
else 0,
|
||||
"last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0,
|
||||
"transaction_date": args.get("transaction_date"),
|
||||
"against_blanket_order": args.get("against_blanket_order"),
|
||||
"bom_no": item.get("default_bom"),
|
||||
@@ -544,9 +531,7 @@ def get_barcode_data(items_list):
|
||||
|
||||
itemwise_barcode = {}
|
||||
for item in items_list:
|
||||
barcodes = frappe.db.get_all(
|
||||
"Item Barcode", filters={"parent": item.item_code}, fields="barcode"
|
||||
)
|
||||
barcodes = frappe.db.get_all("Item Barcode", filters={"parent": item.item_code}, fields="barcode")
|
||||
|
||||
for barcode in barcodes:
|
||||
if item.item_code not in itemwise_barcode:
|
||||
@@ -566,13 +551,13 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
|
||||
if item_rates is None:
|
||||
item_rates = {}
|
||||
|
||||
if isinstance(item_codes, (str,)):
|
||||
if isinstance(item_codes, str):
|
||||
item_codes = json.loads(item_codes)
|
||||
|
||||
if isinstance(item_rates, (str,)):
|
||||
if isinstance(item_rates, str):
|
||||
item_rates = json.loads(item_rates)
|
||||
|
||||
if isinstance(item_tax_templates, (str,)):
|
||||
if isinstance(item_tax_templates, str):
|
||||
item_tax_templates = json.loads(item_tax_templates)
|
||||
|
||||
for item_code in item_codes:
|
||||
@@ -640,9 +625,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
taxes_with_no_validity.append(tax)
|
||||
|
||||
if taxes_with_validity:
|
||||
taxes = sorted(
|
||||
taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True
|
||||
)
|
||||
taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True)
|
||||
else:
|
||||
taxes = taxes_with_no_validity
|
||||
|
||||
@@ -795,7 +778,7 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
|
||||
|
||||
elif not cost_center and args.get("item_code") and company:
|
||||
for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]:
|
||||
path = "erpnext.stock.get_item_details.{0}".format(method)
|
||||
path = f"erpnext.stock.get_item_details.{method}"
|
||||
data = frappe.get_attr(path)(args.get("item_code"), company)
|
||||
|
||||
if data and (data.selling_cost_center or data.buying_cost_center):
|
||||
@@ -810,11 +793,7 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
|
||||
if not cost_center and args.get("cost_center"):
|
||||
cost_center = args.get("cost_center")
|
||||
|
||||
if (
|
||||
company
|
||||
and cost_center
|
||||
and frappe.get_cached_value("Cost Center", cost_center, "company") != company
|
||||
):
|
||||
if company and cost_center and frappe.get_cached_value("Cost Center", cost_center, "company") != company:
|
||||
return None
|
||||
|
||||
if not cost_center and company:
|
||||
@@ -824,11 +803,7 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
|
||||
|
||||
|
||||
def get_default_supplier(args, item, item_group, brand):
|
||||
return (
|
||||
item.get("default_supplier")
|
||||
or item_group.get("default_supplier")
|
||||
or brand.get("default_supplier")
|
||||
)
|
||||
return item.get("default_supplier") or item_group.get("default_supplier") or brand.get("default_supplier")
|
||||
|
||||
|
||||
def get_price_list_rate(args, item_doc, out=None):
|
||||
@@ -863,9 +838,7 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
if not price_list_rate:
|
||||
return out
|
||||
|
||||
out.price_list_rate = (
|
||||
flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)
|
||||
)
|
||||
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)
|
||||
|
||||
if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
|
||||
return out
|
||||
@@ -880,9 +853,7 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
|
||||
def insert_item_price(args):
|
||||
"""Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
|
||||
if frappe.db.get_value(
|
||||
"Price List", args.price_list, "currency", cache=True
|
||||
) == args.currency and cint(
|
||||
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint(
|
||||
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
|
||||
):
|
||||
if frappe.has_permission("Item Price", "write"):
|
||||
@@ -904,7 +875,9 @@ def insert_item_price(args):
|
||||
):
|
||||
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
|
||||
frappe.msgprint(
|
||||
_("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list),
|
||||
_("Item Price updated for {0} in Price List {1}").format(
|
||||
args.item_code, args.price_list
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
@@ -1044,11 +1017,7 @@ def validate_conversion_rate(args, meta):
|
||||
if not args.conversion_rate and args.currency == company_currency:
|
||||
args.conversion_rate = 1.0
|
||||
|
||||
if (
|
||||
not args.ignore_conversion_rate
|
||||
and args.conversion_rate == 1
|
||||
and args.currency != company_currency
|
||||
):
|
||||
if not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency != company_currency:
|
||||
args.conversion_rate = (
|
||||
get_exchange_rate(args.currency, company_currency, args.transaction_date, "for_buying") or 1.0
|
||||
)
|
||||
@@ -1083,7 +1052,9 @@ def validate_conversion_rate(args, meta):
|
||||
if meta.get_field("plc_conversion_rate"):
|
||||
args.plc_conversion_rate = flt(
|
||||
args.plc_conversion_rate,
|
||||
get_field_precision(meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args})),
|
||||
get_field_precision(
|
||||
meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args})
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1264,9 +1235,7 @@ def get_serial_no_details(item_code, warehouse, stock_qty, serial_no):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bin_details_and_serial_nos(
|
||||
item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None
|
||||
):
|
||||
def get_bin_details_and_serial_nos(item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None):
|
||||
bin_details_and_serial_nos = {}
|
||||
bin_details_and_serial_nos.update(get_bin_details(item_code, warehouse))
|
||||
if flt(stock_qty) > 0:
|
||||
@@ -1276,9 +1245,7 @@ def get_bin_details_and_serial_nos(
|
||||
bin_details_and_serial_nos.update({"serial_no": serial_no})
|
||||
return bin_details_and_serial_nos
|
||||
|
||||
bin_details_and_serial_nos.update(
|
||||
get_serial_no_details(item_code, warehouse, stock_qty, serial_no)
|
||||
)
|
||||
bin_details_and_serial_nos.update(get_serial_no_details(item_code, warehouse, stock_qty, serial_no))
|
||||
|
||||
return bin_details_and_serial_nos
|
||||
|
||||
@@ -1390,9 +1357,7 @@ def get_price_list_currency_and_exchange_rate(args):
|
||||
company_currency = get_company_currency(args.company)
|
||||
|
||||
if (not plc_conversion_rate) or (
|
||||
price_list_currency
|
||||
and args.price_list_currency
|
||||
and price_list_currency != args.price_list_currency
|
||||
price_list_currency and args.price_list_currency and price_list_currency != args.price_list_currency
|
||||
):
|
||||
# cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate
|
||||
plc_conversion_rate = (
|
||||
@@ -1414,9 +1379,7 @@ def get_price_list_currency_and_exchange_rate(args):
|
||||
@frappe.whitelist()
|
||||
def get_default_bom(item_code=None):
|
||||
def _get_bom(item):
|
||||
bom = frappe.get_all(
|
||||
"BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1
|
||||
)
|
||||
bom = frappe.get_all("BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1)
|
||||
return bom[0].name if bom else None
|
||||
|
||||
if not item_code:
|
||||
@@ -1456,7 +1419,7 @@ def get_valuation_rate(item_code, company, warehouse=None):
|
||||
pi_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
valuation_rate = (
|
||||
frappe.qb.from_(pi_item)
|
||||
.select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor)))
|
||||
.select(Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor))
|
||||
.where((pi_item.docstatus == 1) & (pi_item.item_code == item_code))
|
||||
).run()
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ def _reorder_item():
|
||||
item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group)
|
||||
)
|
||||
else:
|
||||
projected_qty = flt(
|
||||
item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)
|
||||
)
|
||||
projected_qty = flt(item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse))
|
||||
|
||||
if (reorder_level or reorder_qty) and projected_qty < reorder_level:
|
||||
deficiency = reorder_level - projected_qty
|
||||
@@ -188,13 +186,12 @@ def get_item_warehouse_projected_qty(items_to_consider):
|
||||
|
||||
for item_code, warehouse, projected_qty in frappe.db.sql(
|
||||
"""select item_code, warehouse, projected_qty
|
||||
from tabBin where item_code in ({0})
|
||||
from tabBin where item_code in ({})
|
||||
and (warehouse != '' and warehouse is not null)""".format(
|
||||
", ".join(["%s"] * len(items_to_consider))
|
||||
),
|
||||
items_to_consider,
|
||||
):
|
||||
|
||||
if item_code not in item_warehouse_projected_qty:
|
||||
item_warehouse_projected_qty.setdefault(item_code, {})
|
||||
|
||||
@@ -242,7 +239,9 @@ def create_material_request(material_requests):
|
||||
{
|
||||
"company": company,
|
||||
"transaction_date": nowdate(),
|
||||
"material_request_type": "Material Transfer" if request_type == "Transfer" else request_type,
|
||||
"material_request_type": "Material Transfer"
|
||||
if request_type == "Transfer"
|
||||
else request_type,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -257,7 +256,9 @@ def create_material_request(material_requests):
|
||||
if uom != item.stock_uom:
|
||||
conversion_factor = (
|
||||
frappe.db.get_value(
|
||||
"UOM Conversion Detail", {"parent": item.name, "uom": uom}, "conversion_factor"
|
||||
"UOM Conversion Detail",
|
||||
{"parent": item.name, "uom": uom},
|
||||
"conversion_factor",
|
||||
)
|
||||
or 1.0
|
||||
)
|
||||
@@ -323,9 +324,7 @@ def send_email_notification(company_wise_mr):
|
||||
|
||||
msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg
|
||||
)
|
||||
frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg)
|
||||
|
||||
|
||||
def get_email_list(company):
|
||||
@@ -378,7 +377,7 @@ def notify_errors(exceptions_list):
|
||||
for exception in exceptions_list:
|
||||
try:
|
||||
exception = json.loads(exception)
|
||||
error_message = """<div class='small text-muted'>{0}</div><br>""".format(
|
||||
error_message = """<div class='small text-muted'>{}</div><br>""".format(
|
||||
_(exception.get("message"))
|
||||
)
|
||||
content += error_message
|
||||
|
||||
@@ -28,15 +28,15 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_columns():
|
||||
return (
|
||||
[_("Item") + ":Link/Item:150"]
|
||||
+ [_("Item Name") + "::150"]
|
||||
+ [_("Batch") + ":Link/Batch:150"]
|
||||
+ [_("Stock UOM") + ":Link/UOM:100"]
|
||||
+ [_("Quantity") + ":Float:100"]
|
||||
+ [_("Expires On") + ":Date:100"]
|
||||
+ [_("Expiry (In Days)") + ":Int:130"]
|
||||
)
|
||||
return [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::150",
|
||||
_("Batch") + ":Link/Batch:150",
|
||||
_("Stock UOM") + ":Link/UOM:100",
|
||||
_("Quantity") + ":Float:100",
|
||||
_("Expires On") + ":Date:100",
|
||||
_("Expiry (In Days)") + ":Int:130",
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
@@ -76,9 +76,7 @@ def get_batch_details(filters):
|
||||
.where(
|
||||
(batch.disabled == 0)
|
||||
& (batch.batch_qty > 0)
|
||||
& (
|
||||
(Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"])
|
||||
)
|
||||
& ((Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"]))
|
||||
)
|
||||
.orderby(batch.creation)
|
||||
)
|
||||
|
||||
@@ -69,18 +69,18 @@ def execute(filters=None):
|
||||
def get_columns(filters):
|
||||
"""return columns based on filters"""
|
||||
|
||||
columns = (
|
||||
[_("Item") + ":Link/Item:100"]
|
||||
+ [_("Item Name") + "::150"]
|
||||
+ [_("Description") + "::150"]
|
||||
+ [_("Warehouse") + ":Link/Warehouse:100"]
|
||||
+ [_("Batch") + ":Link/Batch:100"]
|
||||
+ [_("Opening Qty") + ":Float:90"]
|
||||
+ [_("In Qty") + ":Float:80"]
|
||||
+ [_("Out Qty") + ":Float:80"]
|
||||
+ [_("Balance Qty") + ":Float:90"]
|
||||
+ [_("UOM") + "::90"]
|
||||
)
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:100",
|
||||
_("Item Name") + "::150",
|
||||
_("Description") + "::150",
|
||||
_("Warehouse") + ":Link/Warehouse:100",
|
||||
_("Batch") + ":Link/Batch:100",
|
||||
_("Opening Qty") + ":Float:90",
|
||||
_("In Qty") + ":Float:80",
|
||||
_("Out Qty") + ":Float:80",
|
||||
_("Balance Qty") + ":Float:90",
|
||||
_("UOM") + "::90",
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -141,7 +141,9 @@ def get_item_warehouse_batch_map(filters, float_precision):
|
||||
)
|
||||
elif d.posting_date >= from_date and d.posting_date <= to_date:
|
||||
if flt(d.actual_qty) > 0:
|
||||
qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision)
|
||||
qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(
|
||||
d.actual_qty, float_precision
|
||||
)
|
||||
else:
|
||||
qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs(
|
||||
flt(d.actual_qty, float_precision)
|
||||
@@ -154,9 +156,7 @@ def get_item_warehouse_batch_map(filters, float_precision):
|
||||
|
||||
def get_item_details(filters):
|
||||
item_map = {}
|
||||
for d in (frappe.qb.from_("Item").select("name", "item_name", "description", "stock_uom")).run(
|
||||
as_dict=1
|
||||
):
|
||||
for d in (frappe.qb.from_("Item").select("name", "item_name", "description", "stock_uom")).run(as_dict=1):
|
||||
item_map.setdefault(d.name, d)
|
||||
|
||||
return item_map
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import datetime
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List, Tuple, Union
|
||||
from typing import Union
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -13,15 +13,15 @@ from erpnext.accounts.report.general_ledger.general_ledger import get_gl_entries
|
||||
|
||||
Filters = frappe._dict
|
||||
Row = frappe._dict
|
||||
Data = List[Row]
|
||||
Columns = List[Dict[str, str]]
|
||||
Data = list[Row]
|
||||
Columns = list[dict[str, str]]
|
||||
DateTime = Union[datetime.date, datetime.datetime]
|
||||
FilteredEntries = List[Dict[str, Union[str, float, DateTime, None]]]
|
||||
ItemGroupsDict = Dict[Tuple[int, int], Dict[str, Union[str, int]]]
|
||||
SVDList = List[frappe._dict]
|
||||
FilteredEntries = list[dict[str, str | float | DateTime | None]]
|
||||
ItemGroupsDict = dict[tuple[int, int], dict[str, str | int]]
|
||||
SVDList = list[frappe._dict]
|
||||
|
||||
|
||||
def execute(filters: Filters) -> Tuple[Columns, Data]:
|
||||
def execute(filters: Filters) -> tuple[Columns, Data]:
|
||||
update_filters_with_account(filters)
|
||||
validate_filters(filters)
|
||||
columns = get_columns()
|
||||
@@ -158,7 +158,7 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None:
|
||||
item.item_group = ig_map[item.get("item_code")]
|
||||
|
||||
|
||||
def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]:
|
||||
def get_item_groups_map(svd_list: SVDList) -> dict[str, str]:
|
||||
item_codes = set(i["item_code"] for i in svd_list)
|
||||
ig_list = frappe.get_list(
|
||||
"Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)]
|
||||
@@ -168,9 +168,7 @@ def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]:
|
||||
|
||||
def get_item_groups_dict() -> ItemGroupsDict:
|
||||
item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt"))
|
||||
return {
|
||||
(i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list
|
||||
}
|
||||
return {(i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list}
|
||||
|
||||
|
||||
def update_leveled_dict(leveled_dict: OrderedDict) -> None:
|
||||
|
||||
@@ -13,7 +13,7 @@ def execute(filters=None, consolidated=False):
|
||||
return data, columns
|
||||
|
||||
|
||||
class DelayedItemReport(object):
|
||||
class DelayedItemReport:
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class DelayedOrderReport(DelayedItemReport):
|
||||
return self.get_columns(), self.get_data(consolidated=True) or []
|
||||
|
||||
def get_data(self, consolidated=False):
|
||||
data = super(DelayedOrderReport, self).get_data(consolidated) or []
|
||||
data = super().get_data(consolidated) or []
|
||||
|
||||
so_list = []
|
||||
result = []
|
||||
|
||||
@@ -40,19 +40,18 @@ def get_data(filters):
|
||||
|
||||
|
||||
def get_stock_ledger_entries(filters):
|
||||
|
||||
sle_filters = {"is_cancelled": 0}
|
||||
|
||||
if filters.warehouse:
|
||||
children = get_descendants_of("Warehouse", filters.warehouse)
|
||||
sle_filters["warehouse"] = ("in", children + [filters.warehouse])
|
||||
sle_filters["warehouse"] = ("in", [*children, filters.warehouse])
|
||||
|
||||
if filters.item_code:
|
||||
sle_filters["item_code"] = filters.item_code
|
||||
elif filters.get("item_group"):
|
||||
item_group = filters.get("item_group")
|
||||
children = get_descendants_of("Item Group", item_group)
|
||||
item_group_filter = {"item_group": ("in", children + [item_group])}
|
||||
item_group_filter = {"item_group": ("in", [*children, item_group])}
|
||||
sle_filters["item_code"] = (
|
||||
"in",
|
||||
frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
|
||||
|
||||
@@ -27,7 +27,7 @@ def get_data(filters):
|
||||
|
||||
def validate_data(itewise_balance_qty):
|
||||
res = []
|
||||
for key, data in itewise_balance_qty.items():
|
||||
for _key, data in itewise_balance_qty.items():
|
||||
row = get_incorrect_data(data)
|
||||
if row:
|
||||
res.append(row)
|
||||
|
||||
@@ -43,11 +43,9 @@ def prepare_serial_nos(data):
|
||||
def get_incorrect_serial_nos(serial_nos_data):
|
||||
result = []
|
||||
|
||||
total_value = frappe._dict(
|
||||
{"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))}
|
||||
)
|
||||
total_value = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))})
|
||||
|
||||
for serial_no, data in serial_nos_data.items():
|
||||
for _serial_no, data in serial_nos_data.items():
|
||||
total_dict = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Total"))})
|
||||
|
||||
if check_incorrect_serial_data(data, total_dict):
|
||||
|
||||
@@ -16,9 +16,7 @@ from erpnext.stock.utils import get_stock_value_on
|
||||
def execute(filters=None):
|
||||
if not erpnext.is_perpetual_inventory_enabled(filters.company):
|
||||
frappe.throw(
|
||||
_("Perpetual inventory required for the company {0} to view this report.").format(
|
||||
filters.company
|
||||
)
|
||||
_("Perpetual inventory required for the company {0} to view this report.").format(filters.company)
|
||||
)
|
||||
|
||||
data = get_data(filters)
|
||||
@@ -83,9 +81,7 @@ def get_data(report_filters):
|
||||
|
||||
closing_date = add_days(from_date, -1)
|
||||
for key, stock_data in voucher_wise_dict.items():
|
||||
prev_stock_value = get_stock_value_on(
|
||||
posting_date=closing_date, item_code=key[0], warehouses=key[1]
|
||||
)
|
||||
prev_stock_value = get_stock_value_on(posting_date=closing_date, item_code=key[0], warehouses=key[1])
|
||||
for data in stock_data:
|
||||
expected_stock_value = prev_stock_value + data.stock_value_difference
|
||||
if abs(data.stock_value - expected_stock_value) > 0.1:
|
||||
|
||||
@@ -111,9 +111,7 @@ def get_price_list():
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in price_list:
|
||||
d.update(
|
||||
{"price": "{0} {1} - {2}".format(d.currency, round(d.price_list_rate, 2), d.price_list)}
|
||||
)
|
||||
d.update({"price": f"{d.currency} {round(d.price_list_rate, 2)} - {d.price_list}"})
|
||||
d.pop("currency")
|
||||
d.pop("price_list_rate")
|
||||
d.pop("price_list")
|
||||
|
||||
@@ -24,9 +24,7 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for item in items:
|
||||
total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(
|
||||
delivered_item_map.get(item.name, 0)
|
||||
)
|
||||
total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name, 0))
|
||||
avg_daily_outgoing = flt(total_outgoing / diff, float_precision)
|
||||
reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List, Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -14,7 +13,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
Filters = frappe._dict
|
||||
|
||||
|
||||
def execute(filters: Filters = None) -> Tuple:
|
||||
def execute(filters: Filters = None) -> tuple:
|
||||
to_date = filters["to_date"]
|
||||
columns = get_columns(filters)
|
||||
|
||||
@@ -26,14 +25,14 @@ def execute(filters: Filters = None) -> Tuple:
|
||||
return columns, data, None, chart_data
|
||||
|
||||
|
||||
def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]:
|
||||
def format_report_data(filters: Filters, item_details: dict, to_date: str) -> list[dict]:
|
||||
"Returns ordered, formatted data with ranges."
|
||||
_func = itemgetter(1)
|
||||
data = []
|
||||
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||
|
||||
for item, item_dict in item_details.items():
|
||||
for _item, item_dict in item_details.items():
|
||||
if not flt(item_dict.get("total_qty"), precision):
|
||||
continue
|
||||
|
||||
@@ -74,12 +73,12 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
|
||||
return data
|
||||
|
||||
|
||||
def get_average_age(fifo_queue: List, to_date: str) -> float:
|
||||
def get_average_age(fifo_queue: list, to_date: str) -> float:
|
||||
batch_age = age_qty = total_qty = 0.0
|
||||
for batch in fifo_queue:
|
||||
batch_age = date_diff(to_date, batch[1])
|
||||
|
||||
if isinstance(batch[0], (int, float)):
|
||||
if isinstance(batch[0], int | float):
|
||||
age_qty += batch_age * batch[0]
|
||||
total_qty += batch[0]
|
||||
else:
|
||||
@@ -89,8 +88,7 @@ def get_average_age(fifo_queue: List, to_date: str) -> float:
|
||||
return flt(age_qty / total_qty, 2) if total_qty else 0.0
|
||||
|
||||
|
||||
def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
|
||||
|
||||
def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple:
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||
|
||||
range1 = range2 = range3 = above_range3 = 0.0
|
||||
@@ -111,7 +109,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D
|
||||
return range1, range2, range3, above_range3
|
||||
|
||||
|
||||
def get_columns(filters: Filters) -> List[Dict]:
|
||||
def get_columns(filters: Filters) -> list[dict]:
|
||||
range_columns = []
|
||||
setup_ageing_columns(filters, range_columns)
|
||||
columns = [
|
||||
@@ -169,7 +167,7 @@ def get_columns(filters: Filters) -> List[Dict]:
|
||||
return columns
|
||||
|
||||
|
||||
def get_chart_data(data: List, filters: Filters) -> Dict:
|
||||
def get_chart_data(data: list, filters: Filters) -> dict:
|
||||
if not data:
|
||||
return []
|
||||
|
||||
@@ -193,7 +191,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict:
|
||||
}
|
||||
|
||||
|
||||
def setup_ageing_columns(filters: Filters, range_columns: List):
|
||||
def setup_ageing_columns(filters: Filters, range_columns: list):
|
||||
ranges = [
|
||||
f"0 - {filters['range1']}",
|
||||
f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}",
|
||||
@@ -205,23 +203,21 @@ def setup_ageing_columns(filters: Filters, range_columns: List):
|
||||
add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname)
|
||||
|
||||
|
||||
def add_column(
|
||||
range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140
|
||||
):
|
||||
def add_column(range_columns: list, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140):
|
||||
range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width))
|
||||
|
||||
|
||||
class FIFOSlots:
|
||||
"Returns FIFO computed slots of inwarded stock as per date."
|
||||
|
||||
def __init__(self, filters: Dict = None, sle: List = None):
|
||||
def __init__(self, filters: dict | None = None, sle: list | None = None):
|
||||
self.item_details = {}
|
||||
self.transferred_item_details = {}
|
||||
self.serial_no_batch_purchase_details = {}
|
||||
self.filters = filters
|
||||
self.sle = sle
|
||||
|
||||
def generate(self) -> Dict:
|
||||
def generate(self) -> dict:
|
||||
"""
|
||||
Returns dict of the foll.g structure:
|
||||
Key = Item A / (Item A, Warehouse A)
|
||||
@@ -257,7 +253,7 @@ class FIFOSlots:
|
||||
|
||||
return self.item_details
|
||||
|
||||
def __init_key_stores(self, row: Dict) -> Tuple:
|
||||
def __init_key_stores(self, row: dict) -> tuple:
|
||||
"Initialise keys and FIFO Queue."
|
||||
|
||||
key = (row.name, row.warehouse)
|
||||
@@ -269,9 +265,7 @@ class FIFOSlots:
|
||||
|
||||
return key, fifo_queue, transferred_item_key
|
||||
|
||||
def __compute_incoming_stock(
|
||||
self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List
|
||||
):
|
||||
def __compute_incoming_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
|
||||
"Update FIFO Queue on inward stock."
|
||||
|
||||
transfer_data = self.transferred_item_details.get(transfer_key)
|
||||
@@ -297,9 +291,7 @@ class FIFOSlots:
|
||||
self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date)
|
||||
fifo_queue.append([serial_no, row.posting_date])
|
||||
|
||||
def __compute_outgoing_stock(
|
||||
self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List
|
||||
):
|
||||
def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
|
||||
"Update FIFO Queue on outward stock."
|
||||
if serial_nos:
|
||||
fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos]
|
||||
@@ -325,7 +317,7 @@ class FIFOSlots:
|
||||
self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
|
||||
qty_to_pop = 0
|
||||
|
||||
def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict):
|
||||
def __adjust_incoming_transfer_qty(self, transfer_data: dict, fifo_queue: list, row: dict):
|
||||
"Add previously removed stock back to FIFO Queue."
|
||||
transfer_qty_to_pop = flt(row.actual_qty)
|
||||
|
||||
@@ -352,7 +344,7 @@ class FIFOSlots:
|
||||
add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]])
|
||||
transfer_qty_to_pop = 0
|
||||
|
||||
def __update_balances(self, row: Dict, key: Union[Tuple, str]):
|
||||
def __update_balances(self, row: dict, key: tuple | str):
|
||||
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
|
||||
|
||||
if "total_qty" not in self.item_details[key]:
|
||||
@@ -362,7 +354,7 @@ class FIFOSlots:
|
||||
|
||||
self.item_details[key]["has_serial_no"] = row.has_serial_no
|
||||
|
||||
def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict:
|
||||
def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
|
||||
"Aggregate Item-Wh wise data into single Item entry."
|
||||
item_aggregated_data = {}
|
||||
for key, row in wh_wise_data.items():
|
||||
@@ -370,7 +362,12 @@ class FIFOSlots:
|
||||
if not item_aggregated_data.get(item):
|
||||
item_aggregated_data.setdefault(
|
||||
item,
|
||||
{"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0},
|
||||
{
|
||||
"details": frappe._dict(),
|
||||
"fifo_queue": [],
|
||||
"qty_after_transaction": 0.0,
|
||||
"total_qty": 0.0,
|
||||
},
|
||||
)
|
||||
item_row = item_aggregated_data.get(item)
|
||||
item_row["details"].update(row["details"])
|
||||
@@ -381,7 +378,7 @@ class FIFOSlots:
|
||||
|
||||
return item_aggregated_data
|
||||
|
||||
def __get_stock_ledger_entries(self) -> List[Dict]:
|
||||
def __get_stock_ledger_entries(self) -> list[dict]:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
item = self.__get_item_query() # used as derived table in sle query
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
@@ -48,12 +47,10 @@ def get_columns(filters):
|
||||
|
||||
ranges = get_period_date_ranges(filters)
|
||||
|
||||
for dummy, end_date in ranges:
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
|
||||
columns.append(
|
||||
{"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120}
|
||||
)
|
||||
columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120})
|
||||
|
||||
return columns
|
||||
|
||||
@@ -67,7 +64,7 @@ def get_period_date_ranges(filters):
|
||||
increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(filters.range, 1)
|
||||
|
||||
periodic_daterange = []
|
||||
for dummy in range(1, 53, increment):
|
||||
for _dummy in range(1, 53, increment):
|
||||
if filters.range == "Weekly":
|
||||
period_end_date = from_date + relativedelta(days=6)
|
||||
else:
|
||||
@@ -116,9 +113,7 @@ def get_period(posting_date, filters):
|
||||
elif filters.range == "Monthly":
|
||||
period = _(str(months[posting_date.month - 1])) + " " + str(posting_date.year)
|
||||
elif filters.range == "Quarterly":
|
||||
period = _("Quarter {0} {1}").format(
|
||||
str(((posting_date.month - 1) // 3) + 1), str(posting_date.year)
|
||||
)
|
||||
period = _("Quarter {0} {1}").format(str(((posting_date.month - 1) // 3) + 1), str(posting_date.year))
|
||||
else:
|
||||
year = get_fiscal_year(posting_date, company=filters.company)
|
||||
period = str(year[2])
|
||||
@@ -188,15 +183,13 @@ def get_periodic_data(entry, filters):
|
||||
periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0)
|
||||
|
||||
periodic_data[d.item_code]["balance"][d.warehouse] += value
|
||||
periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][
|
||||
d.warehouse
|
||||
]
|
||||
periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][d.warehouse]
|
||||
|
||||
return periodic_data
|
||||
|
||||
|
||||
def fill_intermediate_periods(
|
||||
periodic_data, item_code: str, current_period: str, all_periods: List[str]
|
||||
periodic_data, item_code: str, current_period: str, all_periods: list[str]
|
||||
) -> None:
|
||||
"""There might be intermediate periods where no stock ledger entry exists, copy previous previous data.
|
||||
|
||||
@@ -235,7 +228,7 @@ def get_data(filters):
|
||||
|
||||
today = getdate()
|
||||
|
||||
for dummy, item_data in item_details.items():
|
||||
for _dummy, item_data in item_details.items():
|
||||
row = {
|
||||
"name": item_data.name,
|
||||
"item_name": item_data.item_name,
|
||||
@@ -273,7 +266,7 @@ def get_items(filters):
|
||||
item_filters = {"is_stock_item": 1}
|
||||
if item_group := filters.get("item_group"):
|
||||
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
|
||||
item_filters["item_group"] = ("in", children + [item_group])
|
||||
item_filters["item_group"] = ("in", [*children, item_group])
|
||||
if brand := filters.get("brand"):
|
||||
item_filters["brand"] = brand
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import datetime
|
||||
import frappe
|
||||
from frappe import _dict
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.data import add_to_date, get_datetime, getdate, nowdate
|
||||
from frappe.utils.data import add_to_date, getdate
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -55,7 +55,6 @@ class TestStockAnalyticsReport(FrappeTestCase):
|
||||
self.assertEqual(actual_buckets, expected_buckets)
|
||||
|
||||
def test_get_period_date_ranges(self):
|
||||
|
||||
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
|
||||
|
||||
ranges = get_period_date_ranges(filters)
|
||||
@@ -69,7 +68,6 @@ class TestStockAnalyticsReport(FrappeTestCase):
|
||||
self.assertEqual(ranges, expected_ranges)
|
||||
|
||||
def test_get_period_date_ranges_yearly(self):
|
||||
|
||||
filters = _dict(range="Yearly", from_date="2021-01-28", to_date="2021-02-06")
|
||||
|
||||
ranges = get_period_date_ranges(filters)
|
||||
|
||||
@@ -14,9 +14,7 @@ from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_ac
|
||||
def execute(filters=None):
|
||||
if not erpnext.is_perpetual_inventory_enabled(filters.company):
|
||||
frappe.throw(
|
||||
_("Perpetual inventory required for the company {0} to view this report.").format(
|
||||
filters.company
|
||||
)
|
||||
_("Perpetual inventory required for the company {0} to view this report.").format(filters.company)
|
||||
)
|
||||
|
||||
data = get_data(filters)
|
||||
@@ -34,7 +32,7 @@ def get_data(report_filters):
|
||||
"posting_date": ("<=", report_filters.as_on_date),
|
||||
}
|
||||
|
||||
currency_precision = get_currency_precision() or 2
|
||||
get_currency_precision() or 2
|
||||
stock_ledger_entries = get_stock_ledger_data(report_filters, filters)
|
||||
voucher_wise_gl_data = get_gl_data(report_filters, filters)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -20,27 +20,27 @@ from erpnext.stock.utils import add_additional_uom_columns
|
||||
|
||||
|
||||
class StockBalanceFilter(TypedDict):
|
||||
company: Optional[str]
|
||||
company: str | None
|
||||
from_date: str
|
||||
to_date: str
|
||||
item_group: Optional[str]
|
||||
item: Optional[str]
|
||||
warehouse: Optional[str]
|
||||
warehouse_type: Optional[str]
|
||||
include_uom: Optional[str] # include extra info in converted UOM
|
||||
item_group: str | None
|
||||
item: str | None
|
||||
warehouse: str | None
|
||||
warehouse_type: str | None
|
||||
include_uom: str | None # include extra info in converted UOM
|
||||
show_stock_ageing_data: bool
|
||||
show_variant_attributes: bool
|
||||
|
||||
|
||||
SLEntry = Dict[str, Any]
|
||||
SLEntry = dict[str, Any]
|
||||
|
||||
|
||||
def execute(filters: Optional[StockBalanceFilter] = None):
|
||||
def execute(filters: StockBalanceFilter | None = None):
|
||||
return StockBalanceReport(filters).run()
|
||||
|
||||
|
||||
class StockBalanceReport(object):
|
||||
def __init__(self, filters: Optional[StockBalanceFilter]) -> None:
|
||||
class StockBalanceReport:
|
||||
def __init__(self, filters: StockBalanceFilter | None) -> None:
|
||||
self.filters = filters
|
||||
self.from_date = getdate(filters.get("from_date"))
|
||||
self.to_date = getdate(filters.get("to_date"))
|
||||
@@ -48,7 +48,7 @@ class StockBalanceReport(object):
|
||||
self.start_from = None
|
||||
self.data = []
|
||||
self.columns = []
|
||||
self.sle_entries: List[SLEntry] = []
|
||||
self.sle_entries: list[SLEntry] = []
|
||||
self.set_company_currency()
|
||||
|
||||
def set_company_currency(self) -> None:
|
||||
@@ -104,7 +104,7 @@ class StockBalanceReport(object):
|
||||
if self.filters.get("show_variant_attributes"):
|
||||
variant_values = self.get_variant_values_for()
|
||||
|
||||
for key, report_data in self.item_warehouse_map.items():
|
||||
for _key, report_data in self.item_warehouse_map.items():
|
||||
if variant_data := variant_values.get(report_data.item_code):
|
||||
report_data.update(variant_data)
|
||||
|
||||
@@ -184,7 +184,6 @@ class StockBalanceReport(object):
|
||||
qty_dict.opening_val += value_diff
|
||||
|
||||
elif entry.posting_date >= self.from_date and entry.posting_date <= self.to_date:
|
||||
|
||||
if flt(qty_diff, self.float_precision) >= 0:
|
||||
qty_dict.in_qty += qty_diff
|
||||
qty_dict.in_val += value_diff
|
||||
@@ -230,7 +229,7 @@ class StockBalanceReport(object):
|
||||
|
||||
return tuple(group_by_key)
|
||||
|
||||
def get_closing_balance(self) -> List[Dict[str, Any]]:
|
||||
def get_closing_balance(self) -> list[dict[str, Any]]:
|
||||
if self.filters.get("ignore_closing_balance"):
|
||||
return []
|
||||
|
||||
@@ -325,7 +324,7 @@ class StockBalanceReport(object):
|
||||
def apply_items_filters(self, query, item_table) -> str:
|
||||
if item_group := self.filters.get("item_group"):
|
||||
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
|
||||
query = query.where(item_table.item_group.isin(children + [item_group]))
|
||||
query = query.where(item_table.item_group.isin([*children, item_group]))
|
||||
|
||||
for field in ["item_code", "brand"]:
|
||||
if not self.filters.get(field):
|
||||
@@ -542,7 +541,9 @@ class StockBalanceReport(object):
|
||||
frappe.qb.from_(sr)
|
||||
.select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type"))
|
||||
.where(
|
||||
(sr.docstatus == 1) & (sr.posting_date <= self.to_date) & (sr.purpose == "Opening Stock")
|
||||
(sr.docstatus == 1)
|
||||
& (sr.posting_date <= self.to_date)
|
||||
& (sr.purpose == "Opening Stock")
|
||||
)
|
||||
)
|
||||
).select("voucher_type", "name")
|
||||
@@ -568,7 +569,7 @@ class StockBalanceReport(object):
|
||||
|
||||
|
||||
def filter_items_with_no_transactions(
|
||||
iwb_map, float_precision: float, inventory_dimensions: list = None
|
||||
iwb_map, float_precision: float, inventory_dimensions: list | None = None
|
||||
):
|
||||
pop_keys = []
|
||||
for group_by_key in iwb_map:
|
||||
@@ -605,6 +606,6 @@ def filter_items_with_no_transactions(
|
||||
return iwb_map
|
||||
|
||||
|
||||
def get_variants_attributes() -> List[str]:
|
||||
def get_variants_attributes() -> list[str]:
|
||||
"""Return all item variant attributes."""
|
||||
return frappe.get_all("Item Attribute", pluck="name")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _dict
|
||||
@@ -32,12 +32,11 @@ class TestStockBalance(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def assertPartialDictEq(self, expected: Dict[str, Any], actual: Dict[str, Any]):
|
||||
def assertPartialDictEq(self, expected: dict[str, Any], actual: dict[str, Any]):
|
||||
for k, v in expected.items():
|
||||
self.assertEqual(v, actual[k], msg=f"{expected=}\n{actual=}")
|
||||
|
||||
def generate_stock_ledger(self, item_code: str, movements):
|
||||
|
||||
for movement in map(_dict, movements):
|
||||
if "to_warehouse" not in movement:
|
||||
movement.to_warehouse = "_Test Warehouse - _TC"
|
||||
@@ -128,7 +127,6 @@ class TestStockBalance(FrappeTestCase):
|
||||
self.assertPartialDictEq({"opening_qty": 6, "in_qty": 0}, rows[0])
|
||||
|
||||
def test_uom_converted_info(self):
|
||||
|
||||
self.item.append("uoms", {"conversion_factor": 5, "uom": "Box"})
|
||||
self.item.save()
|
||||
|
||||
@@ -167,8 +165,6 @@ class TestStockBalance(FrappeTestCase):
|
||||
variant.save()
|
||||
|
||||
self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)])
|
||||
rows = stock_balance(
|
||||
self.filters.update({"show_variant_attributes": 1, "item_code": variant.name})
|
||||
)
|
||||
rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": variant.name}))
|
||||
self.assertPartialDictEq(attributes, rows[0])
|
||||
self.assertInvariants(rows)
|
||||
|
||||
@@ -437,11 +437,8 @@ def get_opening_balance(filters, columns, sl_entries):
|
||||
def get_warehouse_condition(warehouse):
|
||||
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
|
||||
if warehouse_details:
|
||||
return (
|
||||
" exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)"
|
||||
% (warehouse_details.lft, warehouse_details.rgt)
|
||||
)
|
||||
return f" exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
|
||||
|
||||
return ""
|
||||
|
||||
@@ -452,22 +449,17 @@ def get_item_group_condition(item_group, item_table=None):
|
||||
if item_table:
|
||||
ig = frappe.qb.DocType("Item Group")
|
||||
return item_table.item_group.isin(
|
||||
(
|
||||
frappe.qb.from_(ig)
|
||||
.select(ig.name)
|
||||
.where(
|
||||
(ig.lft >= item_group_details.lft)
|
||||
& (ig.rgt <= item_group_details.rgt)
|
||||
& (item_table.item_group == ig.name)
|
||||
)
|
||||
frappe.qb.from_(ig)
|
||||
.select(ig.name)
|
||||
.where(
|
||||
(ig.lft >= item_group_details.lft)
|
||||
& (ig.rgt <= item_group_details.rgt)
|
||||
& (item_table.item_group == ig.name)
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
"item.item_group in (select ig.name from `tabItem Group` ig \
|
||||
where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)"
|
||||
% (item_group_details.lft, item_group_details.rgt)
|
||||
)
|
||||
return f"item.item_group in (select ig.name from `tabItem Group` ig \
|
||||
where ig.lft >= {item_group_details.lft} and ig.rgt <= {item_group_details.rgt} and item.item_group = ig.name)"
|
||||
|
||||
|
||||
def check_inventory_dimension_filters_applied(filters) -> bool:
|
||||
|
||||
@@ -228,7 +228,7 @@ def get_data(filters=None):
|
||||
return data
|
||||
|
||||
|
||||
def get_item_warehouse_combinations(filters: dict = None) -> dict:
|
||||
def get_item_warehouse_combinations(filters: dict | None = None) -> dict:
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
@@ -284,7 +284,5 @@ def has_difference(row, precision, difference_in, valuation_method):
|
||||
return True
|
||||
elif difference_in == "Valuation" and valuation_diff:
|
||||
return True
|
||||
elif difference_in not in ["Qty", "Value", "Valuation"] and (
|
||||
qty_diff or value_diff or valuation_diff
|
||||
):
|
||||
elif difference_in not in ["Qty", "Value", "Valuation"] and (qty_diff or value_diff or valuation_diff):
|
||||
return True
|
||||
|
||||
@@ -250,9 +250,7 @@ def get_bin_list(filters):
|
||||
query = query.where(bin.item_code == filters.item_code)
|
||||
|
||||
if filters.warehouse:
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
warehouse_details = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1)
|
||||
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
@@ -286,7 +284,9 @@ def get_item_map(item_code, include_uom):
|
||||
(item.is_stock_item == 1)
|
||||
& (item.disabled == 0)
|
||||
& (
|
||||
(item.end_of_life > today()) | (item.end_of_life.isnull()) | (item.end_of_life == "0000-00-00")
|
||||
(item.end_of_life > today())
|
||||
| (item.end_of_life.isnull())
|
||||
| (item.end_of_life == "0000-00-00")
|
||||
)
|
||||
& (ExistsCriterion(frappe.qb.from_(bin).select(bin.name).where(bin.item_code == item.name)))
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ def execute(filters=None):
|
||||
total_qty = total_amount = 0.0
|
||||
if consumed_details.get(item_code):
|
||||
for cd in consumed_details.get(item_code):
|
||||
|
||||
if cd.voucher_no not in material_transfer_vouchers:
|
||||
if cd.voucher_type in ["Delivery Note", "Sales Invoice"]:
|
||||
delivered_qty += abs(flt(cd.actual_qty))
|
||||
@@ -54,19 +53,19 @@ def execute(filters=None):
|
||||
def get_columns(filters):
|
||||
"""return columns based on filters"""
|
||||
|
||||
columns = (
|
||||
[_("Item") + ":Link/Item:100"]
|
||||
+ [_("Item Name") + "::100"]
|
||||
+ [_("Description") + "::150"]
|
||||
+ [_("UOM") + ":Link/UOM:90"]
|
||||
+ [_("Consumed Qty") + ":Float:110"]
|
||||
+ [_("Consumed Amount") + ":Currency:130"]
|
||||
+ [_("Delivered Qty") + ":Float:110"]
|
||||
+ [_("Delivered Amount") + ":Currency:130"]
|
||||
+ [_("Total Qty") + ":Float:110"]
|
||||
+ [_("Total Amount") + ":Currency:130"]
|
||||
+ [_("Supplier(s)") + "::250"]
|
||||
)
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:100",
|
||||
_("Item Name") + "::100",
|
||||
_("Description") + "::150",
|
||||
_("UOM") + ":Link/UOM:90",
|
||||
_("Consumed Qty") + ":Float:110",
|
||||
_("Consumed Amount") + ":Currency:130",
|
||||
_("Delivered Qty") + ":Float:110",
|
||||
_("Delivered Amount") + ":Currency:130",
|
||||
_("Total Qty") + ":Float:110",
|
||||
_("Total Amount") + ":Currency:130",
|
||||
_("Supplier(s)") + "::250",
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -173,9 +172,7 @@ def get_suppliers_details(filters):
|
||||
def get_material_transfer_vouchers():
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
query = (
|
||||
frappe.qb.from_(se)
|
||||
.select(se.name)
|
||||
.where((se.purpose == "Material Transfer") & (se.docstatus == 1))
|
||||
frappe.qb.from_(se).select(se.name).where((se.purpose == "Material Transfer") & (se.docstatus == 1))
|
||||
)
|
||||
|
||||
return [r[0] for r in query.run()]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import unittest
|
||||
from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
|
||||
@@ -14,7 +13,7 @@ DEFAULT_FILTERS = {
|
||||
|
||||
batch = frappe.db.get_value("Batch", fieldname=["name"], as_dict=True, order_by="creation desc")
|
||||
|
||||
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("Stock Ledger", {"_optional": True}),
|
||||
("Stock Ledger", {"batch_no": batch}),
|
||||
("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}),
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns(filters)
|
||||
@@ -54,8 +53,8 @@ def get_total_stock(filters):
|
||||
else:
|
||||
query = query.select(wh.company).groupby(wh.company)
|
||||
|
||||
query = query.select(
|
||||
item.item_code, item.description, Sum(bin.actual_qty).as_("actual_qty")
|
||||
).groupby(item.item_code)
|
||||
query = query.select(item.item_code, item.description, Sum(bin.actual_qty).as_("actual_qty")).groupby(
|
||||
item.item_code
|
||||
)
|
||||
|
||||
return query.run()
|
||||
|
||||
@@ -40,7 +40,7 @@ def execute(filters=None):
|
||||
item_balance = {}
|
||||
item_value = {}
|
||||
|
||||
for (company, item, warehouse) in sorted(iwb_map):
|
||||
for company, item, warehouse in sorted(iwb_map):
|
||||
if not item_map.get(item):
|
||||
continue
|
||||
|
||||
@@ -71,7 +71,7 @@ def execute(filters=None):
|
||||
|
||||
row += [average_age]
|
||||
|
||||
bal_qty = [sum(bal_qty) for bal_qty in zip(*wh_balance)]
|
||||
bal_qty = [sum(bal_qty) for bal_qty in zip(*wh_balance, strict=False)]
|
||||
total_qty = sum(bal_qty)
|
||||
if len(warehouse_list) > 1:
|
||||
row += [total_qty]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -9,12 +9,12 @@ from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
class StockBalanceFilter(TypedDict):
|
||||
company: Optional[str]
|
||||
warehouse: Optional[str]
|
||||
show_disabled_warehouses: Optional[int]
|
||||
company: str | None
|
||||
warehouse: str | None
|
||||
show_disabled_warehouses: int | None
|
||||
|
||||
|
||||
SLEntry = Dict[str, Any]
|
||||
SLEntry = dict[str, Any]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -25,7 +25,7 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_warehouse_wise_balance(filters: StockBalanceFilter) -> List[SLEntry]:
|
||||
def get_warehouse_wise_balance(filters: StockBalanceFilter) -> list[SLEntry]:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
query = (
|
||||
@@ -95,7 +95,7 @@ def set_balance_in_parent(warehouses):
|
||||
update_balance(warehouse, warehouse.stock_balance)
|
||||
|
||||
|
||||
def get_columns(filters: StockBalanceFilter) -> List[Dict]:
|
||||
def get_columns(filters: StockBalanceFilter) -> list[dict]:
|
||||
columns = [
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
|
||||
@@ -15,9 +15,7 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False,
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
|
||||
if allow_negative_stock:
|
||||
existing_allow_negative_stock = frappe.db.get_value(
|
||||
"Stock Settings", None, "allow_negative_stock"
|
||||
)
|
||||
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
|
||||
item_warehouses = frappe.db.sql(
|
||||
@@ -37,9 +35,7 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False,
|
||||
frappe.db.rollback()
|
||||
|
||||
if allow_negative_stock:
|
||||
frappe.db.set_value(
|
||||
"Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock
|
||||
)
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
|
||||
@@ -51,7 +47,6 @@ def repost_stock(
|
||||
only_bin=False,
|
||||
allow_negative_stock=False,
|
||||
):
|
||||
|
||||
if not only_bin:
|
||||
repost_actual_qty(item_code, warehouse, allow_zero_rate, allow_negative_stock)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
from typing import Optional, Set, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -185,9 +184,7 @@ def repost_future_sle(
|
||||
if not args:
|
||||
args = [] # set args to empty list if None to avoid enumerate error
|
||||
|
||||
items_to_be_repost = get_items_to_be_repost(
|
||||
voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
|
||||
)
|
||||
items_to_be_repost = get_items_to_be_repost(voucher_type=voucher_type, voucher_no=voucher_no, doc=doc)
|
||||
if items_to_be_repost:
|
||||
args = items_to_be_repost
|
||||
|
||||
@@ -212,12 +209,10 @@ def repost_future_sle(
|
||||
)
|
||||
affected_transactions.update(obj.affected_transactions)
|
||||
|
||||
distinct_item_warehouses[
|
||||
(args[i].get("item_code"), args[i].get("warehouse"))
|
||||
].reposting_status = True
|
||||
distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True
|
||||
|
||||
if obj.new_items_found:
|
||||
for item_wh, data in distinct_item_warehouses.items():
|
||||
for _item_wh, data in distinct_item_warehouses.items():
|
||||
if ("args_idx" not in data and not data.reposting_status) or (
|
||||
data.sle_changed and data.reposting_status
|
||||
):
|
||||
@@ -242,9 +237,7 @@ def validate_item_warehouse(args):
|
||||
frappe.throw(_(validation_msg))
|
||||
|
||||
|
||||
def update_args_in_repost_item_valuation(
|
||||
doc, index, args, distinct_item_warehouses, affected_transactions
|
||||
):
|
||||
def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses, affected_transactions):
|
||||
doc.db_set(
|
||||
{
|
||||
"items_to_be_repost": json.dumps(args, default=str),
|
||||
@@ -300,7 +293,7 @@ def get_distinct_item_warehouse(args=None, doc=None):
|
||||
return distinct_item_warehouses
|
||||
|
||||
|
||||
def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
|
||||
def get_affected_transactions(doc) -> set[tuple[str, str]]:
|
||||
if not doc.affected_transactions:
|
||||
return set()
|
||||
|
||||
@@ -313,7 +306,7 @@ def get_current_index(doc=None):
|
||||
return doc.current_index
|
||||
|
||||
|
||||
class update_entries_after(object):
|
||||
class update_entries_after:
|
||||
"""
|
||||
update valution rate and qty after transaction
|
||||
from the current time-bucket onwards
|
||||
@@ -355,7 +348,7 @@ class update_entries_after(object):
|
||||
|
||||
self.new_items_found = False
|
||||
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
|
||||
self.affected_transactions: Set[Tuple[str, str]] = set()
|
||||
self.affected_transactions: set[tuple[str, str]] = set()
|
||||
|
||||
self.data = frappe._dict()
|
||||
self.initialize_previous_data(self.args)
|
||||
@@ -433,9 +426,7 @@ class update_entries_after(object):
|
||||
self.process_sle(sle)
|
||||
|
||||
def get_sle_against_current_voucher(self):
|
||||
self.args["posting_datetime"] = get_combine_datetime(
|
||||
self.args.posting_date, self.args.posting_time
|
||||
)
|
||||
self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
@@ -473,9 +464,7 @@ class update_entries_after(object):
|
||||
|
||||
if not dependant_sle:
|
||||
return entries_to_fix
|
||||
elif (
|
||||
dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
|
||||
):
|
||||
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
|
||||
return entries_to_fix
|
||||
elif dependant_sle.item_code != self.item_code:
|
||||
self.update_distinct_item_warehouses(dependant_sle)
|
||||
@@ -495,9 +484,7 @@ class update_entries_after(object):
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
else:
|
||||
existing_sle_posting_date = (
|
||||
self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
|
||||
)
|
||||
existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
|
||||
|
||||
dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
|
||||
|
||||
@@ -584,7 +571,9 @@ class update_entries_after(object):
|
||||
self.wh_data.valuation_rate
|
||||
)
|
||||
if self.valuation_method != "Moving Average":
|
||||
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
|
||||
self.wh_data.stock_queue = [
|
||||
[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]
|
||||
]
|
||||
else:
|
||||
if self.valuation_method == "Moving Average":
|
||||
self.get_moving_average_values(sle)
|
||||
@@ -927,9 +916,7 @@ class update_entries_after(object):
|
||||
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
|
||||
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_rate = self.check_if_allow_zero_valuation_rate(
|
||||
sle.voucher_type, sle.voucher_detail_no
|
||||
)
|
||||
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
@@ -1044,9 +1031,7 @@ class update_entries_after(object):
|
||||
stock_value_difference = stock_value - prev_stock_value
|
||||
|
||||
self.wh_data.stock_queue = stock_queue.state
|
||||
self.wh_data.stock_value = round_off_if_near_zero(
|
||||
self.wh_data.stock_value + stock_value_difference
|
||||
)
|
||||
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
|
||||
|
||||
if not self.wh_data.stock_queue:
|
||||
self.wh_data.stock_queue.append(
|
||||
@@ -1083,9 +1068,7 @@ class update_entries_after(object):
|
||||
outgoing_rate = self.get_fallback_rate(sle)
|
||||
stock_value_difference = outgoing_rate * actual_qty
|
||||
|
||||
self.wh_data.stock_value = round_off_if_near_zero(
|
||||
self.wh_data.stock_value + stock_value_difference
|
||||
)
|
||||
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
|
||||
if self.wh_data.qty_after_transaction:
|
||||
self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
|
||||
|
||||
@@ -1135,7 +1118,6 @@ class update_entries_after(object):
|
||||
exceptions[0]["voucher_type"],
|
||||
exceptions[0]["voucher_no"],
|
||||
) in frappe.local.flags.currently_saving:
|
||||
|
||||
msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
|
||||
abs(deficiency),
|
||||
frappe.get_desk_link("Item", exceptions[0]["item_code"]),
|
||||
@@ -1201,7 +1183,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||
|
||||
sle = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select *, posting_datetime as "timestamp"
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %(item_code)s
|
||||
@@ -1213,10 +1195,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
)
|
||||
order by posting_datetime desc, creation desc
|
||||
limit 1
|
||||
for update""".format(
|
||||
operator=operator,
|
||||
voucher_condition=voucher_condition,
|
||||
),
|
||||
for update""",
|
||||
args,
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -1256,7 +1235,7 @@ def get_stock_ledger_entries(
|
||||
extra_cond=None,
|
||||
):
|
||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||
conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
|
||||
conditions = f" and posting_datetime {operator} %(posting_datetime)s"
|
||||
if previous_sle.get("warehouse"):
|
||||
conditions += " and warehouse = %(warehouse)s"
|
||||
elif previous_sle.get("warehouse_condition"):
|
||||
@@ -1268,17 +1247,17 @@ def get_stock_ledger_entries(
|
||||
conditions += (
|
||||
""" and
|
||||
(
|
||||
serial_no = {0}
|
||||
or serial_no like {1}
|
||||
or serial_no like {2}
|
||||
or serial_no like {3}
|
||||
serial_no = {}
|
||||
or serial_no like {}
|
||||
or serial_no like {}
|
||||
or serial_no like {}
|
||||
)
|
||||
"""
|
||||
).format(
|
||||
frappe.db.escape(serial_no),
|
||||
frappe.db.escape("{}\n%".format(serial_no)),
|
||||
frappe.db.escape("%\n{}".format(serial_no)),
|
||||
frappe.db.escape("%\n{}\n%".format(serial_no)),
|
||||
frappe.db.escape(f"{serial_no}\n%"),
|
||||
frappe.db.escape(f"%\n{serial_no}"),
|
||||
frappe.db.escape(f"%\n{serial_no}\n%"),
|
||||
)
|
||||
|
||||
if not previous_sle.get("posting_date"):
|
||||
@@ -1301,17 +1280,16 @@ def get_stock_ledger_entries(
|
||||
"""
|
||||
select *, posting_datetime as "timestamp"
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %%(item_code)s
|
||||
where item_code = %(item_code)s
|
||||
and is_cancelled = 0
|
||||
%(conditions)s
|
||||
order by posting_datetime %(order)s, creation %(order)s
|
||||
%(limit)s %(for_update)s"""
|
||||
% {
|
||||
"conditions": conditions,
|
||||
"limit": limit or "",
|
||||
"for_update": for_update and "for update" or "",
|
||||
"order": order,
|
||||
},
|
||||
{conditions}
|
||||
order by posting_datetime {order}, creation {order}
|
||||
{limit} {for_update}""".format(
|
||||
conditions=conditions,
|
||||
limit=limit or "",
|
||||
for_update=for_update and "for update" or "",
|
||||
order=order,
|
||||
),
|
||||
previous_sle,
|
||||
as_dict=1,
|
||||
debug=debug,
|
||||
@@ -1336,10 +1314,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
)
|
||||
|
||||
|
||||
def get_batch_incoming_rate(
|
||||
item_code, warehouse, batch_no, posting_date, posting_time, creation=None
|
||||
):
|
||||
|
||||
def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
|
||||
import datetime
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
@@ -1350,9 +1325,9 @@ def get_batch_incoming_rate(
|
||||
|
||||
timestamp_condition = sle.posting_datetime < posting_datetime
|
||||
if creation:
|
||||
timestamp_condition |= (
|
||||
sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
|
||||
) & (sle.creation < creation)
|
||||
timestamp_condition |= (sle.posting_datetime == get_combine_datetime(posting_date, posting_time)) & (
|
||||
sle.creation < creation
|
||||
)
|
||||
|
||||
batch_details = (
|
||||
frappe.qb.from_(sle)
|
||||
@@ -1381,7 +1356,6 @@ def get_valuation_rate(
|
||||
raise_error_if_no_rate=True,
|
||||
batch_no=None,
|
||||
):
|
||||
|
||||
if not company:
|
||||
company = frappe.get_cached_value("Warehouse", warehouse, "company")
|
||||
|
||||
@@ -1457,7 +1431,7 @@ def get_valuation_rate(
|
||||
solutions += (
|
||||
"<li>"
|
||||
+ _("If not, you can Cancel / Submit this entry")
|
||||
+ " {0} ".format(frappe.bold("after"))
|
||||
+ " {} ".format(frappe.bold("after"))
|
||||
+ _("performing either one below:")
|
||||
+ "</li>"
|
||||
)
|
||||
@@ -1591,9 +1565,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
neg_sle = get_future_sle_with_negative_qty(args)
|
||||
|
||||
if is_negative_with_precision(neg_sle):
|
||||
message = _(
|
||||
"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
|
||||
).format(
|
||||
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
||||
abs(neg_sle[0]["qty_after_transaction"]),
|
||||
frappe.get_desk_link("Item", args.item_code),
|
||||
frappe.get_desk_link("Warehouse", args.warehouse),
|
||||
@@ -1609,9 +1581,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
|
||||
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
|
||||
if is_negative_with_precision(neg_batch_sle, is_batch=True):
|
||||
message = _(
|
||||
"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
|
||||
).format(
|
||||
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
||||
abs(neg_batch_sle[0]["cumulative_total"]),
|
||||
frappe.get_desk_link("Batch", args.batch_no),
|
||||
frappe.get_desk_link("Warehouse", args.warehouse),
|
||||
@@ -1689,7 +1659,7 @@ def get_future_sle_with_negative_batch_qty(args):
|
||||
)
|
||||
|
||||
|
||||
def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
|
||||
def is_negative_stock_allowed(*, item_code: str | None = None) -> bool:
|
||||
if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
|
||||
return True
|
||||
if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
@@ -16,7 +14,6 @@ class TestGetItemDetail(FrappeTestCase):
|
||||
super().setUp()
|
||||
|
||||
def test_get_item_detail_purchase_order(self):
|
||||
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
@@ -29,7 +26,6 @@ class TestGetItemDetail(FrappeTestCase):
|
||||
"name": None,
|
||||
"supplier": "_Test Supplier",
|
||||
"transaction_date": None,
|
||||
"conversion_rate": 1.0,
|
||||
"price_list": "_Test Buying Price List",
|
||||
"is_subcontracted": 0,
|
||||
"ignore_pricing_rule": 1,
|
||||
|
||||
@@ -28,7 +28,7 @@ class StockTestMixin:
|
||||
)
|
||||
self.assertGreaterEqual(len(sles), len(expected_sles))
|
||||
|
||||
for exp_sle, act_sle in zip(expected_sles, sles):
|
||||
for exp_sle, act_sle in zip(expected_sles, sles, strict=False):
|
||||
for k, v in exp_sle.items():
|
||||
act_value = act_sle[k]
|
||||
if k == "stock_queue":
|
||||
@@ -51,7 +51,7 @@ class StockTestMixin:
|
||||
order_by=order_by or "posting_date, creation",
|
||||
)
|
||||
self.assertGreaterEqual(len(actual_gles), len(expected_gles))
|
||||
for exp_gle, act_gle in zip(expected_gles, actual_gles):
|
||||
for exp_gle, act_gle in zip(expected_gles, actual_gles, strict=False):
|
||||
for k, exp_value in exp_gle.items():
|
||||
act_value = act_gle[k]
|
||||
self.assertEqual(exp_value, act_value, msg=f"{k} doesn't match \n{exp_gle}\n{act_gle}")
|
||||
|
||||
@@ -28,9 +28,7 @@ class TestFIFOValuation(unittest.TestCase):
|
||||
self.assertAlmostEqual(sum(q for q, _ in self.queue), qty, msg=f"queue: {self.queue}", places=4)
|
||||
|
||||
def assertTotalValue(self, value):
|
||||
self.assertAlmostEqual(
|
||||
sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2
|
||||
)
|
||||
self.assertAlmostEqual(sum(q * r for q, r in self.queue), value, msg=f"queue: {self.queue}", places=2)
|
||||
|
||||
def test_simple_addition(self):
|
||||
self.queue.add_stock(1, 10)
|
||||
@@ -210,9 +208,7 @@ class TestLIFOValuation(unittest.TestCase):
|
||||
self.assertAlmostEqual(sum(q for q, _ in self.stack), qty, msg=f"stack: {self.stack}", places=4)
|
||||
|
||||
def assertTotalValue(self, value):
|
||||
self.assertAlmostEqual(
|
||||
sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2
|
||||
)
|
||||
self.assertAlmostEqual(sum(q * r for q, r in self.stack), value, msg=f"stack: {self.stack}", places=2)
|
||||
|
||||
def test_simple_addition(self):
|
||||
self.stack.add_stock(1, 10)
|
||||
@@ -355,7 +351,6 @@ class TestLIFOValuationSLE(FrappeTestCase):
|
||||
self.assertEqual(stock_queue, expected_queue)
|
||||
|
||||
def test_lifo_values(self):
|
||||
|
||||
in1 = self._make_stock_entry(1, 1)
|
||||
self.assertStockQueue(in1, [[1, 1]])
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -14,7 +13,7 @@ import erpnext
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
|
||||
|
||||
BarcodeScanResult = Dict[str, Optional[str]]
|
||||
BarcodeScanResult = dict[str, str | None]
|
||||
|
||||
|
||||
class InvalidWarehouseCompany(frappe.ValidationError):
|
||||
@@ -55,7 +54,7 @@ def get_stock_value_from_bin(warehouse=None, item_code=None):
|
||||
|
||||
|
||||
def get_stock_value_on(
|
||||
warehouses: list | str = None, posting_date: str = None, item_code: str = None
|
||||
warehouses: list | str | None = None, posting_date: str | None = None, item_code: str | None = None
|
||||
) -> float:
|
||||
if not posting_date:
|
||||
posting_date = nowdate()
|
||||
@@ -140,9 +139,7 @@ def get_stock_balance(
|
||||
else (0.0, 0.0, None)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
(last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
|
||||
)
|
||||
return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
|
||||
else:
|
||||
return last_entry.qty_after_transaction if last_entry else 0.0
|
||||
|
||||
@@ -205,10 +202,8 @@ def get_latest_stock_qty(item_code, warehouse=None):
|
||||
condition += " AND warehouse = %s"
|
||||
|
||||
actual_qty = frappe.db.sql(
|
||||
"""select sum(actual_qty) from tabBin
|
||||
where item_code=%s {0}""".format(
|
||||
condition
|
||||
),
|
||||
f"""select sum(actual_qty) from tabBin
|
||||
where item_code=%s {condition}""",
|
||||
values,
|
||||
)[0][0]
|
||||
|
||||
@@ -337,9 +332,7 @@ def get_valuation_method(item_code):
|
||||
"""get valuation method from item or default"""
|
||||
val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True)
|
||||
if not val_method:
|
||||
val_method = (
|
||||
frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
|
||||
)
|
||||
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
|
||||
return val_method
|
||||
|
||||
|
||||
@@ -414,7 +407,6 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
|
||||
if not include_uom or not conversion_factors:
|
||||
return
|
||||
|
||||
convertible_cols = {}
|
||||
is_dict_obj = False
|
||||
if isinstance(result[0], dict):
|
||||
is_dict_obj = True
|
||||
@@ -429,8 +421,8 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
|
||||
columns.insert(
|
||||
idx + 1,
|
||||
{
|
||||
"label": "{0} (per {1})".format(d.get("label"), include_uom),
|
||||
"fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)),
|
||||
"label": "{} (per {})".format(d.get("label"), include_uom),
|
||||
"fieldname": "{}_{}".format(d.get("fieldname"), frappe.scrub(include_uom)),
|
||||
"fieldtype": "Currency" if d.get("convertible") == "rate" else "Float",
|
||||
},
|
||||
)
|
||||
@@ -453,7 +445,7 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
|
||||
if not is_dict_obj:
|
||||
row.insert(key + 1, new_value)
|
||||
else:
|
||||
new_key = "{0}_{1}".format(key, frappe.scrub(include_uom))
|
||||
new_key = f"{key}_{frappe.scrub(include_uom)}"
|
||||
update_dict_values.append([row, new_key, new_value])
|
||||
|
||||
for data in update_dict_values:
|
||||
@@ -487,9 +479,9 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors)
|
||||
{"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")}
|
||||
)
|
||||
if col.get("convertible") == "rate":
|
||||
columns[next_col]["label"] += " (per {})".format(include_uom)
|
||||
columns[next_col]["label"] += f" (per {include_uom})"
|
||||
else:
|
||||
columns[next_col]["label"] += " ({})".format(include_uom)
|
||||
columns[next_col]["label"] += f" ({include_uom})"
|
||||
|
||||
for row_idx, row in enumerate(result):
|
||||
for convertible_col, data in convertible_column_map.items():
|
||||
@@ -563,7 +555,7 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
|
||||
def set_cache(data: BarcodeScanResult):
|
||||
frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120)
|
||||
|
||||
def get_cache() -> Optional[BarcodeScanResult]:
|
||||
def get_cache() -> BarcodeScanResult | None:
|
||||
if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"):
|
||||
return data
|
||||
|
||||
@@ -609,7 +601,7 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
|
||||
return {}
|
||||
|
||||
|
||||
def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]:
|
||||
def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]:
|
||||
if item_code := scan_result.get("item_code"):
|
||||
if item_info := frappe.get_cached_value(
|
||||
"Item",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from typing import Callable, List, NewType, Optional, Tuple
|
||||
from collections.abc import Callable
|
||||
from typing import NewType
|
||||
|
||||
from frappe.utils import flt
|
||||
|
||||
StockBin = NewType("StockBin", List[float]) # [[qty, rate], ...]
|
||||
StockBin = NewType("StockBin", list[float]) # [[qty, rate], ...]
|
||||
|
||||
# Indexes of values inside FIFO bin 2-tuple
|
||||
QTY = 0
|
||||
@@ -17,15 +18,15 @@ class BinWiseValuation(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def remove_stock(
|
||||
self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
|
||||
) -> List[StockBin]:
|
||||
self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] | None = None
|
||||
) -> list[StockBin]:
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def state(self) -> List[StockBin]:
|
||||
def state(self) -> list[StockBin]:
|
||||
pass
|
||||
|
||||
def get_total_stock_and_value(self) -> Tuple[float, float]:
|
||||
def get_total_stock_and_value(self) -> tuple[float, float]:
|
||||
total_qty = 0.0
|
||||
total_value = 0.0
|
||||
|
||||
@@ -62,11 +63,11 @@ class FIFOValuation(BinWiseValuation):
|
||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||
__slots__ = ["queue"]
|
||||
|
||||
def __init__(self, state: Optional[List[StockBin]]):
|
||||
self.queue: List[StockBin] = state if state is not None else []
|
||||
def __init__(self, state: list[StockBin] | None):
|
||||
self.queue: list[StockBin] = state if state is not None else []
|
||||
|
||||
@property
|
||||
def state(self) -> List[StockBin]:
|
||||
def state(self) -> list[StockBin]:
|
||||
"""Get current state of queue."""
|
||||
return self.queue
|
||||
|
||||
@@ -95,8 +96,8 @@ class FIFOValuation(BinWiseValuation):
|
||||
self.queue[-1][QTY] = qty
|
||||
|
||||
def remove_stock(
|
||||
self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
|
||||
) -> List[StockBin]:
|
||||
self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] | None = None
|
||||
) -> list[StockBin]:
|
||||
"""Remove stock from the queue and return popped bins.
|
||||
|
||||
args:
|
||||
@@ -166,11 +167,11 @@ class LIFOValuation(BinWiseValuation):
|
||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||
__slots__ = ["stack"]
|
||||
|
||||
def __init__(self, state: Optional[List[StockBin]]):
|
||||
self.stack: List[StockBin] = state if state is not None else []
|
||||
def __init__(self, state: list[StockBin] | None):
|
||||
self.stack: list[StockBin] = state if state is not None else []
|
||||
|
||||
@property
|
||||
def state(self) -> List[StockBin]:
|
||||
def state(self) -> list[StockBin]:
|
||||
"""Get current state of stack."""
|
||||
return self.stack
|
||||
|
||||
@@ -201,8 +202,8 @@ class LIFOValuation(BinWiseValuation):
|
||||
self.stack[-1][QTY] = qty
|
||||
|
||||
def remove_stock(
|
||||
self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] = None
|
||||
) -> List[StockBin]:
|
||||
self, qty: float, outgoing_rate: float = 0.0, rate_generator: Callable[[], float] | None = None
|
||||
) -> list[StockBin]:
|
||||
"""Remove stock from the stack and return popped bins.
|
||||
|
||||
args:
|
||||
|
||||
Reference in New Issue
Block a user