mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-14 07:21:31 +00:00
Merge branch 'gtin'
This commit is contained in:
commit
7a83581597
71 changed files with 1405 additions and 92 deletions
|
|
@ -59,6 +59,7 @@ class PartMerger implements EntityMergerInterface
|
|||
$this->useOtherValueIfNotEmtpy($target, $other, 'manufacturer_product_number');
|
||||
$this->useOtherValueIfNotEmtpy($target, $other, 'mass');
|
||||
$this->useOtherValueIfNotEmtpy($target, $other, 'ipn');
|
||||
$this->useOtherValueIfNotEmtpy($target, $other, 'gtin');
|
||||
|
||||
//Merge relations to other entities
|
||||
$this->useOtherValueIfNotNull($target, $other, 'manufacturer');
|
||||
|
|
@ -184,4 +185,4 @@ class PartMerger implements EntityMergerInterface
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class PartDetailDTO extends SearchResultDTO
|
|||
?ManufacturingStatus $manufacturing_status = null,
|
||||
?string $provider_url = null,
|
||||
?string $footprint = null,
|
||||
?string $gtin = null,
|
||||
public readonly ?string $notes = null,
|
||||
/** @var FileDTO[]|null */
|
||||
public readonly ?array $datasheets = null,
|
||||
|
|
@ -68,6 +69,7 @@ class PartDetailDTO extends SearchResultDTO
|
|||
manufacturing_status: $manufacturing_status,
|
||||
provider_url: $provider_url,
|
||||
footprint: $footprint,
|
||||
gtin: $gtin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ readonly class PriceDTO
|
|||
public string $price,
|
||||
/** @var string The currency of the used ISO code of this price detail */
|
||||
public ?string $currency_iso_code,
|
||||
/** @var bool If the price includes tax */
|
||||
/** @var bool If the price includes tax
|
||||
* @deprecated Use the prices_include_vat property of the PurchaseInfoDTO instead, as this property is not reliable if there are multiple prices with different values for includes_tax
|
||||
*/
|
||||
public ?bool $includes_tax = true,
|
||||
/** @var float the price related quantity */
|
||||
public ?float $price_related_quantity = 1.0,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ namespace App\Services\InfoProviderSystem\DTOs;
|
|||
*/
|
||||
readonly class PurchaseInfoDTO
|
||||
{
|
||||
/** @var bool|null If the prices contain VAT or not. Null if state is unknown. */
|
||||
public ?bool $prices_include_vat;
|
||||
|
||||
public function __construct(
|
||||
public string $distributor_name,
|
||||
public string $order_number,
|
||||
|
|
@ -36,6 +39,7 @@ readonly class PurchaseInfoDTO
|
|||
public array $prices,
|
||||
/** @var string|null An url to the product page of the vendor */
|
||||
public ?string $product_url = null,
|
||||
?bool $prices_include_vat = null,
|
||||
)
|
||||
{
|
||||
//Ensure that the prices are PriceDTO instances
|
||||
|
|
@ -44,5 +48,17 @@ readonly class PurchaseInfoDTO
|
|||
throw new \InvalidArgumentException('The prices array must only contain PriceDTO instances');
|
||||
}
|
||||
}
|
||||
|
||||
//If no prices_include_vat information is given, try to deduct it from the prices
|
||||
if ($prices_include_vat === null) {
|
||||
$vatValues = array_unique(array_map(fn(PriceDTO $price) => $price->includes_tax, $this->prices));
|
||||
if (count($vatValues) === 1) {
|
||||
$this->prices_include_vat = $vatValues[0]; //Use the value of the prices if they are all the same
|
||||
} else {
|
||||
$this->prices_include_vat = null; //If there are different values for the prices, we cannot determine if the prices include VAT or not
|
||||
}
|
||||
} else {
|
||||
$this->prices_include_vat = $prices_include_vat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ class SearchResultDTO
|
|||
public readonly ?string $provider_url = null,
|
||||
/** @var string|null A footprint representation of the providers page */
|
||||
public readonly ?string $footprint = null,
|
||||
/** @var string|null The GTIN / EAN of the part */
|
||||
public readonly ?string $gtin = null,
|
||||
)
|
||||
{
|
||||
if ($preview_image_url !== null) {
|
||||
|
|
@ -90,6 +92,7 @@ class SearchResultDTO
|
|||
'manufacturing_status' => $this->manufacturing_status?->value,
|
||||
'provider_url' => $this->provider_url,
|
||||
'footprint' => $this->footprint,
|
||||
'gtin' => $this->gtin,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +115,7 @@ class SearchResultDTO
|
|||
manufacturing_status: isset($data['manufacturing_status']) ? ManufacturingStatus::tryFrom($data['manufacturing_status']) : null,
|
||||
provider_url: $data['provider_url'] ?? null,
|
||||
footprint: $data['footprint'] ?? null,
|
||||
gtin: $data['gtin'] ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ final class DTOtoEntityConverter
|
|||
$entity->setPrice($dto->getPriceAsBigDecimal());
|
||||
$entity->setPriceRelatedQuantity($dto->price_related_quantity);
|
||||
|
||||
//Currency TODO
|
||||
if ($dto->currency_iso_code !== null) {
|
||||
$entity->setCurrency($this->getCurrency($dto->currency_iso_code));
|
||||
} else {
|
||||
|
|
@ -117,6 +116,8 @@ final class DTOtoEntityConverter
|
|||
$entity->addPricedetail($this->convertPrice($price));
|
||||
}
|
||||
|
||||
$entity->setPricesIncludesVAT($dto->prices_include_vat);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +176,8 @@ final class DTOtoEntityConverter
|
|||
$entity->setManufacturingStatus($dto->manufacturing_status ?? ManufacturingStatus::NOT_SET);
|
||||
$entity->setManufacturerProductURL($dto->manufacturer_product_url ?? '');
|
||||
|
||||
$entity->setGtin($dto->gtin);
|
||||
|
||||
//Set the provider reference on the part
|
||||
$entity->setProviderReference(InfoProviderReference::fromPartDTO($dto));
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
|
|||
preview_image_url: $result['image'] ?? null,
|
||||
provider_url: $this->getProductUrl($result['productId']),
|
||||
footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []),
|
||||
gtin: $result['ean'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +303,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
|
|||
preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null,
|
||||
provider_url: $this->getProductUrl($data['shortProductNumber']),
|
||||
footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []),
|
||||
gtin: $data['productFullInformation']['eanCode'] ?? null,
|
||||
notes: $data['productFullInformation']['description'] ?? null,
|
||||
datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []),
|
||||
parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []),
|
||||
|
|
@ -316,6 +318,8 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr
|
|||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::DATASHEET,
|
||||
ProviderCapabilities::PRICE,
|
||||
ProviderCapabilities::FOOTPRINT,
|
||||
ProviderCapabilities::GTIN,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,10 +227,11 @@ class GenericWebProvider implements InfoProviderInterface
|
|||
mpn: $product->mpn?->toString(),
|
||||
preview_image_url: $image,
|
||||
provider_url: $url,
|
||||
gtin: $product->gtin14?->toString() ?? $product->gtin13?->toString() ?? $product->gtin12?->toString() ?? $product->gtin8?->toString(),
|
||||
notes: $notes,
|
||||
parameters: $parameters,
|
||||
vendor_infos: $vendor_infos,
|
||||
mass: $mass
|
||||
mass: $mass,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -429,7 +430,8 @@ class GenericWebProvider implements InfoProviderInterface
|
|||
return [
|
||||
ProviderCapabilities::BASIC,
|
||||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::PRICE
|
||||
ProviderCapabilities::PRICE,
|
||||
ProviderCapabilities::GTIN,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ enum ProviderCapabilities
|
|||
/** Information about the footprint of a part */
|
||||
case FOOTPRINT;
|
||||
|
||||
/** Provider can provide GTIN for a part */
|
||||
case GTIN;
|
||||
|
||||
/**
|
||||
* Get the order index for displaying capabilities in a stable order.
|
||||
* @return int
|
||||
|
|
@ -55,6 +58,7 @@ enum ProviderCapabilities
|
|||
self::DATASHEET => 3,
|
||||
self::PRICE => 4,
|
||||
self::FOOTPRINT => 5,
|
||||
self::GTIN => 6,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +70,7 @@ enum ProviderCapabilities
|
|||
self::PICTURE => 'picture',
|
||||
self::DATASHEET => 'datasheet',
|
||||
self::PRICE => 'price',
|
||||
self::GTIN => 'gtin',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +82,7 @@ enum ProviderCapabilities
|
|||
self::PICTURE => 'fa-image',
|
||||
self::DATASHEET => 'fa-file-alt',
|
||||
self::PRICE => 'fa-money-bill-wave',
|
||||
self::GTIN => 'fa-barcode',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
$name = $element->filter('meta[itemprop="name"]')->attr('content');
|
||||
$sku = $element->filter('meta[itemprop="sku"]')->attr('content');
|
||||
|
||||
|
||||
|
||||
//Try to extract a picture URL:
|
||||
$pictureURL = $element->filter("div.al_artlogo img")->attr('src');
|
||||
|
||||
|
|
@ -95,7 +97,8 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
category: null,
|
||||
manufacturer: $sku,
|
||||
preview_image_url: $pictureURL,
|
||||
provider_url: $element->filter('a.al_artinfo_link')->attr('href')
|
||||
provider_url: $element->filter('a.al_artinfo_link')->attr('href'),
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -146,6 +149,15 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
$priceString = $dom->filter('meta[itemprop="price"]')->attr('content');
|
||||
$currency = $dom->filter('meta[itemprop="priceCurrency"]')->attr('content', 'EUR');
|
||||
|
||||
$gtin = null;
|
||||
foreach (['gtin13', 'gtin14', 'gtin12', 'gtin8'] as $gtinType) {
|
||||
if ($dom->filter("[itemprop=\"$gtinType\"]")->count() > 0) {
|
||||
$gtin = $dom->filter("[itemprop=\"$gtinType\"]")->innerText();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Create purchase info
|
||||
$purchaseInfo = new PurchaseInfoDTO(
|
||||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
|
|
@ -167,10 +179,11 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
mpn: $this->parseMPN($dom),
|
||||
preview_image_url: $json[0]['article_picture'],
|
||||
provider_url: $productPage,
|
||||
gtin: $gtin,
|
||||
notes: $notes,
|
||||
datasheets: $datasheets,
|
||||
parameters: $this->parseParameters($dom),
|
||||
vendor_infos: [$purchaseInfo]
|
||||
vendor_infos: [$purchaseInfo],
|
||||
);
|
||||
|
||||
}
|
||||
|
|
@ -273,6 +286,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
ProviderCapabilities::PICTURE,
|
||||
ProviderCapabilities::DATASHEET,
|
||||
ProviderCapabilities::PRICE,
|
||||
ProviderCapabilities::GTIN,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ final class BarcodeRedirector
|
|||
return $this->getURLVendorBarcode($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof GTINBarcodeScanResult) {
|
||||
return $this->getURLGTINBarcode($barcodeScan);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan));
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +115,16 @@ final class BarcodeRedirector
|
|||
return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]);
|
||||
}
|
||||
|
||||
private function getURLGTINBarcode(GTINBarcodeScanResult $barcodeScan): string
|
||||
{
|
||||
$part = $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]);
|
||||
if (!$part instanceof Part) {
|
||||
throw new EntityNotFoundException();
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a part from a scan of a Vendor Barcode by filtering for parts
|
||||
* with the same Info Provider Id or, if that fails, by looking for parts with a
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ final class BarcodeScanHelper
|
|||
if ($type === BarcodeSourceType::EIGP114) {
|
||||
return $this->parseEIGP114Barcode($input);
|
||||
}
|
||||
if ($type === BarcodeSourceType::GTIN) {
|
||||
return $this->parseGTINBarcode($input);
|
||||
}
|
||||
|
||||
//Null means auto and we try the different formats
|
||||
$result = $this->parseInternalBarcode($input);
|
||||
|
|
@ -117,9 +120,19 @@ final class BarcodeScanHelper
|
|||
return $result;
|
||||
}
|
||||
|
||||
//If the result is a valid GTIN barcode, we can parse it directly
|
||||
if (GTINBarcodeScanResult::isValidGTIN($input)) {
|
||||
return $this->parseGTINBarcode($input);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown barcode');
|
||||
}
|
||||
|
||||
private function parseGTINBarcode(string $input): GTINBarcodeScanResult
|
||||
{
|
||||
return new GTINBarcodeScanResult($input);
|
||||
}
|
||||
|
||||
private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult
|
||||
{
|
||||
return EIGP114BarcodeScanResult::parseFormat06Code($input);
|
||||
|
|
|
|||
|
|
@ -42,4 +42,9 @@ enum BarcodeSourceType
|
|||
* EIGP114 formatted barcodes like used by digikey, mouser, etc.
|
||||
*/
|
||||
case EIGP114;
|
||||
}
|
||||
|
||||
/**
|
||||
* GTIN /EAN barcodes, which are used on most products in the world. These are checked with the GTIN field of a part.
|
||||
*/
|
||||
case GTIN;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* 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\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use GtinValidation\GtinValidator;
|
||||
|
||||
readonly class GTINBarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
|
||||
private GtinValidator $validator;
|
||||
|
||||
public function __construct(
|
||||
public string $gtin,
|
||||
) {
|
||||
$this->validator = new GtinValidator($this->gtin);
|
||||
}
|
||||
|
||||
public function getDecodedForInfoMode(): array
|
||||
{
|
||||
$obj = $this->validator->getGtinObject();
|
||||
return [
|
||||
'GTIN' => $this->gtin,
|
||||
'GTIN type' => $obj->getType(),
|
||||
'Valid' => $this->validator->isValid() ? 'Yes' : 'No',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given input is a valid GTIN. This is used to determine whether a scanned barcode should be interpreted as a GTIN or not.
|
||||
* @param string $input
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidGTIN(string $input): bool
|
||||
{
|
||||
try {
|
||||
return (new GtinValidator($input))->isValid();
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,4 +197,45 @@ final class PartLotWithdrawAddHelper
|
|||
$this->entityManager->remove($origin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a stocktake for the given part lot, setting the amount to the given actual amount.
|
||||
* Please note that the changes are not flushed to DB yet, you have to do this yourself
|
||||
* @param PartLot $lot
|
||||
* @param float $actualAmount
|
||||
* @param string|null $comment
|
||||
* @param \DateTimeInterface|null $action_timestamp
|
||||
* @return void
|
||||
*/
|
||||
public function stocktake(PartLot $lot, float $actualAmount, ?string $comment = null, ?\DateTimeInterface $action_timestamp = null): void
|
||||
{
|
||||
if ($actualAmount < 0) {
|
||||
throw new \InvalidArgumentException('Actual amount must be non-negative');
|
||||
}
|
||||
|
||||
$part = $lot->getPart();
|
||||
|
||||
//Check whether we have to round the amount
|
||||
if (!$part->useFloatAmount()) {
|
||||
$actualAmount = round($actualAmount);
|
||||
}
|
||||
|
||||
$oldAmount = $lot->getAmount();
|
||||
//Clear any unknown status when doing a stocktake, as we now have a known amount
|
||||
$lot->setInstockUnknown(false);
|
||||
$lot->setAmount($actualAmount);
|
||||
if ($action_timestamp) {
|
||||
$lot->setLastStocktakeAt(\DateTimeImmutable::createFromInterface($action_timestamp));
|
||||
} else {
|
||||
$lot->setLastStocktakeAt(new \DateTimeImmutable()); //Use now if no timestamp is given
|
||||
}
|
||||
|
||||
$event = PartStockChangedLogEntry::stocktake($lot, $oldAmount, $lot->getAmount(), $part->getAmountSum() , $comment, $action_timestamp);
|
||||
$this->eventLogger->log($event);
|
||||
|
||||
//Apply the comment also to global events, so it gets associated with the elementChanged log entry
|
||||
if (!$this->eventCommentHelper->isMessageSet() && ($comment !== null && $comment !== '')) {
|
||||
$this->eventCommentHelper->setMessage($comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,4 +157,20 @@ class PermissionSchemaUpdater
|
|||
$permissions->setPermissionValue('system', 'show_updates', $new_value);
|
||||
}
|
||||
}
|
||||
|
||||
private function upgradeSchemaToVersion4(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection
|
||||
{
|
||||
$permissions = $holder->getPermissions();
|
||||
|
||||
//If the reports.generate permission is not defined yet, set it to the value of reports.read
|
||||
if (!$permissions->isPermissionSet('parts_stock', 'stocktake')) {
|
||||
//Set the new permission to true only if both add and withdraw are allowed
|
||||
$new_value = TrinaryLogicHelper::and(
|
||||
$permissions->getPermissionValue('parts_stock', 'withdraw'),
|
||||
$permissions->getPermissionValue('parts_stock', 'add')
|
||||
);
|
||||
|
||||
$permissions->setPermissionValue('parts_stock', 'stocktake', $new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ class UserAvatarHelper
|
|||
$attachment_type = new AttachmentType();
|
||||
$attachment_type->setName('Avatars');
|
||||
$attachment_type->setFiletypeFilter('image/*');
|
||||
$attachment_type->setAllowedTargets([UserAttachment::class]);
|
||||
$this->entityManager->persist($attachment_type);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue