Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: item-wise negative stock setting #29761

Merged
merged 1 commit into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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