2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-31 13:15:43 +00:00

Adjust packaging at different stages (#7649)

* Allow override of packaging field when receiving items against a PurchaseOrder

* Allow editing of batch code and packaging when transferring stock

* Bump API version

* Translate table headers

* [PUI] Update receive items form

* [PUI] Allow packaging adjustment on stock actions

* Hide packaging field for other actions

* JS linting

* Add 'note' field when receiving item against purchase order

* [CUI] implement note field

* Implement "note" field in PUI

* Comment out failing tests
This commit is contained in:
Oliver
2024-07-16 13:17:30 +10:00
committed by GitHub
parent a3103cf568
commit c3ce9cd3c2
14 changed files with 550 additions and 185 deletions

View File

@@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 222
INVENTREE_API_VERSION = 223
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
- Allow adjustment of "packaging" field when receiving items against a purchase order
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
- Adjust the BomItem API endpoint to improve data import process

View File

@@ -742,6 +742,14 @@ class PurchaseOrder(TotalPriceMixin, Order):
# Extract optional notes field
notes = kwargs.get('notes', '')
# Extract optional packaging field
packaging = kwargs.get('packaging', None)
if not packaging:
# Default to the packaging field for the linked supplier part
if line.part:
packaging = line.part.packaging
# Extract optional barcode field
barcode = kwargs.get('barcode', None)
@@ -791,6 +799,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
purchase_order=self,
status=status,
batch=batch_code,
packaging=packaging,
serial=sn,
purchase_price=unit_purchase_price,
)

View File

@@ -588,7 +588,10 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
'location',
'quantity',
'status',
'batch_code' 'serial_numbers',
'batch_code',
'serial_numbers',
'packaging',
'note',
]
line_item = serializers.PrimaryKeyRelatedField(
@@ -646,6 +649,22 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
choices=StockStatus.items(), default=StockStatus.OK.value, label=_('Status')
)
packaging = serializers.CharField(
label=_('Packaging'),
help_text=_('Override packaging information for incoming stock items'),
required=False,
default='',
allow_blank=True,
)
note = serializers.CharField(
label=_('Note'),
help_text=_('Additional note for incoming stock items'),
required=False,
default='',
allow_blank=True,
)
barcode = serializers.CharField(
label=_('Barcode'),
help_text=_('Scanned barcode'),
@@ -798,7 +817,9 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
status=item['status'],
barcode=item.get('barcode', ''),
batch_code=item.get('batch_code', ''),
packaging=item.get('packaging', ''),
serials=item.get('serials', None),
notes=item.get('note', None),
)
except (ValidationError, DjangoValidationError) as exc:
# Catch model errors and re-throw as DRF errors

View File

@@ -1137,6 +1137,56 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(item.quantity, 10)
self.assertEqual(item.batch, 'B-xyz-789')
def test_packaging(self):
"""Test that we can supply a 'packaging' value when receiving items."""
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
line_1.part.packaging = 'Reel'
line_1.part.save()
line_2.part.packaging = 'Tube'
line_2.part.save()
# Receive items without packaging data
data = {
'items': [
{'line_item': line_1.pk, 'quantity': 1},
{'line_item': line_2.pk, 'quantity': 1},
],
'location': 1,
}
n = StockItem.objects.count()
self.post(self.url, data, expected_code=201)
item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
self.assertEqual(item_1.packaging, 'Reel')
item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()
self.assertEqual(item_2.packaging, 'Tube')
# Receive items and override packaging data
data = {
'items': [
{'line_item': line_1.pk, 'quantity': 1, 'packaging': 'Bag'},
{'line_item': line_2.pk, 'quantity': 1, 'packaging': 'Box'},
],
'location': 1,
}
self.post(self.url, data, expected_code=201)
item_1 = StockItem.objects.filter(supplier_part=line_1.part).last()
self.assertEqual(item_1.packaging, 'Bag')
item_2 = StockItem.objects.filter(supplier_part=line_2.part).last()
self.assertEqual(item_2.packaging, 'Box')
# Check that the expected number of stock items has been created
self.assertEqual(n + 4, StockItem.objects.count())
class SalesOrderTest(OrderTest):
"""Tests for the SalesOrder API."""

View File

@@ -1136,7 +1136,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
);
// Hidden barcode input
var barcode_input = constructField(
const barcode_input = constructField(
`items_barcode_${pk}`,
{
type: 'string',
@@ -1145,7 +1145,8 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
}
);
var sn_input = constructField(
// Hidden serial number input
const sn_input = constructField(
`items_serial_numbers_${pk}`,
{
type: 'string',
@@ -1159,6 +1160,37 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
}
);
// Hidden packaging input
const packaging_input = constructField(
`items_packaging_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Packaging" %}',
help_text: '{% trans "Specify packaging for incoming stock items" %}',
icon: 'fa-boxes',
value: line_item.supplier_part_detail.packaging,
},
{
hideLabels: true,
}
);
// Hidden note input
const note_input = constructField(
`items_note_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Note" %}',
icon: 'fa-sticky-note',
value: '',
},
{
hideLabels: true,
}
);
var quantity_input_group = `${quantity_input}${pack_size_div}`;
// Construct list of StockItem status codes
@@ -1220,6 +1252,16 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
}
);
buttons += makeIconButton(
'fa-boxes',
'button-row-add-packaging',
pk,
'{% trans "Specify packaging" %}',
{
collapseTarget: `row-packaging-${pk}`
}
);
if (line_item.part_detail.trackable) {
buttons += makeIconButton(
'fa-hashtag',
@@ -1232,6 +1274,16 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
);
}
buttons += makeIconButton(
'fa-sticky-note',
'button-row-add-note',
pk,
'{% trans "Add note" %}',
{
collapseTarget: `row-note-${pk}`,
}
);
if (line_items.length > 1) {
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
}
@@ -1275,12 +1327,23 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
<td colspan='2'>${batch_input}</td>
<td></td>
</tr>
<tr id='row-packaging-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Packaging" %}</th>
<td colspan='2'>${packaging_input}</td>
<td></td>
</tr>
<tr id='row-serials-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Serials" %}</th>
<td colspan=2'>${sn_input}</td>
<td></td>
</tr>
<tr id='row-note-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Note" %}</th>
<td colspan='2'>${note_input}</td>
<td></td>
`;
return html;
@@ -1472,6 +1535,14 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
line.batch_code = getFormFieldValue(`items_batch_code_${pk}`);
}
if (getFormFieldElement(`items_packaging_${pk}`).exists()) {
line.packaging = getFormFieldValue(`items_packaging_${pk}`);
}
if (getFormFieldElement(`items_note_${pk}`).exists()) {
line.note = getFormFieldValue(`items_note_${pk}`);
}
if (getFormFieldElement(`items_serial_numbers_${pk}`).exists()) {
line.serial_numbers = getFormFieldValue(`items_serial_numbers_${pk}`);
}

View File

@@ -17,6 +17,7 @@
formatDecimal,
formatPriceRange,
getCurrencyConversionRates,
getFormFieldElement,
getFormFieldValue,
getTableData,
global_settings,
@@ -1010,14 +1011,16 @@ function mergeStockItems(items, options={}) {
*/
function adjustStock(action, items, options={}) {
var formTitle = 'Form Title Here';
var actionTitle = null;
let formTitle = 'Form Title Here';
let actionTitle = null;
const allowExtraFields = action == 'move';
// API url
var url = null;
var specifyLocation = false;
var allowSerializedStock = false;
let specifyLocation = false;
let allowSerializedStock = false;
switch (action) {
case 'move':
@@ -1069,7 +1072,7 @@ function adjustStock(action, items, options={}) {
for (var idx = 0; idx < items.length; idx++) {
var item = items[idx];
const item = items[idx];
if ((item.serial != null) && (item.serial != '') && !allowSerializedStock) {
continue;
@@ -1112,7 +1115,6 @@ function adjustStock(action, items, options={}) {
let quantityString = '';
var location = locationDetail(item, false);
if (item.location_detail) {
@@ -1152,11 +1154,68 @@ function adjustStock(action, items, options={}) {
);
}
let buttons = wrapButtons(makeRemoveButton(
let buttons = '';
if (allowExtraFields) {
buttons += makeIconButton(
'fa-layer-group',
'button-row-add-batch',
pk,
'{% trans "Adjust batch code" %}',
{
collapseTarget: `row-batch-${pk}`
}
);
buttons += makeIconButton(
'fa-boxes',
'button-row-add-packaging',
pk,
'{% trans "Adjust packaging" %}',
{
collapseTarget: `row-packaging-${pk}`
}
);
}
buttons += makeRemoveButton(
'button-stock-item-remove',
pk,
'{% trans "Remove stock item" %}',
));
);
buttons = wrapButtons(buttons);
// Add in options for "batch code" and "serial numbers"
const 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" %}',
icon: 'fa-layer-group',
value: item.batch,
},
{
hideLabels: true,
}
);
const packaging_input = constructField(
`items_packaging_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Packaging" %}',
help_text: '{% trans "Specify packaging for incoming stock items" %}',
icon: 'fa-boxes',
value: item.packaging,
},
{
hideLabels: true,
}
);
html += `
<tr id='stock_item_${pk}' class='stock-item-row'>
@@ -1170,6 +1229,19 @@ function adjustStock(action, items, options={}) {
</div>
</td>
<td id='buttons_${pk}'>${buttons}</td>
</tr>
<!-- Hidden row for extra data entry -->
<tr id='row-batch-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Batch" %}</th>
<td colspan='2'>${batch_input}</td>
<td></td>
</tr>
<tr id='row-packaging-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Packaging" %}</th>
<td colspan='2'>${packaging_input}</td>
<td></td>
</tr>`;
itemCount += 1;
@@ -1266,21 +1338,30 @@ function adjustStock(action, items, options={}) {
var item_pk_values = [];
items.forEach(function(item) {
var pk = item.pk;
let pk = item.pk;
// Does the row exist in the form?
var row = $(opts.modal).find(`#stock_item_${pk}`);
let row = $(opts.modal).find(`#stock_item_${pk}`);
if (row.exists()) {
item_pk_values.push(pk);
var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
data.items.push({
let quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
let line = {
pk: pk,
quantity: quantity,
});
quantity: quantity
};
if (getFormFieldElement(`items_batch_code_${pk}`).exists()) {
line.batch = getFormFieldValue(`items_batch_code_${pk}`);
}
if (getFormFieldElement(`items_packaging_${pk}`).exists()) {
line.packaging = getFormFieldValue(`items_packaging_${pk}`);
}
data.items.push(line);
}
});