mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 02:59:29 +00:00
Merge d4b9ca3bc1 into 023d38d170
This commit is contained in:
commit
30c439cc17
7 changed files with 861 additions and 0 deletions
135
assets/controllers/pages/handheld_scan_controller.js
Normal file
135
assets/controllers/pages/handheld_scan_controller.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2024 Alex Barclay (https://github.com/barclaac)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
||||
_scanner = null;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
_scanner = new ScannerSerial()
|
||||
}
|
||||
|
||||
connect(event) {
|
||||
console.log('Controller connected')
|
||||
|
||||
if (_scanner.isConnected()) {
|
||||
console.log('already connected')
|
||||
document.getElementById('handheld_scanner_dialog_connect').style.display='none'
|
||||
document.getElementById('handheld_scanner_dialog_disconnect').style.display=''
|
||||
}
|
||||
}
|
||||
|
||||
async onConnectScanner(event) {
|
||||
console.log('Connect to barcode reader')
|
||||
|
||||
if (!_scanner.isConnected()) {
|
||||
await _scanner.connect(async (reader) => {
|
||||
decoder = new TextDecoder();
|
||||
var barcodeBuffer = '';
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
console.log('releasing reader')
|
||||
reader.releaseLock();
|
||||
reader = null;
|
||||
break;
|
||||
}
|
||||
console.log(value);
|
||||
console.log(decoder.decode(value));
|
||||
partial = decoder.decode(value);
|
||||
barcodeBuffer += partial
|
||||
end = false
|
||||
endidx = partial.indexOf('\x1e\x04');
|
||||
if (endidx != -1) {
|
||||
end = true;
|
||||
} else {
|
||||
endidx = partial.indexOf('\r');
|
||||
if (endidx != -1) {
|
||||
end = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (end) {
|
||||
// Decode the barcode
|
||||
console.log(barcodeBuffer)
|
||||
start = barcodeBuffer.indexOf('[)>')
|
||||
if (start == -1) {
|
||||
console.log('badly formed barcode')
|
||||
} else {
|
||||
// Post this back to the server
|
||||
document.getElementById('handheld_scanner_dialog_barcode').value = barcodeBuffer;
|
||||
form = document.getElementById('handheld_dialog_form');
|
||||
form.requestSubmit();
|
||||
}
|
||||
barcodeBuffer = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
document.getElementById('handheld_scanner_dialog_connect').style.display='none'
|
||||
document.getElementById('handheld_scanner_dialog_disconnect').style.display=''
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async onDisconnectScanner(event) {
|
||||
console.log('Disconnect called')
|
||||
if (_scanner.isConnected) {
|
||||
_scanner.disconnect()
|
||||
document.getElementById('handheld_scanner_dialog_connect').style.display=''
|
||||
document.getElementById('handheld_scanner_dialog_disconnect').style.display='none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScannerSerial {
|
||||
device = null
|
||||
reader = null
|
||||
|
||||
constructor() {
|
||||
if (ScannerSerial.instance) {
|
||||
return ScannerSerial.instance;
|
||||
}
|
||||
ScannerSerial.instance = this;
|
||||
}
|
||||
|
||||
async connect(readHandler) {
|
||||
this.device = await navigator.serial.requestPort({ filters: [{ usbVendorId: 0x03f0, usbDeviceId: 0x0339 }] })
|
||||
await this.device.open({baudRate: 9600})
|
||||
|
||||
console.log(this.device)
|
||||
this.reader = this.device.readable.getReader()
|
||||
readHandler(this.reader)
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.reader.cancel()
|
||||
this.reader = null
|
||||
this.device.close()
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
if (this.reader == null) {
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
28
docs/usage/eigp114_barcode_operations.md
Normal file
28
docs/usage/eigp114_barcode_operations.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# EIGP 114 Barcode Operations #
|
||||
|
||||
## Concept of Operation ##
|
||||
Module is intended to be as quick and easy as possible once you receive your bag of goodies from
|
||||
Digikey or Mouser. Steps would be:
|
||||
* Grab a bag from the shipment
|
||||
* Identify the component type and scan your intended storage box
|
||||
* Scan the storage container
|
||||
* Scan the bag and put in container
|
||||
* Grab the next bag (same component type)
|
||||
* Scan the bag and put in container
|
||||
* Grab the next bag (different component type)
|
||||
* Scan new container
|
||||
* Scan the bag and put in container
|
||||
|
||||
### Alternatives ###
|
||||
|
||||
* Grab a bag from the shipment
|
||||
* Scan the bag
|
||||
* Put the bag in the single storage location indicated
|
||||
* Scan next bag from shipment...
|
||||
|
||||
## Scanner Types ##
|
||||
|
||||
Currently only supports an HP 4430 scanner. Code uses WebSerial to communicate with the scanner. For
|
||||
this to me more widespread would need to add a database of scanner USB device IDs and some data
|
||||
indicating how each is different
|
||||
|
||||
437
src/Controller/HandheldScannerController.php
Normal file
437
src/Controller/HandheldScannerController.php
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2024 Alex Barclay (https://github.com/barclaac)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\LabelSystem\LabelOptions;
|
||||
use App\Entity\LabelSystem\LabelProcessMode;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\LabelSystem\LabelSupportedElement;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Exceptions\TwigModeException;
|
||||
use App\Form\LabelSystem\HandheldScannerDialogType;
|
||||
use App\Helpers\EIGP114;
|
||||
use App\Repository\DBElementRepository;
|
||||
use App\Services\ElementTypeNameGenerator;
|
||||
use App\Services\LabelSystem\LabelGenerator;
|
||||
use App\Services\Misc\RangeParser;
|
||||
use App\Services\Parts\PartLotWithdrawAddHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class BarcodeScanType
|
||||
{
|
||||
protected ?string $barcode;
|
||||
protected ?string $manufacturerPN;
|
||||
protected ?int $quantity;
|
||||
protected ?string $location;
|
||||
|
||||
// Show last item and quantity added to help check if scanning is working well
|
||||
protected ?string $lastManufacturerPN;
|
||||
protected ?int $lastQuantity;
|
||||
|
||||
public function __construct() {
|
||||
$this->barcode = "";
|
||||
$this->manufacturerPN = "";
|
||||
$this->quantity = 0;
|
||||
$this->location = "";
|
||||
|
||||
$this->lastManufacturerPN = "";
|
||||
$this->lastQuantity = 0;
|
||||
}
|
||||
|
||||
public function getBarcode(): ?string {
|
||||
return $this->barcode;
|
||||
}
|
||||
|
||||
public function setBarcode(?string $barcode): self {
|
||||
$this->barcode = $barcode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getManufacturerPN(): ?string {
|
||||
return $this->manufacturerPN;
|
||||
}
|
||||
|
||||
public function setManufacturerPN(?string $manufacturerPN): self {
|
||||
$this->manufacturerPN = $manufacturerPN;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastManufacturerPN(): ?string {
|
||||
return $this->lastManufacturerPN;
|
||||
}
|
||||
|
||||
public function setLastManufacturerPN(?string $lastManufacturerPN): self {
|
||||
$this->lastManufacturerPN = $lastManufacturerPN;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuantity(): ?int {
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function setQuantity(?int $quantity): self {
|
||||
$this->quantity = $quantity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastQuantity(): ?int {
|
||||
return $this->lastQuantity;
|
||||
}
|
||||
|
||||
public function setLastQuantity(?int $lastQuantity): self {
|
||||
$this->lastQuantity = $lastQuantity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLocation(): ?string {
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(?string $location): self {
|
||||
$this->location = $location;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function cycleLastAdded() : void {
|
||||
$this->lastManufacturerPN = $this->manufacturerPN;
|
||||
$this->manufacturerPN = null;
|
||||
$this->lastQuantity = $this->quantity;
|
||||
$this->quantity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class HandheldScannerController extends AbstractController
|
||||
{
|
||||
public function __construct(protected EntityManagerInterface $em, protected LoggerInterface $logger, protected TranslatorInterface $translator)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/handheldscanner',name: 'handheld_scanner_dialog')]
|
||||
public function generator(Request $request,
|
||||
PartLotWithdrawAddHelper $withdrawAddHelper): Response
|
||||
{
|
||||
$this->logger->info('*** rendering form ***');
|
||||
$this->logger->info(var_export($request->getPayload()->all(), true));
|
||||
|
||||
$barcode = new BarcodeScanType();
|
||||
$form = $this->buildForm($barcode);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->get('autocommit')->getData() == true || ($form->isSubmitted() && $form->isValid())) {
|
||||
if ($this->processSubmit($form, $withdrawAddHelper, $barcode)) {
|
||||
// Need a new form to render because we can't change submitted form
|
||||
$this->logger->info('replacing form with fresh');
|
||||
$barcode->cycleLastAdded(); // Shuffle into last added slot
|
||||
$newForm = $this->buildForm($barcode);
|
||||
$newForm->get('missingloc')->setData($form->get('missingloc')->getData());
|
||||
$newForm->get('locfrompart')->setData($form->get('locfrompart')->getData());
|
||||
$newForm->get('foundloc')->setData($form->get('foundloc')->getData());
|
||||
$newForm->get('missingpart')->setData($form->get('missingpart')->getData());
|
||||
$newForm->get('manufacturer_pn')->setData('');
|
||||
$newForm->get('quantity')->setData(0);
|
||||
$form = $newForm;
|
||||
}
|
||||
}
|
||||
return $this->render('label_system/handheld_scanner/handheld_scanner.html.twig', [
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function buildForm(BarcodeScanType $barcode) : FormInterface
|
||||
{
|
||||
$builder = $this->container->get('form.factory')
|
||||
->createBuilder(HandheldScannerDialogType::class, $barcode);
|
||||
$this->addPreSubmitEventHandler($builder);
|
||||
|
||||
$form = $builder->getForm();
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
protected function addPreSubmitEventHandler(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
|
||||
$form = $event->getForm();
|
||||
$data = $event->getData();
|
||||
if (!isset($data['barcode'])) {
|
||||
return;
|
||||
}
|
||||
$r = EIGP114::decode($data['barcode']);
|
||||
|
||||
// Remove barcode so that if user has edited fields that the barcode won't
|
||||
// override any of the user's values
|
||||
unset($data['barcode']);
|
||||
|
||||
if (array_key_exists('location', $r)) {
|
||||
$data['location'] = $r['location'];
|
||||
$data['last_scan'] = 'location';
|
||||
$data['manufacturer_pn'] = '';
|
||||
$data['locfrompart'] = false;
|
||||
$data['foundpart'] = false;
|
||||
$data['quantity'] = '';
|
||||
} else if (array_key_exists('supplier_pn', $r)) {
|
||||
$data['manufacturer_pn'] = $r['supplier_pn'];
|
||||
$data['last_scan'] = 'part';
|
||||
if (array_key_exists('quantity', $r)) {
|
||||
$data['quantity'] = $r['quantity'];
|
||||
}
|
||||
}
|
||||
|
||||
// Look up the location in the database to see if one needs to be created
|
||||
if ($data['location'] != "") {
|
||||
$storageRepository = $this->em->getRepository(StorageLocation::class);
|
||||
$storage = $this->getStorageLocation($data['location']);
|
||||
$data['foundloc'] = ($storage != null);
|
||||
}
|
||||
|
||||
// Look up the part in the database to see if one needs to be created
|
||||
$part = null;
|
||||
if ($data['manufacturer_pn'] != "") {
|
||||
$partRepository = $this->em->getRepository(Part::class);
|
||||
$part = $partRepository->findOneBy(['manufacturer_product_number' => $data['manufacturer_pn']]);
|
||||
$data['foundpart'] = ($part != null);
|
||||
if ($data['foundpart'] == false &&
|
||||
(array_key_exists('missingpart', $data) && $data['missingpart'] == false) &&
|
||||
$data['autocommit'] == true) {
|
||||
$this->addFlash('error', 'Cannot autocommit part - part not in database');
|
||||
}
|
||||
}
|
||||
|
||||
// Did we want to use the storage location for this part
|
||||
// Will require all part-lots to be in the same location
|
||||
if (array_key_exists('locfrompart', $data) && $part) {
|
||||
$this->logger->info('take loc from part');
|
||||
$locs=[];
|
||||
foreach ($part->getPartLots() as &$pl) {
|
||||
$locs[$pl->getStorageLocation()->getId()] = $pl->getStorageLocation();
|
||||
}
|
||||
if (count($locs) == 1) {
|
||||
// Got exactly 1 location - can set this as the default
|
||||
$storageLoc = array_pop($locs);
|
||||
$data['location'] = $storageLoc->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$event->setData($data);
|
||||
});
|
||||
}
|
||||
|
||||
protected function processSubmit(FormInterface $form, PartLotWithdrawAddHelper $withdrawAddHelper,
|
||||
BarcodeScanType $barcode) : bool {
|
||||
$this->logger->info("form submitted");
|
||||
// We could be here through an autosubmit or because the actual button was pressed
|
||||
// To proceed we need a storage location, manufacturer part number and a quantity
|
||||
$this->logger->info('processSubmit');
|
||||
if ($form instanceof Form && $form->getClickedButton() != null || $form->get('autocommit')->getData() == true) {
|
||||
$fail = false;
|
||||
if ($barcode->getLocation() != '' && $barcode->getManufacturerPN() != '' &&
|
||||
$barcode->getQuantity() != 0) {
|
||||
// Got all the data that we need - now work through the items to see if we
|
||||
// can submit the data
|
||||
$storageLocation = $this->getStorageLocation($barcode->getLocation());
|
||||
if (!$storageLocation) {
|
||||
if ($form->get('missingloc')->getData() == true) {
|
||||
$storageLocation = $this->createStorageLocation($barcode->getLocation());
|
||||
} else {
|
||||
$fail = true;
|
||||
$this->addFlash('error', 'storage doesn\'t exist');
|
||||
}
|
||||
}
|
||||
|
||||
$part = $this->getPart($barcode->getManufacturerPN());
|
||||
if (!$part) {
|
||||
$this->logger->debug('part not found');
|
||||
if ($form->get('missingpart')->getData() == true) {
|
||||
$this->logger->debug('create part');
|
||||
$part = $this->createMissingPart($barcode->getManufacturerPN());
|
||||
$this->logger->debug('part created');
|
||||
|
||||
$this->em->flush();
|
||||
} else {
|
||||
$fail = true;
|
||||
$this->addFlash('error', 'part doesn\'t exist');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$fail) {
|
||||
// Have a part and storage location so attempt to add stock for this combination
|
||||
$found=false;
|
||||
$partLots = $part->getPartLots();
|
||||
if ($partLots != null) {
|
||||
foreach ($part->getPartLots() as &$pl) {
|
||||
if ($pl->getStorageLocation()->getId() == $storageLocation->getId()) {
|
||||
$this->logger->info('Found existing storage location, adding stock');
|
||||
if ($withdrawAddHelper->canAdd($pl)) {
|
||||
$withdrawAddHelper->add($pl, $barcode->getQuantity(), "Barcode scan add");
|
||||
$found = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
$this->logger->info('Part lot {fullPath}', ['fullPath' => $pl->getStorageLocation()->getFullPath()]);
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
// No part lot for this storage location - add one
|
||||
$partLot = new PartLot();
|
||||
$partLot->setStorageLocation($storageLocation);
|
||||
$partLot->setInstockUnknown(false);
|
||||
$partLot->setAmount(0.0);
|
||||
$part->addPartLot($partLot);
|
||||
$this->em->flush(); // Must have an ID for target
|
||||
if ($withdrawAddHelper->canAdd($partLot)) {
|
||||
$withdrawAddHelper->add($partLot, $barcode->getQuantity(), "Barcode scan add");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!$fail) {
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getStorageLocation(string $name) : ?StorageLocation
|
||||
{
|
||||
$repository = $this->em->getRepository(StorageLocation::class);
|
||||
$storage = $repository->findOneBy(['name' => $name]);
|
||||
if ($storage) {
|
||||
$this->logger->info($storage->getFullPath());
|
||||
} else {
|
||||
$this->logger->info('Storage not found in database');
|
||||
}
|
||||
if ($storage instanceof StorageLocation) {
|
||||
return $storage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function createStorageLocation(string $location): StorageLocation
|
||||
{
|
||||
$repository = $this->em->getRepository(StorageLocation::class);
|
||||
$storage = new StorageLocation();
|
||||
$storage->setName($location);
|
||||
$this->em->persist($storage);
|
||||
return $storage;
|
||||
}
|
||||
|
||||
protected function getPart(string $partNumber) : ?Part
|
||||
{
|
||||
$repository = $this->em->getRepository(Part::class);
|
||||
$part = $repository->findOneBy(['manufacturer_product_number' => $partNumber]);
|
||||
if ($part) {
|
||||
$this->logger->info($part->getName());
|
||||
}
|
||||
return $part;
|
||||
}
|
||||
|
||||
protected function createMissingPart(string $partNumber) : ?Part
|
||||
{
|
||||
$repository = $this->em->getRepository(Category::class);
|
||||
$category = $repository->findOneBy(['name' => 'Unclassified']);
|
||||
|
||||
$part = new Part();
|
||||
if ($category instanceof Category) {
|
||||
$part->setCategory($category);
|
||||
}
|
||||
$part->setName($partNumber);
|
||||
$part->setManufacturerProductNumber($partNumber);
|
||||
$this->em->persist($part);
|
||||
|
||||
return $part;
|
||||
}
|
||||
|
||||
protected function addStock(Form $form, PartLotWithdrawAddHelper $withdrawAddHelper,
|
||||
BarcodeScanType $barcode)
|
||||
{
|
||||
$storage = null;
|
||||
$part = null;
|
||||
// See if the storage location exists was in barcode
|
||||
if ($barcode->getLocation() != "") {
|
||||
$storage = $this->getStorageLocation($barcode->getLocation());
|
||||
}
|
||||
if ($barcode->getManufacturerPN() != "") {
|
||||
// Got a part instead
|
||||
$repository = $this->em->getRepository(Part::class);
|
||||
$part = $repository->findOneBy(['manufacturer_product_number' => $barcode->getManufacturerPN()]);
|
||||
if ($part) {
|
||||
$this->logger->info($part->getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Does a part lot exist for this combination?
|
||||
if ($storage != null && $part != null) {
|
||||
$found=false;
|
||||
foreach ($part->getPartLots() as &$pl) {
|
||||
if ($pl->getStorageLocation()->getId() == $storage->getId()) {
|
||||
$this->logger->info('Found existing storage location, adding stock');
|
||||
if ($withdrawAddHelper->canAdd($pl)) {
|
||||
$withdrawAddHelper->add($pl, $barcode->getQuantity(), "Test add");
|
||||
$found = true;
|
||||
}
|
||||
$this->em->flush();
|
||||
$this->addFlash('success', 'stock added');
|
||||
break;
|
||||
}
|
||||
$this->logger->info('Part lot {fullPath}', ['fullPath' => $pl->getStorageLocation()->getFullPath()]);
|
||||
}
|
||||
if (!$found) {
|
||||
// No part lot for this storage location - add one
|
||||
$partLot = new PartLot();
|
||||
$partLot->setStorageLocation($storage);
|
||||
$partLot->setInstockUnknown(false);
|
||||
$partLot->setAmount(0.0);
|
||||
$part->addPartLot($partLot);
|
||||
$this->em->flush();
|
||||
if ($withdrawAddHelper->canAdd($partLot)) {
|
||||
$withdrawAddHelper->add($partLot, $barcode->getQuantity(), "Creational add");
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->addFlash('success', 'partlot added');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/Form/LabelSystem/HandheldScannerDialogType.php
Normal file
139
src/Form/LabelSystem/HandheldScannerDialogType.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2024 Alex Barclay (https://github.com/barclaac)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\LabelSystem;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use App\Form\LabelOptionsType;
|
||||
use App\Helpers\EIGP114;
|
||||
use App\Validator\Constraints\Misc\ValidRange;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class HandheldScannerDialogType extends AbstractType
|
||||
{
|
||||
public function __construct(protected Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options = []): void
|
||||
{
|
||||
$builder->add('barcode', HiddenType::Class, [
|
||||
'required' => true,
|
||||
'action' => '',
|
||||
]);
|
||||
$builder->add('location', TextType::Class, [
|
||||
'required' => false,
|
||||
'label' => 'Storage Location',
|
||||
'help' => 'Scan this first, will erase part fields',
|
||||
]);
|
||||
|
||||
$builder->add('manufacturer_pn', TextType::Class, [
|
||||
'required' => false,
|
||||
'label' => 'Manufacturer Part',
|
||||
]);
|
||||
|
||||
$builder->add('last_manufacturer_pn', TextType::Class, [
|
||||
'required' => false,
|
||||
'label' => 'Last Added Manufacturer Part',
|
||||
]);
|
||||
|
||||
$builder->add('quantity', IntegerType::Class, [
|
||||
'required' => false,
|
||||
'label' => 'Quantity',
|
||||
]);
|
||||
|
||||
$builder->add('last_quantity', IntegerType::Class, [
|
||||
'required' => false,
|
||||
'label' => 'Last Added Quantity',
|
||||
]);
|
||||
|
||||
$builder->add('missingloc', CheckboxType::Class, [
|
||||
'label' => 'Create missing Storage Location',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('locfrompart', CheckboxType::Class, [
|
||||
'label' => 'Take storage location from part',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('foundloc', CheckboxType::Class, [
|
||||
'label' => 'Storage Location in database',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('missingpart', CheckboxType::Class, [
|
||||
'label' => 'Create missing Part',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('foundpart', CheckboxType::Class, [
|
||||
'label' => 'Part in database',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('autocommit', CheckboxType::Class, [
|
||||
'label' => 'Autocommit on Scan',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('connect', ButtonType::Class, [
|
||||
'label' => 'Connect',
|
||||
'attr' => ['data-action' => 'pages--handheld-scan#onConnectScanner',
|
||||
'class' => 'btn btn-primary' ],
|
||||
]);
|
||||
|
||||
$builder->add('disconnect', ButtonType::Class, [
|
||||
'label' => 'Disconnect',
|
||||
'attr' => ['data-action' => 'pages--handheld-scan#onDisconnectScanner',
|
||||
'class' => 'btn btn-primary',
|
||||
'style' => 'display: none'],
|
||||
]);
|
||||
|
||||
$builder->add('submit', SubmitType::Class, [
|
||||
'label' => 'Submit',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefault('mapped', false);
|
||||
$resolver->setDefault('allow_extra_fields', true);
|
||||
}
|
||||
}
|
||||
85
src/Helpers/EIGP114.php
Normal file
85
src/Helpers/EIGP114.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2024 Alex Barclay (https://github.com/barclaac)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/*
|
||||
EIGP 114
|
||||
ISO/IEC 15418
|
||||
ANS MH10.8.2-2016
|
||||
|
||||
Based on the ISO standard but it seems that both Mouser and Digikey aren't quite
|
||||
following the standard.
|
||||
|
||||
Message is defined as (very loose RE format): ({:RS:} = 0x1e, {:GS:} = 0x1d, {:EOT:} = 0x04)
|
||||
Header: [)>{:RS:}06{:GS:}
|
||||
Field data identifier: [0-9]*[A-Z]
|
||||
Field data: .*
|
||||
Field Separator: {:GS:} - may or may not be present after last field
|
||||
Trailer: {:RS:}{:EOT:}
|
||||
|
||||
Mouser & Digikey differences:
|
||||
Header: [)>06{:GS:} - note the missing {:RS:}
|
||||
Field: last field missing trailer {:RS:}{:EOT:} so use {:CR:} inserted by scanner itself
|
||||
|
||||
*/
|
||||
class EIGP114
|
||||
{
|
||||
public static function decode($barcode): array {
|
||||
$rslt = [];
|
||||
|
||||
// Find the start sequence for a type 6 ISO/IEC 15434 message
|
||||
$hdrPattern = "/(\[\)>\u{001e}*06\u{001d})([[:print:]\u{001d}]*)(\u{001e}\u{0004})*/";
|
||||
$matches = [];
|
||||
$loc = preg_match_all($hdrPattern, $barcode, $matches);
|
||||
$fields = [];
|
||||
if ($loc) {
|
||||
$remain = $matches[2][0];
|
||||
$fields = preg_split("/\u{001d}/", $remain);
|
||||
foreach ($fields as &$v) {
|
||||
EIGP114::processField($v, $rslt);
|
||||
}
|
||||
}
|
||||
return $rslt;
|
||||
}
|
||||
|
||||
private static function processField($field, &$result) : void {
|
||||
$pattern = "/([0-9]{0,2}[A-Z])(.+)/";
|
||||
$matches = [];
|
||||
if (preg_match_all($pattern, $field, $matches)) {
|
||||
switch ($matches[1][0]) {
|
||||
case 'L':
|
||||
$result['location'] = $matches[2][0];
|
||||
break;
|
||||
case '1P':
|
||||
$result['supplier_pn'] = $matches[2][0];
|
||||
break;
|
||||
case 'Q':
|
||||
$result['quantity'] = $matches[2][0];
|
||||
break;
|
||||
default:
|
||||
//array_push($result, $matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,6 +153,13 @@ class ToolsTreeBuilder
|
|||
))->setIcon('fa-treeview fa-fw fa-solid fa-tasks');
|
||||
}
|
||||
|
||||
if ($this->security->isGranted('@info_providers.create_parts')) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
"Handheld Scanner",
|
||||
$this->urlGenerator->generate('handheld_scanner_dialog')
|
||||
))->setIcon('fa-treeview fa-fw fa-solid fa-camera-retro');
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
{% extends 'main_card.html.twig' %}
|
||||
|
||||
{% block card_title %}<i class="fas fa-camera-retro fa-fw"></i>Handheld Scanner Operations{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
{{ form_start(form, {'attr': {'id': 'handheld_dialog_form'}}) }}
|
||||
|
||||
{{ form_row(form.location) }}
|
||||
{{ form_row(form.missingloc) }}
|
||||
{{ form_row(form.locfrompart) }}
|
||||
{{ form_row(form.foundloc) }}
|
||||
{{ form_row(form.manufacturer_pn) }}
|
||||
{{ form_row(form.missingpart) }}
|
||||
{{ form_row(form.foundpart) }}
|
||||
{{ form_row(form.quantity) }}
|
||||
{{ form_row(form.autocommit) }}
|
||||
|
||||
<div>
|
||||
{{ form_row(form.last_manufacturer_pn) }}
|
||||
{{ form_row(form.last_quantity) }}
|
||||
</div>
|
||||
|
||||
<div {{ stimulus_controller('pages/handheld_scan')}}>
|
||||
{{ form_widget(form.connect) }}
|
||||
{{ form_widget(form.disconnect) }}
|
||||
{{ form_widget(form.submit) }}
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue