feat: item-wise negative stock setting (#29761)
This commit is contained in:
parent
749005eb8b
commit
eb8b424722
@ -172,9 +172,10 @@ class POSInvoice(SalesInvoice):
|
|||||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||||
|
|
||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
|
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||||
|
|
||||||
if self.is_return or self.docstatus != 1:
|
if self.is_return or self.docstatus != 1:
|
||||||
return
|
return
|
||||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
|
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
|
||||||
if is_service_item:
|
if is_service_item:
|
||||||
@ -186,7 +187,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
elif d.batch_no:
|
elif d.batch_no:
|
||||||
self.validate_pos_reserved_batch_qty(d)
|
self.validate_pos_reserved_batch_qty(d)
|
||||||
else:
|
else:
|
||||||
if allow_negative_stock:
|
if is_negative_stock_allowed(item_code=d.item_code):
|
||||||
return
|
return
|
||||||
|
|
||||||
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
"warranty_period",
|
"warranty_period",
|
||||||
"weight_per_unit",
|
"weight_per_unit",
|
||||||
"weight_uom",
|
"weight_uom",
|
||||||
|
"allow_negative_stock",
|
||||||
"reorder_section",
|
"reorder_section",
|
||||||
"reorder_levels",
|
"reorder_levels",
|
||||||
"unit_of_measure_conversion",
|
"unit_of_measure_conversion",
|
||||||
@ -907,6 +908,12 @@
|
|||||||
"fieldname": "is_grouped_asset",
|
"fieldname": "is_grouped_asset",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Create Grouped Asset"
|
"label": "Create Grouped Asset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "allow_negative_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Negative Stock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-tag",
|
"icon": "fa fa-tag",
|
||||||
@ -914,7 +921,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-18 12:57:54.273202",
|
"modified": "2022-02-11 08:07:46.663220",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_objects
|
from frappe.test_runner import make_test_objects
|
||||||
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
from erpnext.controllers.item_variant import (
|
from erpnext.controllers.item_variant import (
|
||||||
InvalidItemAttributeValueError,
|
InvalidItemAttributeValueError,
|
||||||
@ -608,6 +609,45 @@ class TestItem(ERPNextTestCase):
|
|||||||
item.item_group = "All Item Groups"
|
item.item_group = "All Item Groups"
|
||||||
item.save() # if item code saved without item_code then series worked
|
item.save() # if item code saved without item_code then series worked
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
|
def test_item_wise_negative_stock(self):
|
||||||
|
""" When global settings are disabled check that item that allows
|
||||||
|
negative stock can still consume material in all known stock
|
||||||
|
transactions that consume inventory."""
|
||||||
|
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||||
|
|
||||||
|
item = make_item("_TestNegativeItemSetting", {"allow_negative_stock": 1, "valuation_rate": 100})
|
||||||
|
self.assertTrue(is_negative_stock_allowed(item_code=item.name))
|
||||||
|
|
||||||
|
self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
|
def test_backdated_negative_stock(self):
|
||||||
|
""" same as test above but backdated entries """
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
item = make_item("_TestNegativeItemSetting", {"allow_negative_stock": 1, "valuation_rate": 100})
|
||||||
|
|
||||||
|
# create a future entry so all new entries are backdated
|
||||||
|
make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5))
|
||||||
|
self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
|
||||||
|
|
||||||
|
|
||||||
|
def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"):
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
typical_args = {"item_code": item_code, "warehouse": warehouse}
|
||||||
|
|
||||||
|
create_delivery_note(**typical_args)
|
||||||
|
create_sales_invoice(update_stock=1, **typical_args)
|
||||||
|
make_stock_entry(item_code=item_code, source=warehouse, qty=1, purpose="Material Issue")
|
||||||
|
make_stock_entry(item_code=item_code, source=warehouse, target="Stores - _TC", qty=1)
|
||||||
|
# standalone return
|
||||||
|
make_purchase_receipt(is_return=True, qty=-1, **typical_args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def set_item_variant_settings(fields):
|
def set_item_variant_settings(fields):
|
||||||
doc = frappe.get_doc('Item Variant Settings')
|
doc = frappe.get_doc('Item Variant Settings')
|
||||||
|
|||||||
@ -433,9 +433,10 @@ class StockEntry(StockController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_actual_qty(self):
|
def set_actual_qty(self):
|
||||||
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
|
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||||
|
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
|
allow_negative_stock = is_negative_stock_allowed(item_code=d.item_code)
|
||||||
previous_sle = get_previous_sle({
|
previous_sle = get_previous_sle({
|
||||||
"item_code": d.item_code,
|
"item_code": d.item_code,
|
||||||
"warehouse": d.s_warehouse or d.t_warehouse,
|
"warehouse": d.s_warehouse or d.t_warehouse,
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@ -268,11 +269,10 @@ class update_entries_after(object):
|
|||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.allow_zero_rate = allow_zero_rate
|
self.allow_zero_rate = allow_zero_rate
|
||||||
self.via_landed_cost_voucher = via_landed_cost_voucher
|
self.via_landed_cost_voucher = via_landed_cost_voucher
|
||||||
self.allow_negative_stock = allow_negative_stock \
|
self.item_code = args.get("item_code")
|
||||||
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(item_code=self.item_code)
|
||||||
|
|
||||||
self.args = frappe._dict(args)
|
self.args = frappe._dict(args)
|
||||||
self.item_code = args.get("item_code")
|
|
||||||
if self.args.sle_id:
|
if self.args.sle_id:
|
||||||
self.args['name'] = self.args.sle_id
|
self.args['name'] = self.args.sle_id
|
||||||
|
|
||||||
@ -1049,10 +1049,7 @@ def get_datetime_limit_condition(detail):
|
|||||||
)"""
|
)"""
|
||||||
|
|
||||||
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||||
allow_negative_stock = cint(allow_negative_stock) \
|
if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
|
||||||
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
|
||||||
|
|
||||||
if allow_negative_stock:
|
|
||||||
return
|
return
|
||||||
if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
|
if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
|
||||||
return
|
return
|
||||||
@ -1121,3 +1118,11 @@ def get_future_sle_with_negative_batch_qty(args):
|
|||||||
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
|
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
|
||||||
limit 1
|
limit 1
|
||||||
""", args, as_dict=1)
|
""", args, as_dict=1)
|
||||||
|
|
||||||
|
|
||||||
|
def is_negative_stock_allowed(*, item_code: Optional[str] = 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)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user