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:
		| @@ -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