mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-06 00:44:25 +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:
@@ -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.
|
||||
|
||||
*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
|
||||
|
||||
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.
|
||||
|
||||
*Note: supplier SKU's are required to be found on the barcode for this function to find the associated line item.*
|
||||
|
||||
### Sales Order Actions
|
||||
|
||||
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
|
||||
plugins = registry.with_mixin(PluginMixinEnum.SUPPLIER_BARCODE)
|
||||
|
||||
plugin_slug = 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:
|
||||
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(
|
||||
barcode,
|
||||
request.user,
|
||||
@@ -546,12 +559,58 @@ class BarcodePOReceive(BarcodeView):
|
||||
line_item=line_item,
|
||||
auto_allocate=auto_allocate,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
log_error('BarcodePOReceive.handle_barcode', plugin=current_plugin.slug)
|
||||
continue
|
||||
|
||||
if result is None:
|
||||
continue
|
||||
no_match = result.get('no_match', True)
|
||||
|
||||
# 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:
|
||||
logger.info(
|
||||
@@ -569,13 +628,20 @@ class BarcodePOReceive(BarcodeView):
|
||||
|
||||
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}
|
||||
elif no_supplier_plugin_error:
|
||||
response['no_supplier_plugin_error'] = no_supplier_plugin_error
|
||||
|
||||
# A plugin has not been found!
|
||||
if plugin is None:
|
||||
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)
|
||||
|
||||
if 'error' in response:
|
||||
|
||||
@@ -320,7 +320,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
||||
location=None,
|
||||
auto_allocate: bool = True,
|
||||
**kwargs,
|
||||
) -> dict | None:
|
||||
) -> dict:
|
||||
"""Attempt to receive an item against a PurchaseOrder via barcode scanning.
|
||||
|
||||
Arguments:
|
||||
@@ -344,32 +344,50 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
||||
# Extract supplier information
|
||||
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
|
||||
return None
|
||||
debug_response['supplier'] = None
|
||||
else:
|
||||
debug_response['supplier'] = supplier.name
|
||||
|
||||
# Extract purchase order information
|
||||
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
|
||||
return None
|
||||
debug_response['PO'] = None
|
||||
else:
|
||||
debug_response['PO'] = purchase_order.reference
|
||||
|
||||
supplier_part = self.get_supplier_part()
|
||||
|
||||
if not supplier_part:
|
||||
if supplier_part is None:
|
||||
# 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
|
||||
if not line_item:
|
||||
if not line_item and purchase_order != None:
|
||||
line_items = purchase_order.lines.filter(part=supplier_part)
|
||||
if line_items.count() == 1:
|
||||
line_item = line_items.first()
|
||||
|
||||
if not line_item:
|
||||
# No line item information available
|
||||
return None
|
||||
# If Purchase Order or Supplier Part does not exist, throw debug response
|
||||
if debug_response['PO'] is None or debug_response['supplier_part'] is None:
|
||||
debug_response['no_match'] = True
|
||||
return debug_response
|
||||
|
||||
if line_item.part != supplier_part:
|
||||
return {'error': _('Supplier part does not match line item')}
|
||||
@@ -406,7 +424,8 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
||||
'supplier_part': supplier_part.pk,
|
||||
'purchase_order': purchase_order.pk,
|
||||
'location': location.pk if location else None,
|
||||
}
|
||||
},
|
||||
'no_match': False,
|
||||
}
|
||||
|
||||
if action_required:
|
||||
|
||||
@@ -428,6 +428,95 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
# Quantity should be pre-filled with the remaining 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 = (
|
||||
'[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d'
|
||||
@@ -476,3 +565,17 @@ TME_QRCODE = (
|
||||
)
|
||||
|
||||
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'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user