2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-06 08:54:24 +00:00

Update to /api/barcode/po-recieve/ error messages (#11369)

* Update Dockerfile

* Update Dockerfile

* Added More Info In Barcode API Scans

Added DebugResponse
Added Suppliers Tested
Added Suppliers Errors

* Add NotFound to base Response

* Added Simple Plugin Debug

Changed "NotFound" to 'No_Match'
different style for Debug response

* Added finishing touches for API Error

* clean up + comments

* Added No Supplier Plugin error

* fix style

* Formatting and fixing comments

Added more variables
updated formatting
Added more in depth explanation block for Debug response

* duplicate code fix

* fixed some variable names

* Small move of variables

* Updated To Nested Debug Dictionary

fixed some issues with comments made in original PR
added more comments and small adjustments
got rid of "line item" in debug response

* Added notes to documentation

* Update docs/docs/app/barcode.md

adding matmirs comments

Co-authored-by: Matthias Mair <code@mjmair.com>

* Updated JSON plugin debug name

also added 'plugin_slug' variable

* fix style

* DNE style fix

* Style fix(again)

* Add unit tests

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
Idea-Junkie
2026-05-31 04:45:23 -04:00
committed by GitHub
parent b2b476b570
commit 4b415cb8ae
4 changed files with 207 additions and 15 deletions
+4
View File
@@ -67,6 +67,8 @@ Transfer the currently selected stock location into another location. Scanning a
Receive incoming purchase order items into the selected location. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into the selected location. Receive incoming purchase order items into the selected location. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into the selected location.
*Note: Both purchase order number and supplier SKU's are required to be found on the barcode for this function to find the associated line item. Missing one will lead to an error.*
#### Scan Items Into Location #### Scan Items Into Location
the *Scan Items Into Location* action allows you to scan items into the selected location. Scanning a valid barcode associated with a stock item (already in the database) will result in that item being transferred to the selected location. the *Scan Items Into Location* action allows you to scan items into the selected location. Scanning a valid barcode associated with a stock item (already in the database) will result in that item being transferred to the selected location.
@@ -105,6 +107,8 @@ From the [Purchase Order detail page](./po.md#purchase-order-detail) page, the f
Receive incoming purchase order items against the selected purchase order. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into stock. Receive incoming purchase order items against the selected purchase order. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into stock.
*Note: supplier SKU's are required to be found on the barcode for this function to find the associated line item.*
### Sales Order Actions ### Sales Order Actions
The following barcode actions are available for [Sales Orders](./so.md): The following barcode actions are available for [Sales Orders](./so.md):
@@ -533,10 +533,23 @@ class BarcodePOReceive(BarcodeView):
# Now, look just for "supplier-barcode" plugins # Now, look just for "supplier-barcode" plugins
plugins = registry.with_mixin(PluginMixinEnum.SUPPLIER_BARCODE) plugins = registry.with_mixin(PluginMixinEnum.SUPPLIER_BARCODE)
plugin_slug = None
plugin_response = None plugin_response = None
plugin_error = None
no_supplier_plugin_error = []
supplier_purchase_order = None
plugin_supplier = None
supplier_part = None
for current_plugin in plugins: for current_plugin in plugins:
try: try:
# Will either Output Debugresponse if No_Match is True or return the regular response if No_Match is False
result = current_plugin.scan_receive_item( result = current_plugin.scan_receive_item(
barcode, barcode,
request.user, request.user,
@@ -546,12 +559,58 @@ class BarcodePOReceive(BarcodeView):
line_item=line_item, line_item=line_item,
auto_allocate=auto_allocate, auto_allocate=auto_allocate,
) )
except Exception: except Exception:
log_error('BarcodePOReceive.handle_barcode', plugin=current_plugin.slug) log_error('BarcodePOReceive.handle_barcode', plugin=current_plugin.slug)
continue continue
if result is None: no_match = result.get('no_match', True)
continue
# No_Match Determines if it found a exact match for all the required fields from scan_recieve_item
if no_match is True:
supplier_found = False
try:
plugin_slug = current_plugin.slug
supplier_purchase_order = result.get('PO')
plugin_supplier = result.get('supplier')
supplier_part = result.get('supplier_part')
except KeyError as e:
log_error(
f'BarcodePOReceive.handle_barcode debugresponse: KeyError {e}'
)
continue
# Supplier does not have associated Supplier ID
if plugin_supplier is None:
no_supplier_plugin_error.append(plugin_slug)
continue
# No Purchase Order or Supplier Part Found
if supplier_purchase_order is None and supplier_part is None:
continue
# Purchase Order exists and is found but Supplier part does not exist
if supplier_purchase_order != None and supplier_part is None:
# Supplier was Found
supplier_found = True
plugin_error = _('Purchase order Found\rNo supplier Part Match')
# Supplier Part is Found but Purchase Order does not exist
elif supplier_purchase_order is None and supplier_part != None:
# Supplier was Found
supplier_found = True
plugin_error = _('Supplier Part Found\rNo Purchase Order Match')
# Supplier for PO or Supplier part in barcode was found
if supplier_found is True:
# Adds info on for what was found in the barcode
response['supplier_matches'] = {
'purchase_order': supplier_purchase_order,
'no_match': no_match,
'supplier': plugin_supplier,
'supplier_part': supplier_part,
}
if 'error' in result: if 'error' in result:
logger.info( logger.info(
@@ -569,13 +628,20 @@ class BarcodePOReceive(BarcodeView):
response['plugin'] = plugin.name if plugin else None response['plugin'] = plugin.name if plugin else None
if plugin_response: # If there is a plugin response, and there is a match (no_match = false), combine the dictionaries
if plugin_response and plugin_response.get('no_match') is False:
response = {**response, **plugin_response} response = {**response, **plugin_response}
elif no_supplier_plugin_error:
response['no_supplier_plugin_error'] = no_supplier_plugin_error
# A plugin has not been found! # A plugin has not been found!
if plugin is None: if plugin is None:
response['error'] = _('No plugin match for supplier barcode') response['error'] = _('No plugin match for supplier barcode')
# A plugin was found, with a Error
elif plugin_error:
response['error'] = plugin_error
self.log_scan(request, response, 'success' in response) self.log_scan(request, response, 'success' in response)
if 'error' in response: if 'error' in response:
@@ -320,7 +320,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
location=None, location=None,
auto_allocate: bool = True, auto_allocate: bool = True,
**kwargs, **kwargs,
) -> dict | None: ) -> dict:
"""Attempt to receive an item against a PurchaseOrder via barcode scanning. """Attempt to receive an item against a PurchaseOrder via barcode scanning.
Arguments: Arguments:
@@ -344,32 +344,50 @@ class SupplierBarcodeMixin(BarcodeMixin):
# Extract supplier information # Extract supplier information
supplier = supplier or self.get_supplier(cache=True) supplier = supplier or self.get_supplier(cache=True)
if not supplier: """Construct Debug Response
This is returned if a perfect match is not found with the info provided from the barcode
Response Info:
'supplier': get supplier ID
'PO': Represented for "Purchase Order", find PO number to supplier
'supplier_part': find supplier part info to supplier
'no_match': Boolean, did we find a perfect match with info given? False is Yes, True is No
"""
debug_response = {}
if supplier is None:
# No supplier information available # No supplier information available
return None debug_response['supplier'] = None
else:
debug_response['supplier'] = supplier.name
# Extract purchase order information # Extract purchase order information
purchase_order = purchase_order or self.get_purchase_order() purchase_order = purchase_order or self.get_purchase_order()
if not purchase_order or purchase_order.supplier != supplier: if purchase_order is None or purchase_order.supplier != supplier:
# Purchase order does not match supplier # Purchase order does not match supplier
return None debug_response['PO'] = None
else:
debug_response['PO'] = purchase_order.reference
supplier_part = self.get_supplier_part() supplier_part = self.get_supplier_part()
if not supplier_part: if supplier_part is None:
# No supplier part information available # No supplier part information available
return None debug_response['supplier_part'] = None
else:
debug_response['supplier_part'] = str(supplier_part.part)
# Attempt to find matching line item # Attempt to find matching line item
if not line_item: if not line_item and purchase_order != None:
line_items = purchase_order.lines.filter(part=supplier_part) line_items = purchase_order.lines.filter(part=supplier_part)
if line_items.count() == 1: if line_items.count() == 1:
line_item = line_items.first() line_item = line_items.first()
if not line_item: # If Purchase Order or Supplier Part does not exist, throw debug response
# No line item information available if debug_response['PO'] is None or debug_response['supplier_part'] is None:
return None debug_response['no_match'] = True
return debug_response
if line_item.part != supplier_part: if line_item.part != supplier_part:
return {'error': _('Supplier part does not match line item')} return {'error': _('Supplier part does not match line item')}
@@ -406,7 +424,8 @@ class SupplierBarcodeMixin(BarcodeMixin):
'supplier_part': supplier_part.pk, 'supplier_part': supplier_part.pk,
'purchase_order': purchase_order.pk, 'purchase_order': purchase_order.pk,
'location': location.pk if location else None, 'location': location.pk if location else None,
} },
'no_match': False,
} }
if action_required: if action_required:
@@ -428,6 +428,95 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
# Quantity should be pre-filled with the remaining quantity # Quantity should be pre-filled with the remaining quantity
self.assertEqual(5, response.data['lineitem']['quantity']) self.assertEqual(5, response.data['lineitem']['quantity'])
def test_receive_includes_no_match_false(self):
"""Successful receive merges no_match=False from the plugin response."""
self.purchase_order1.place_order()
url = reverse('api-barcode-po-receive')
result = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200)
self.assertIn('success', result.data)
self.assertIn('no_match', result.data)
self.assertFalse(result.data['no_match'])
def test_partial_match_po_found_no_supplier_part(self):
"""Barcode references a known PO but the SKU has no matching supplier part."""
url = reverse('api-barcode-po-receive')
# '1K72991337' matches purchase_order1 via supplier_reference; SKU is unknown
result = self.post(
url, data={'barcode': DIGIKEY_BARCODE_PO_NO_PART}, expected_code=400
)
self.assertIn('error', result.data)
self.assertIn('Purchase order Found', result.data['error'])
self.assertIn('No supplier Part Match', result.data['error'])
self.assertIn('supplier_matches', result.data)
matches = result.data['supplier_matches']
self.assertIsNotNone(matches['purchase_order'])
# DRF's ValidationError converts None leaves to ErrorDetail('None')
self.assertEqual(str(matches['supplier_part']), 'None')
self.assertIsNotNone(matches['supplier'])
self.assertTrue(matches['no_match'])
def test_partial_match_supplier_part_found_no_po(self):
"""Barcode references a known supplier part but the PO reference has no match."""
url = reverse('api-barcode-po-receive')
# 'P296-LM358BIDDFRCT-ND' matches the existing supplier part; PO ref is unknown
result = self.post(
url, data={'barcode': DIGIKEY_BARCODE_PART_NO_PO}, expected_code=400
)
self.assertIn('error', result.data)
self.assertIn('Supplier Part Found', result.data['error'])
self.assertIn('No Purchase Order Match', result.data['error'])
self.assertIn('supplier_matches', result.data)
matches = result.data['supplier_matches']
# DRF's ValidationError converts None leaves to ErrorDetail('None')
self.assertEqual(str(matches['purchase_order']), 'None')
self.assertIsNotNone(matches['supplier_part'])
self.assertIsNotNone(matches['supplier'])
self.assertTrue(matches['no_match'])
def test_no_supplier_plugin_error(self):
"""Plugins that cannot resolve a supplier are listed in no_supplier_plugin_error."""
url = reverse('api-barcode-po-receive')
digikey_plugin = registry.get_plugin('digikeyplugin')
original_supplier_id = digikey_plugin.get_setting('SUPPLIER_ID')
# Use a non-existent PK so get_supplier() returns None
digikey_plugin.set_setting('SUPPLIER_ID', 99999)
if hasattr(digikey_plugin, '_supplier'):
del digikey_plugin._supplier
try:
result = self.post(
url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400
)
self.assertIn('no_supplier_plugin_error', result.data)
self.assertIn('digikeyplugin', result.data['no_supplier_plugin_error'])
finally:
digikey_plugin.set_setting('SUPPLIER_ID', original_supplier_id)
if hasattr(digikey_plugin, '_supplier'):
del digikey_plugin._supplier
def test_no_supplier_matches_when_both_missing(self):
"""When neither PO nor supplier part can be resolved, no supplier_matches is set."""
url = reverse('api-barcode-po-receive')
# Completely unknown barcode — no PO, no part, no supplier
result = self.post(
url, data={'barcode': 'COMPLETELY-UNKNOWN-BARCODE-XYZ'}, expected_code=400
)
self.assertIn('error', result.data)
self.assertNotIn('supplier_matches', result.data)
DIGIKEY_BARCODE = ( DIGIKEY_BARCODE = (
'[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d' '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d'
@@ -476,3 +565,17 @@ TME_QRCODE = (
) )
TME_DATAMATRIX_CODE = 'PWBP-302 1PMPNWBP-302 Q1 K19361337/1' TME_DATAMATRIX_CODE = 'PWBP-302 1PMPNWBP-302 Q1 K19361337/1'
# DigiKey barcode: '1K72991337' matches purchase_order1 via supplier_reference,
# but 'PNONEXISTENT-SKU' has no matching supplier part in the test database.
DIGIKEY_BARCODE_PO_NO_PART = (
'[)>\x1e06\x1dPNONEXISTENT-SKU-XXXX\x1d1PNONEXISTENT-MPN\x1dK\x1d1K72991337\x1d'
'10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK'
)
# DigiKey barcode: 'P296-LM358BIDDFRCT-ND' matches the existing supplier part,
# but '1KBADORDER-XXXXX' matches no purchase order in the test database.
DIGIKEY_BARCODE_PART_NO_PO = (
'[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1KBADORDER-XXXXX\x1d'
'10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK'
)