mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 04:26:44 +00:00
Merge pull request #2686 from SchrodingersGat/po-receive-serials
Po receive serials
This commit is contained in:
commit
f3e7af3cc3
@ -407,14 +407,16 @@ def DownloadFile(data, filename, content_type='application/text', inline=False):
|
|||||||
|
|
||||||
|
|
||||||
def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||||
""" Attempt to extract serial numbers from an input string.
|
"""
|
||||||
- Serial numbers must be integer values
|
Attempt to extract serial numbers from an input string:
|
||||||
- Serial numbers must be positive
|
|
||||||
- Serial numbers can be split by whitespace / newline / commma chars
|
Requirements:
|
||||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
- Serial numbers can be either strings, or integers
|
||||||
- Serial numbers can be defined as ~ for getting the next available serial number
|
- Serial numbers can be split by whitespace / newline / commma chars
|
||||||
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||||
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
- Serial numbers can be defined as ~ for getting the next available serial number
|
||||||
|
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||||
|
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
serials: input string with patterns
|
serials: input string with patterns
|
||||||
@ -428,17 +430,18 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
if '~' in serials:
|
if '~' in serials:
|
||||||
serials = serials.replace('~', str(next_number))
|
serials = serials.replace('~', str(next_number))
|
||||||
|
|
||||||
|
# Split input string by whitespace or comma (,) characters
|
||||||
groups = re.split("[\s,]+", serials)
|
groups = re.split("[\s,]+", serials)
|
||||||
|
|
||||||
numbers = []
|
numbers = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# helpers
|
# Helper function to check for duplicated numbers
|
||||||
def number_add(n):
|
def add_sn(sn):
|
||||||
if n in numbers:
|
if sn in numbers:
|
||||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
errors.append(_('Duplicate serial: {sn}').format(sn=sn))
|
||||||
else:
|
else:
|
||||||
numbers.append(n)
|
numbers.append(sn)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
expected_quantity = int(expected_quantity)
|
expected_quantity = int(expected_quantity)
|
||||||
@ -466,7 +469,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
|
|
||||||
if a < b:
|
if a < b:
|
||||||
for n in range(a, b + 1):
|
for n in range(a, b + 1):
|
||||||
number_add(n)
|
add_sn(n)
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
@ -495,21 +498,20 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
|||||||
end = start + expected_quantity
|
end = start + expected_quantity
|
||||||
|
|
||||||
for n in range(start, end):
|
for n in range(start, end):
|
||||||
number_add(n)
|
add_sn(n)
|
||||||
# no case
|
# no case
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
# Group should be a number
|
# At this point, we assume that the "group" is just a single serial value
|
||||||
elif group:
|
elif group:
|
||||||
# try conversion
|
|
||||||
try:
|
|
||||||
number = int(group)
|
|
||||||
except:
|
|
||||||
# seem like it is not a number
|
|
||||||
raise ValidationError(_(f"Invalid group {group}"))
|
|
||||||
|
|
||||||
number_add(number)
|
try:
|
||||||
|
# First attempt to add as an integer value
|
||||||
|
add_sn(int(group))
|
||||||
|
except (ValueError):
|
||||||
|
# As a backup, add as a string value
|
||||||
|
add_sn(group)
|
||||||
|
|
||||||
# No valid input group detected
|
# No valid input group detected
|
||||||
else:
|
else:
|
||||||
|
@ -14,6 +14,8 @@ from django_q.monitor import Stat
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
import InvenTree.ready
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
@ -56,6 +58,12 @@ def is_email_configured():
|
|||||||
|
|
||||||
configured = True
|
configured = True
|
||||||
|
|
||||||
|
if InvenTree.ready.isInTestMode():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if InvenTree.ready.isImportingData():
|
||||||
|
return False
|
||||||
|
|
||||||
if not settings.EMAIL_HOST:
|
if not settings.EMAIL_HOST:
|
||||||
configured = False
|
configured = False
|
||||||
|
|
||||||
@ -89,6 +97,14 @@ def check_system_health(**kwargs):
|
|||||||
|
|
||||||
result = True
|
result = True
|
||||||
|
|
||||||
|
if InvenTree.ready.isInTestMode():
|
||||||
|
# Do not perform further checks if we are running unit tests
|
||||||
|
return False
|
||||||
|
|
||||||
|
if InvenTree.ready.isImportingData():
|
||||||
|
# Do not perform further checks if we are importing data
|
||||||
|
return False
|
||||||
|
|
||||||
if not is_worker_running(**kwargs): # pragma: no cover
|
if not is_worker_running(**kwargs): # pragma: no cover
|
||||||
result = False
|
result = False
|
||||||
logger.warning(_("Background worker check failed"))
|
logger.warning(_("Background worker check failed"))
|
||||||
|
@ -398,12 +398,22 @@ class PurchaseOrder(Order):
|
|||||||
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs):
|
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
|
||||||
""" Receive a line item (or partial line item) against this PO
|
"""
|
||||||
|
Receive a line item (or partial line item) against this PO
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Extract optional batch code for the new stock item
|
||||||
|
batch_code = kwargs.get('batch_code', '')
|
||||||
|
|
||||||
|
# Extract optional list of serial numbers
|
||||||
|
serials = kwargs.get('serials', None)
|
||||||
|
|
||||||
|
# Extract optional notes field
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
barcode = kwargs.get('barcode', '')
|
|
||||||
|
# Extract optional barcode field
|
||||||
|
barcode = kwargs.get('barcode', None)
|
||||||
|
|
||||||
# Prevent null values for barcode
|
# Prevent null values for barcode
|
||||||
if barcode is None:
|
if barcode is None:
|
||||||
@ -427,33 +437,45 @@ class PurchaseOrder(Order):
|
|||||||
|
|
||||||
# Create a new stock item
|
# Create a new stock item
|
||||||
if line.part and quantity > 0:
|
if line.part and quantity > 0:
|
||||||
stock = stock_models.StockItem(
|
|
||||||
part=line.part.part,
|
|
||||||
supplier_part=line.part,
|
|
||||||
location=location,
|
|
||||||
quantity=quantity,
|
|
||||||
purchase_order=self,
|
|
||||||
status=status,
|
|
||||||
purchase_price=line.purchase_price,
|
|
||||||
uid=barcode
|
|
||||||
)
|
|
||||||
|
|
||||||
stock.save(add_note=False)
|
# Determine if we should individually serialize the items, or not
|
||||||
|
if type(serials) is list and len(serials) > 0:
|
||||||
|
serialize = True
|
||||||
|
else:
|
||||||
|
serialize = False
|
||||||
|
serials = [None]
|
||||||
|
|
||||||
tracking_info = {
|
for sn in serials:
|
||||||
'status': status,
|
|
||||||
'purchaseorder': self.pk,
|
|
||||||
}
|
|
||||||
|
|
||||||
stock.add_tracking_entry(
|
stock = stock_models.StockItem(
|
||||||
StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER,
|
part=line.part.part,
|
||||||
user,
|
supplier_part=line.part,
|
||||||
notes=notes,
|
location=location,
|
||||||
deltas=tracking_info,
|
quantity=1 if serialize else quantity,
|
||||||
location=location,
|
purchase_order=self,
|
||||||
purchaseorder=self,
|
status=status,
|
||||||
quantity=quantity
|
batch=batch_code,
|
||||||
)
|
serial=sn,
|
||||||
|
purchase_price=line.purchase_price,
|
||||||
|
uid=barcode
|
||||||
|
)
|
||||||
|
|
||||||
|
stock.save(add_note=False)
|
||||||
|
|
||||||
|
tracking_info = {
|
||||||
|
'status': status,
|
||||||
|
'purchaseorder': self.pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
stock.add_tracking_entry(
|
||||||
|
StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER,
|
||||||
|
user,
|
||||||
|
notes=notes,
|
||||||
|
deltas=tracking_info,
|
||||||
|
location=location,
|
||||||
|
purchaseorder=self,
|
||||||
|
quantity=quantity
|
||||||
|
)
|
||||||
|
|
||||||
# Update the number of parts received against the particular line item
|
# Update the number of parts received against the particular line item
|
||||||
line.received += quantity
|
line.received += quantity
|
||||||
|
@ -5,6 +5,8 @@ JSON serializers for the Order API
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
@ -203,6 +205,17 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
A serializer for receiving a single purchase order line item against a purchase order
|
A serializer for receiving a single purchase order line item against a purchase order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'barcode',
|
||||||
|
'line_item',
|
||||||
|
'location',
|
||||||
|
'quantity',
|
||||||
|
'status',
|
||||||
|
'batch_code'
|
||||||
|
'serial_numbers',
|
||||||
|
]
|
||||||
|
|
||||||
line_item = serializers.PrimaryKeyRelatedField(
|
line_item = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=order.models.PurchaseOrderLineItem.objects.all(),
|
queryset=order.models.PurchaseOrderLineItem.objects.all(),
|
||||||
many=False,
|
many=False,
|
||||||
@ -241,6 +254,22 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
|
batch_code = serializers.CharField(
|
||||||
|
label=_('Batch Code'),
|
||||||
|
help_text=_('Enter batch code for incoming stock items'),
|
||||||
|
required=False,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_numbers = serializers.CharField(
|
||||||
|
label=_('Serial Numbers'),
|
||||||
|
help_text=_('Enter serial numbers for incoming stock items'),
|
||||||
|
required=False,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status = serializers.ChoiceField(
|
||||||
choices=list(StockStatus.items()),
|
choices=list(StockStatus.items()),
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK,
|
||||||
@ -270,14 +299,35 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return barcode
|
return barcode
|
||||||
|
|
||||||
class Meta:
|
def validate(self, data):
|
||||||
fields = [
|
|
||||||
'barcode',
|
data = super().validate(data)
|
||||||
'line_item',
|
|
||||||
'location',
|
line_item = data['line_item']
|
||||||
'quantity',
|
quantity = data['quantity']
|
||||||
'status',
|
serial_numbers = data.get('serial_numbers', '').strip()
|
||||||
]
|
|
||||||
|
base_part = line_item.part.part
|
||||||
|
|
||||||
|
# Does the quantity need to be "integer" (for trackable parts?)
|
||||||
|
if base_part.trackable:
|
||||||
|
|
||||||
|
if Decimal(quantity) != int(quantity):
|
||||||
|
raise ValidationError({
|
||||||
|
'quantity': _('An integer quantity must be provided for trackable parts'),
|
||||||
|
})
|
||||||
|
|
||||||
|
# If serial numbers are provided
|
||||||
|
if serial_numbers:
|
||||||
|
try:
|
||||||
|
# Pass the serial numbers through to the parent serializer once validated
|
||||||
|
data['serials'] = extract_serial_numbers(serial_numbers, quantity, base_part.getLatestSerialNumberInt())
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
raise ValidationError({
|
||||||
|
'serial_numbers': e.messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class POReceiveSerializer(serializers.Serializer):
|
class POReceiveSerializer(serializers.Serializer):
|
||||||
@ -366,6 +416,8 @@ class POReceiveSerializer(serializers.Serializer):
|
|||||||
request.user,
|
request.user,
|
||||||
status=item['status'],
|
status=item['status'],
|
||||||
barcode=item.get('barcode', ''),
|
barcode=item.get('barcode', ''),
|
||||||
|
batch_code=item.get('batch_code', ''),
|
||||||
|
serials=item.get('serials', None),
|
||||||
)
|
)
|
||||||
except (ValidationError, DjangoValidationError) as exc:
|
except (ValidationError, DjangoValidationError) as exc:
|
||||||
# Catch model errors and re-throw as DRF errors
|
# Catch model errors and re-throw as DRF errors
|
||||||
|
@ -529,6 +529,108 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-123').exists())
|
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-123').exists())
|
||||||
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
|
self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists())
|
||||||
|
|
||||||
|
def test_batch_code(self):
|
||||||
|
"""
|
||||||
|
Test that we can supply a 'batch code' when receiving items
|
||||||
|
"""
|
||||||
|
|
||||||
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||||
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||||
|
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'line_item': 1,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'abc-123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'line_item': 2,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'xyz-789',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'location': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
n = StockItem.objects.count()
|
||||||
|
|
||||||
|
self.post(
|
||||||
|
self.url,
|
||||||
|
data,
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that two new stock items have been created!
|
||||||
|
self.assertEqual(n + 2, StockItem.objects.count())
|
||||||
|
|
||||||
|
item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
|
||||||
|
item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()
|
||||||
|
|
||||||
|
self.assertEqual(item_1.batch, 'abc-123')
|
||||||
|
self.assertEqual(item_2.batch, 'xyz-789')
|
||||||
|
|
||||||
|
def test_serial_numbers(self):
|
||||||
|
"""
|
||||||
|
Test that we can supply a 'serial number' when receiving items
|
||||||
|
"""
|
||||||
|
|
||||||
|
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
|
||||||
|
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
|
||||||
|
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0)
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'line_item': 1,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'abc-123',
|
||||||
|
'serial_numbers': '100+',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'line_item': 2,
|
||||||
|
'quantity': 10,
|
||||||
|
'batch_code': 'xyz-789',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'location': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
n = StockItem.objects.count()
|
||||||
|
|
||||||
|
self.post(
|
||||||
|
self.url,
|
||||||
|
data,
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the expected number of stock items has been created
|
||||||
|
self.assertEqual(n + 11, StockItem.objects.count())
|
||||||
|
|
||||||
|
# 10 serialized stock items created for the first line item
|
||||||
|
self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 10)
|
||||||
|
|
||||||
|
# Check that the correct serial numbers have been allocated
|
||||||
|
for i in range(100, 110):
|
||||||
|
item = StockItem.objects.get(serial_int=i)
|
||||||
|
self.assertEqual(item.serial, str(i))
|
||||||
|
self.assertEqual(item.quantity, 1)
|
||||||
|
self.assertEqual(item.batch, 'abc-123')
|
||||||
|
|
||||||
|
# A single stock item (quantity 10) created for the second line item
|
||||||
|
items = StockItem.objects.filter(supplier_part=line_2.part)
|
||||||
|
self.assertEqual(items.count(), 1)
|
||||||
|
|
||||||
|
item = items.first()
|
||||||
|
|
||||||
|
self.assertEqual(item.quantity, 10)
|
||||||
|
self.assertEqual(item.batch, 'xyz-789')
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderTest(OrderTest):
|
class SalesOrderTest(OrderTest):
|
||||||
"""
|
"""
|
||||||
|
@ -1949,7 +1949,7 @@ function getFieldName(name, options={}) {
|
|||||||
* - Field description (help text)
|
* - Field description (help text)
|
||||||
* - Field errors
|
* - Field errors
|
||||||
*/
|
*/
|
||||||
function constructField(name, parameters, options) {
|
function constructField(name, parameters, options={}) {
|
||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
|
|
||||||
@ -2041,7 +2041,7 @@ function constructField(name, parameters, options) {
|
|||||||
html += `<div class='controls'>`;
|
html += `<div class='controls'>`;
|
||||||
|
|
||||||
// Does this input deserve "extra" decorators?
|
// Does this input deserve "extra" decorators?
|
||||||
var extra = parameters.prefix != null;
|
var extra = (parameters.icon != null) || (parameters.prefix != null) || (parameters.prefixRaw != null);
|
||||||
|
|
||||||
// Some fields can have 'clear' inputs associated with them
|
// Some fields can have 'clear' inputs associated with them
|
||||||
if (!parameters.required && !parameters.read_only) {
|
if (!parameters.required && !parameters.read_only) {
|
||||||
@ -2066,6 +2066,10 @@ function constructField(name, parameters, options) {
|
|||||||
|
|
||||||
if (parameters.prefix) {
|
if (parameters.prefix) {
|
||||||
html += `<span class='input-group-text'>${parameters.prefix}</span>`;
|
html += `<span class='input-group-text'>${parameters.prefix}</span>`;
|
||||||
|
} else if (parameters.prefixRaw) {
|
||||||
|
html += parameters.prefixRaw;
|
||||||
|
} else if (parameters.icon) {
|
||||||
|
html += `<span class='input-group-text'><span class='fas ${parameters.icon}'></span></span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2212,6 +2216,10 @@ function constructInputOptions(name, classes, type, parameters, options={}) {
|
|||||||
|
|
||||||
opts.push(`type='${type}'`);
|
opts.push(`type='${type}'`);
|
||||||
|
|
||||||
|
if (parameters.title || parameters.help_text) {
|
||||||
|
opts.push(`title='${parameters.title || parameters.help_text}'`);
|
||||||
|
}
|
||||||
|
|
||||||
// Read only?
|
// Read only?
|
||||||
if (parameters.read_only) {
|
if (parameters.read_only) {
|
||||||
opts.push(`readonly=''`);
|
opts.push(`readonly=''`);
|
||||||
@ -2257,11 +2265,6 @@ function constructInputOptions(name, classes, type, parameters, options={}) {
|
|||||||
opts.push(`required=''`);
|
opts.push(`required=''`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom mouseover title?
|
|
||||||
if (parameters.title != null) {
|
|
||||||
opts.push(`title='${parameters.title}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder?
|
// Placeholder?
|
||||||
if (parameters.placeholder != null) {
|
if (parameters.placeholder != null) {
|
||||||
opts.push(`placeholder='${parameters.placeholder}'`);
|
opts.push(`placeholder='${parameters.placeholder}'`);
|
||||||
|
@ -116,6 +116,10 @@ function makeIconButton(icon, cls, pk, title, options={}) {
|
|||||||
extraProps += `disabled='true' `;
|
extraProps += `disabled='true' `;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.collapseTarget) {
|
||||||
|
extraProps += `data-bs-toggle='collapse' href='#${options.collapseTarget}'`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
||||||
html += `<span class='fas ${icon}'></span>`;
|
html += `<span class='fas ${icon}'></span>`;
|
||||||
html += `</button>`;
|
html += `</button>`;
|
||||||
|
@ -476,6 +476,19 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
quantity = 0;
|
quantity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepend toggles to the quantity input
|
||||||
|
var toggle_batch = `
|
||||||
|
<span class='input-group-text' title='{% trans "Add batch code" %}' data-bs-toggle='collapse' href='#div-batch-${pk}'>
|
||||||
|
<span class='fas fa-layer-group'></span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
var toggle_serials = `
|
||||||
|
<span class='input-group-text' title='{% trans "Add serial numbers" %}' data-bs-toggle='collapse' href='#div-serials-${pk}'>
|
||||||
|
<span class='fas fa-hashtag'></span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
// Quantity to Receive
|
// Quantity to Receive
|
||||||
var quantity_input = constructField(
|
var quantity_input = constructField(
|
||||||
`items_quantity_${pk}`,
|
`items_quantity_${pk}`,
|
||||||
@ -491,6 +504,36 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add in options for "batch code" and "serial numbers"
|
||||||
|
var batch_input = constructField(
|
||||||
|
`items_batch_code_${pk}`,
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
label: '{% trans "Batch Code" %}',
|
||||||
|
help_text: '{% trans "Enter batch code for incoming stock items" %}',
|
||||||
|
prefixRaw: toggle_batch,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var sn_input = constructField(
|
||||||
|
`items_serial_numbers_${pk}`,
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
label: '{% trans "Serial Numbers" %}',
|
||||||
|
help_text: '{% trans "Enter serial numbers for incoming stock items" %}',
|
||||||
|
prefixRaw: toggle_serials,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hidden inputs below the "quantity" field
|
||||||
|
var quantity_input_group = `${quantity_input}<div class='collapse' id='div-batch-${pk}'>${batch_input}</div>`;
|
||||||
|
|
||||||
|
if (line_item.part_detail.trackable) {
|
||||||
|
quantity_input_group += `<div class='collapse' id='div-serials-${pk}'>${sn_input}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Construct list of StockItem status codes
|
// Construct list of StockItem status codes
|
||||||
var choices = [];
|
var choices = [];
|
||||||
|
|
||||||
@ -528,16 +571,38 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Button to remove the row
|
// Button to remove the row
|
||||||
var delete_button = `<div class='btn-group float-right' role='group'>`;
|
var buttons = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
delete_button += makeIconButton(
|
buttons += makeIconButton(
|
||||||
|
'fa-layer-group',
|
||||||
|
'button-row-add-batch',
|
||||||
|
pk,
|
||||||
|
'{% trans "Add batch code" %}',
|
||||||
|
{
|
||||||
|
collapseTarget: `div-batch-${pk}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (line_item.part_detail.trackable) {
|
||||||
|
buttons += makeIconButton(
|
||||||
|
'fa-hashtag',
|
||||||
|
'button-row-add-serials',
|
||||||
|
pk,
|
||||||
|
'{% trans "Add serial numbers" %}',
|
||||||
|
{
|
||||||
|
collapseTarget: `div-serials-${pk}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons += makeIconButton(
|
||||||
'fa-times icon-red',
|
'fa-times icon-red',
|
||||||
'button-row-remove',
|
'button-row-remove',
|
||||||
pk,
|
pk,
|
||||||
'{% trans "Remove row" %}',
|
'{% trans "Remove row" %}',
|
||||||
);
|
);
|
||||||
|
|
||||||
delete_button += '</div>';
|
buttons += '</div>';
|
||||||
|
|
||||||
var html = `
|
var html = `
|
||||||
<tr id='receive_row_${pk}' class='stock-receive-row'>
|
<tr id='receive_row_${pk}' class='stock-receive-row'>
|
||||||
@ -554,7 +619,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
${line_item.received}
|
${line_item.received}
|
||||||
</td>
|
</td>
|
||||||
<td id='quantity_${pk}'>
|
<td id='quantity_${pk}'>
|
||||||
${quantity_input}
|
${quantity_input_group}
|
||||||
</td>
|
</td>
|
||||||
<td id='status_${pk}'>
|
<td id='status_${pk}'>
|
||||||
${status_input}
|
${status_input}
|
||||||
@ -563,7 +628,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
${destination_input}
|
${destination_input}
|
||||||
</td>
|
</td>
|
||||||
<td id='actions_${pk}'>
|
<td id='actions_${pk}'>
|
||||||
${delete_button}
|
${buttons}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
@ -587,7 +652,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
<th>{% trans "Order Code" %}</th>
|
<th>{% trans "Order Code" %}</th>
|
||||||
<th>{% trans "Ordered" %}</th>
|
<th>{% trans "Ordered" %}</th>
|
||||||
<th>{% trans "Received" %}</th>
|
<th>{% trans "Received" %}</th>
|
||||||
<th style='min-width: 50px;'>{% trans "Receive" %}</th>
|
<th style='min-width: 50px;'>{% trans "Quantity to Receive" %}</th>
|
||||||
<th style='min-width: 150px;'>{% trans "Status" %}</th>
|
<th style='min-width: 150px;'>{% trans "Status" %}</th>
|
||||||
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
|
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -678,13 +743,23 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
|
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
|
||||||
|
|
||||||
if (quantity != null) {
|
if (quantity != null) {
|
||||||
data.items.push({
|
|
||||||
|
var line = {
|
||||||
line_item: pk,
|
line_item: pk,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
status: status,
|
status: status,
|
||||||
location: location,
|
location: location,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (getFormFieldElement(`items_batch_code_${pk}`).exists()) {
|
||||||
|
line.batch_code = getFormFieldValue(`items_batch_code_${pk}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFormFieldElement(`items_serial_numbers_${pk}`).exists()) {
|
||||||
|
line.serial_numbers = getFormFieldValue(`items_serial_numbers_${pk}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.items.push(line);
|
||||||
item_pk_values.push(pk);
|
item_pk_values.push(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user