Improvements to batch auto naming (#12496)

* refactor:
add new function - batch_uses_naming_series
use batch_uses_naming_series in autoname method

* properly update naming series on delete:
- add new functions - get_batch_prefix, get_batch_naming_series_key, get_batch_naming_series
- refactor get_name_from_naming_series
- add after_delete method

* add documentation and rename some functions

* PEP 8 compliance

* test
This commit is contained in:
tundebabzy 2018-01-26 06:30:20 +01:00 committed by Nabin Hait
parent ec3052cc8f
commit db4fd7f5b6
2 changed files with 126 additions and 22 deletions

View File

@ -5,24 +5,31 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.utils import flt, cint
class UnableToSelectBatchError(frappe.ValidationError): pass
class UnableToSelectBatchError(frappe.ValidationError):
pass
def get_name_from_naming_series():
naming_series_prefix = frappe.db.get_single_value('Stock Settings', 'naming_series_prefix')
if not naming_series_prefix:
naming_series_prefix = 'BATCH-'
name = make_autoname(naming_series_prefix + '.#####')
"""
Get a name generated for a Batch from the Batch's naming series.
:return: The string that was generated.
"""
naming_series_prefix = _get_batch_prefix()
key = _make_naming_series_key(naming_series_prefix)
name = make_autoname(key)
return name
def get_name_from_hash():
"""
Get a name for a Batch by generating a unique hash.
:return: The hash that was generated.
"""
temp = None
while not temp:
temp = frappe.generate_hash()[:7].upper()
@ -32,13 +39,66 @@ def get_name_from_hash():
return temp
def batch_uses_naming_series():
"""
Verify if the Batch is to be named using a naming series
:return: bool
"""
use_naming_series = cint(frappe.db.get_single_value('Stock Settings', 'use_naming_series'))
return bool(use_naming_series)
def _get_batch_prefix():
"""
Get the naming series prefix set in Stock Settings.
It does not do any sanity checks so make sure to use it after checking if the Batch
is set to use naming series.
:return: The naming series.
"""
naming_series_prefix = frappe.db.get_single_value('Stock Settings', 'naming_series_prefix')
if not naming_series_prefix:
naming_series_prefix = 'BATCH-'
return naming_series_prefix
def _make_naming_series_key(prefix):
"""
Make naming series key for a Batch.
Naming series key is in the format [prefix].[#####]
:param prefix: Naming series prefix gotten from Stock Settings
:return: The derived key. If no prefix is given, an empty string is returned
"""
if not unicode(prefix):
return ''
else:
return prefix.upper() + '.#####'
def get_batch_naming_series():
"""
Get naming series key for a Batch.
Naming series key is in the format [prefix].[#####]
:return: The naming series or empty string if not available
"""
series = ''
if batch_uses_naming_series():
prefix = _get_batch_prefix()
key = _make_naming_series_key(prefix)
series = key
return series
class Batch(Document):
def autoname(self):
"""Generate random ID for batch if not specified"""
if not self.batch_id:
if frappe.db.get_value('Item', self.item, 'create_new_batch'):
use_naming_series = frappe.db.get_single_value('Stock Settings', 'use_naming_series')
if use_naming_series:
if batch_uses_naming_series():
self.batch_id = get_name_from_naming_series()
else:
self.batch_id = get_name_from_hash()
@ -50,13 +110,17 @@ class Batch(Document):
def onload(self):
self.image = frappe.db.get_value('Item', self.item, 'image')
def after_delete(self):
revert_series_if_last(get_batch_naming_series(), self.name)
def validate(self):
self.item_has_batch_enabled()
def item_has_batch_enabled(self):
if frappe.db.get_value("Item",self.item,"has_batch_no") == 0:
if frappe.db.get_value("Item", self.item, "has_batch_no") == 0:
frappe.throw(_("The selected item cannot have Batch"))
@frappe.whitelist()
def get_batch_qty(batch_no=None, warehouse=None, item_code=None):
"""Returns batch actual qty if warehouse is passed,
@ -89,16 +153,18 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None):
return out
@frappe.whitelist()
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 = 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.sort(key=lambda tup: tup[1])
return batches_dates
@frappe.whitelist()
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id = None):
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
"""Split the batch into a new batch"""
batch = frappe.get_doc(dict(doctype='Batch', item=item_code, batch_id=new_batch_id)).insert()
stock_entry = frappe.get_doc(dict(
@ -106,16 +172,16 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id = None):
purpose='Repack',
items=[
dict(
item_code = item_code,
qty = float(qty or 0),
s_warehouse = warehouse,
batch_no = batch_no
item_code=item_code,
qty=float(qty or 0),
s_warehouse=warehouse,
batch_no=batch_no
),
dict(
item_code = item_code,
qty = float(qty or 0),
t_warehouse = warehouse,
batch_no = batch.name
item_code=item_code,
qty=float(qty or 0),
t_warehouse=warehouse,
batch_no=batch.name
),
]
))
@ -124,7 +190,8 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id = None):
return batch.name
def set_batch_nos(doc, warehouse_field, throw = False):
def set_batch_nos(doc, warehouse_field, throw=False):
"""Automatically select `batch_no` for outgoing items in item table"""
for d in doc.items:
qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0
@ -138,6 +205,7 @@ def set_batch_nos(doc, warehouse_field, throw = False):
if flt(batch_qty) < flt(qty):
frappe.throw(_("Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches").format(d.idx, d.batch_no, batch_qty, d.qty))
@frappe.whitelist()
def get_batch_no(item_code, warehouse, qty=1, throw=False):
"""

View File

@ -7,6 +7,8 @@ from frappe.exceptions import ValidationError
import unittest
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
from frappe.utils import cint
class TestBatch(unittest.TestCase):
@ -21,7 +23,7 @@ class TestBatch(unittest.TestCase):
def make_batch_item(cls, item_name):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists(item_name):
make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1))
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1))
def test_purchase_receipt(self, batch_qty = 100):
'''Test automated batch creation from Purchase Receipt'''
@ -192,3 +194,37 @@ class TestBatch(unittest.TestCase):
]
)).insert()
stock_entry.submit()
def test_batch_name_with_naming_series(self):
stock_settings = frappe.get_single('Stock Settings')
use_naming_series = cint(stock_settings.use_naming_series)
if not use_naming_series:
frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 1)
batch = self.make_new_batch('_Test Stock Item For Batch Test1')
batch_name = batch.name
self.assertTrue(batch_name.startswith('BATCH-'))
batch.delete()
batch = self.make_new_batch('_Test Stock Item For Batch Test2')
self.assertEqual(batch_name, batch.name)
# reset Stock Settings
if not use_naming_series:
frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 0)
def make_new_batch(self, item_name, batch_id=None, do_not_insert=0):
batch = frappe.new_doc('Batch')
item = self.make_batch_item(item_name)
batch.item = item.name
if batch_id:
batch.batch_id = batch_id
if not do_not_insert:
batch.insert()
return batch