Skip to content

Commit

Permalink
feat: item-wise negative stock setting (#29761)
Browse files Browse the repository at this point in the history
  • Loading branch information
ankush committed Feb 12, 2022
1 parent 749005e commit eb8b424
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 11 deletions.
5 changes: 3 additions & 2 deletions erpnext/accounts/doctype/pos_invoice/pos_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ def validate_invalid_serial_nos(self, item):
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)

def validate_stock_availablility(self):
from erpnext.stock.stock_ledger import is_negative_stock_allowed

if self.is_return or self.docstatus != 1:
return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item:
Expand All @@ -186,7 +187,7 @@ def validate_stock_availablility(self):
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else:
if allow_negative_stock:
if is_negative_stock_allowed(item_code=d.item_code):
return

available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
Expand Down
9 changes: 8 additions & 1 deletion erpnext/stock/doctype/item/item.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"warranty_period",
"weight_per_unit",
"weight_uom",
"allow_negative_stock",
"reorder_section",
"reorder_levels",
"unit_of_measure_conversion",
Expand Down Expand Up @@ -907,14 +908,20 @@
"fieldname": "is_grouped_asset",
"fieldtype": "Check",
"label": "Create Grouped Asset"
},
{
"default": "0",
"fieldname": "allow_negative_stock",
"fieldtype": "Check",
"label": "Allow Negative Stock"
}
],
"icon": "fa fa-tag",
"idx": 2,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-18 12:57:54.273202",
"modified": "2022-02-11 08:07:46.663220",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
Expand Down
40 changes: 40 additions & 0 deletions erpnext/stock/doctype/item/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import frappe
from frappe.test_runner import make_test_objects
from frappe.utils import add_days, today

from erpnext.controllers.item_variant import (
InvalidItemAttributeValueError,
Expand Down Expand Up @@ -608,6 +609,45 @@ def test_autoname_series(self):
item.item_group = "All Item Groups"
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):
doc = frappe.get_doc('Item Variant Settings')
Expand Down
3 changes: 2 additions & 1 deletion erpnext/stock/doctype/stock_entry/stock_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,10 @@ def check_duplicate_entry_for_work_order(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'):
allow_negative_stock = is_negative_stock_allowed(item_code=d.item_code)
previous_sle = get_previous_sle({
"item_code": d.item_code,
"warehouse": d.s_warehouse or d.t_warehouse,
Expand Down
19 changes: 12 additions & 7 deletions erpnext/stock/stock_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import copy
import json
from typing import Optional

import frappe
from frappe import _
Expand Down Expand Up @@ -268,11 +269,10 @@ def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_l
self.verbose = verbose
self.allow_zero_rate = allow_zero_rate
self.via_landed_cost_voucher = via_landed_cost_voucher
self.allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
self.item_code = args.get("item_code")
self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(item_code=self.item_code)

self.args = frappe._dict(args)
self.item_code = args.get("item_code")
if self.args.sle_id:
self.args['name'] = self.args.sle_id

Expand Down Expand Up @@ -1049,10 +1049,7 @@ def get_datetime_limit_condition(detail):
)"""

def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
allow_negative_stock = cint(allow_negative_stock) \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))

if allow_negative_stock:
if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
return
if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
return
Expand Down Expand Up @@ -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)
limit 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

0 comments on commit eb8b424

Please sign in to comment.