mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-16 17:28:11 +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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user