mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Allow to retrieve price and shopping informations from info providers
This commit is contained in:
parent
c4439cc9db
commit
0cb46039dd
11 changed files with 348 additions and 4 deletions
|
@ -107,7 +107,14 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
if (data.short) {
|
||||
return '<div><b>' + escape(data.short) + '</b></div>';
|
||||
let short = escape(data.short)
|
||||
|
||||
//Make text italic, if the item is not yet in the DB
|
||||
if (data.not_in_db_yet) {
|
||||
short = '<i>' + short + '</i>';
|
||||
}
|
||||
|
||||
return '<div><b>' + short + '</b></div>';
|
||||
}
|
||||
|
||||
let name = "";
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\PriceInformations;
|
||||
|
||||
use App\Repository\CurrencyRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use App\Entity\Attachments\CurrencyAttachment;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
|
@ -42,7 +43,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* @extends AbstractStructuralDBElement<CurrencyAttachment, CurrencyParameter>
|
||||
*/
|
||||
#[UniqueEntity('iso_code')]
|
||||
#[ORM\Entity]
|
||||
#[ORM\Entity(repositoryClass: CurrencyRepository::class)]
|
||||
#[ORM\Table(name: 'currencies')]
|
||||
#[ORM\Index(name: 'currency_idx_name', columns: ['name'])]
|
||||
#[ORM\Index(name: 'currency_idx_parent_name', columns: ['parent_id', 'name'])]
|
||||
|
|
|
@ -102,6 +102,9 @@ class StructuralEntityChoiceHelper
|
|||
$symbol = empty($choice->getIsoCode()) ? null : Currencies::getSymbol($choice->getIsoCode());
|
||||
$tmp['data-short'] = $options['short'] ? $symbol : $choice->getName();
|
||||
|
||||
//Show entities that are not added to DB yet separately from other entities
|
||||
$tmp['data-not_in_db_yet'] = $choice->getID() === null;
|
||||
|
||||
return $tmp + [
|
||||
'data-symbol' => $symbol,
|
||||
];
|
||||
|
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
namespace App\Form\Type\Helper;
|
||||
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Repository\StructuralDBElementRepository;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
@ -59,6 +60,12 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
|
|||
public function createNewEntitiesFromValue(string $value): array
|
||||
{
|
||||
if (!$this->options['allow_add']) {
|
||||
//Always allow the starting element to be added
|
||||
if ($this->starting_element !== null && $this->starting_element->getID() === null) {
|
||||
$this->entityManager->persist($this->starting_element);
|
||||
return [$this->starting_element];
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!');
|
||||
}
|
||||
|
||||
|
|
59
src/Repository/CurrencyRepository.php
Normal file
59
src/Repository/CurrencyRepository.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\Repository;
|
||||
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use Symfony\Component\Intl\Currencies;
|
||||
|
||||
/**
|
||||
* @extends StructuralDBElementRepository<Currency>
|
||||
*/
|
||||
class CurrencyRepository extends StructuralDBElementRepository
|
||||
{
|
||||
/**
|
||||
* Finds or create a currency with the given ISO code.
|
||||
* @param string $iso_code
|
||||
* @return Currency
|
||||
*/
|
||||
public function findOrCreateByISOCode(string $iso_code): Currency
|
||||
{
|
||||
//Normalize ISO code
|
||||
$iso_code = strtoupper($iso_code);
|
||||
|
||||
//Try to find currency
|
||||
$currency = $this->findOneBy(['iso_code' => $iso_code]);
|
||||
if ($currency !== null) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
//Create currency if it does not exist
|
||||
$name = Currencies::getName($iso_code);
|
||||
|
||||
$currency = $this->findOrCreateForInfoProvider($name);
|
||||
$currency->setIsoCode($iso_code);
|
||||
|
||||
return $currency;
|
||||
}
|
||||
}
|
|
@ -192,6 +192,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository
|
|||
* Also, it will try to find the element using the additional names field, of the elements.
|
||||
* @param string $name
|
||||
* @return AbstractStructuralDBElement|null
|
||||
* @phpstan-return TEntityClass|null
|
||||
*/
|
||||
public function findForInfoProvider(string $name): ?AbstractStructuralDBElement
|
||||
{
|
||||
|
@ -228,6 +229,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository
|
|||
* Similar to findForInfoProvider, but will create a new element with the given name if none was found.
|
||||
* @param string $name
|
||||
* @return AbstractStructuralDBElement
|
||||
* @phpstan-return TEntityClass
|
||||
*/
|
||||
public function findOrCreateForInfoProvider(string $name): AbstractStructuralDBElement
|
||||
{
|
||||
|
|
|
@ -44,6 +44,8 @@ class PartDetailDTO extends SearchResultDTO
|
|||
public readonly ?array $datasheets = null,
|
||||
/** @var ParameterDTO[]|null */
|
||||
public readonly ?array $parameters = null,
|
||||
/** @var PurchaseInfoDTO[]|null */
|
||||
public readonly ?array $vendor_infos = null,
|
||||
) {
|
||||
parent::__construct(
|
||||
provider_key: $provider_key,
|
||||
|
|
53
src/Services/InfoProviderSystem/DTOs/PriceDTO.php
Normal file
53
src/Services/InfoProviderSystem/DTOs/PriceDTO.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\InfoProviderSystem\DTOs;
|
||||
|
||||
use Brick\Math\BigDecimal;
|
||||
|
||||
/**
|
||||
* This DTO represents a price for a single unit in a certain discount range
|
||||
*/
|
||||
class PriceDTO
|
||||
{
|
||||
private readonly BigDecimal $price_as_big_decimal;
|
||||
|
||||
public function __construct(
|
||||
/** @var float The minimum amount that needs to get ordered for this price to be valid */
|
||||
public readonly float $minimum_discount_amount,
|
||||
/** @var string The price as string (with .) */
|
||||
public readonly string $price,
|
||||
/** @var string The currency of the used ISO code of this price detail */
|
||||
public readonly ?string $currency_iso_code,
|
||||
/** @var bool If the price includes tax */
|
||||
public readonly ?bool $includes_tax = true,
|
||||
)
|
||||
{
|
||||
$this->price_as_big_decimal = BigDecimal::of($this->price);
|
||||
}
|
||||
|
||||
public function getPriceAsBigDecimal(): BigDecimal
|
||||
{
|
||||
return $this->price_as_big_decimal;
|
||||
}
|
||||
}
|
44
src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php
Normal file
44
src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\InfoProviderSystem\DTOs;
|
||||
|
||||
class PurchaseInfoDTO
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $distributor_name,
|
||||
public readonly string $order_number,
|
||||
/** @var PriceDTO[] */
|
||||
public readonly array $prices,
|
||||
/** @var string|null An url to the product page of the vendor */
|
||||
public readonly ?string $product_url = null,
|
||||
)
|
||||
{
|
||||
//Ensure that the prices are PriceDTO instances
|
||||
foreach ($this->prices as $price) {
|
||||
if (!$price instanceof PriceDTO) {
|
||||
throw new \InvalidArgumentException('The prices array must only contain PriceDTO instances');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,9 +31,16 @@ use App\Entity\Parameters\PartParameter;
|
|||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\ManufacturingStatus;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
|
@ -42,7 +49,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
class DTOtoEntityConverter
|
||||
{
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em)
|
||||
public function __construct(private readonly EntityManagerInterface $em, private readonly string $base_currency)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -60,6 +67,35 @@ class DTOtoEntityConverter
|
|||
return $entity;
|
||||
}
|
||||
|
||||
public function convertPrice(PriceDTO $dto, Pricedetail $entity = new Pricedetail()): Pricedetail
|
||||
{
|
||||
$entity->setMinDiscountQuantity($dto->minimum_discount_amount);
|
||||
$entity->setPrice($dto->getPriceAsBigDecimal());
|
||||
|
||||
//Currency TODO
|
||||
if ($dto->currency_iso_code !== null) {
|
||||
$entity->setCurrency($this->getCurrency($dto->currency_iso_code));
|
||||
} else {
|
||||
$entity->setCurrency(null);
|
||||
}
|
||||
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function convertPurchaseInfo(PurchaseInfoDTO $dto, Orderdetail $entity = new Orderdetail()): Orderdetail
|
||||
{
|
||||
$entity->setSupplierpartnr($dto->order_number);
|
||||
$entity->setSupplierProductUrl($dto->product_url ?? '');
|
||||
|
||||
$entity->setSupplier($this->getOrCreateEntityNonNull(Supplier::class, $dto->distributor_name));
|
||||
foreach ($dto->prices as $price) {
|
||||
$entity->addPricedetail($this->convertPrice($price));
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function convertFile(FileDTO $dto, PartAttachment $entity = new PartAttachment()): PartAttachment
|
||||
{
|
||||
$entity->setURL($dto->url);
|
||||
|
@ -101,9 +137,22 @@ class DTOtoEntityConverter
|
|||
$entity->addAttachment($this->convertFile($datasheet));
|
||||
}
|
||||
|
||||
//Add orderdetails and prices
|
||||
foreach ($dto->vendor_infos ?? [] as $vendor_info) {
|
||||
$entity->addOrderdetail($this->convertPurchaseInfo($vendor_info));
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of AbstractStructuralDBElement
|
||||
* @param string $class
|
||||
* @phpstan-param class-string<T> $class
|
||||
* @param string|null $name
|
||||
* @return AbstractStructuralDBElement|null
|
||||
* @phpstan-return T|null
|
||||
*/
|
||||
private function getOrCreateEntity(string $class, ?string $name): ?AbstractStructuralDBElement
|
||||
{
|
||||
//Fall through to make converting easier
|
||||
|
@ -111,7 +160,30 @@ class DTOtoEntityConverter
|
|||
return null;
|
||||
}
|
||||
|
||||
return $this->getOrCreateEntityNonNull($class, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of AbstractStructuralDBElement
|
||||
* @param string $class The class of the entity to create
|
||||
* @phpstan-param class-string<T> $class
|
||||
* @param string $name The name of the entity to create
|
||||
* @return AbstractStructuralDBElement
|
||||
* @phpstan-return T|null
|
||||
*/
|
||||
private function getOrCreateEntityNonNull(string $class, string $name): AbstractStructuralDBElement
|
||||
{
|
||||
return $this->em->getRepository($class)->findOrCreateForInfoProvider($name);
|
||||
}
|
||||
|
||||
private function getCurrency(string $iso_code): ?Currency
|
||||
{
|
||||
//Check if the currency is the base currency (then we can just return null)
|
||||
if ($iso_code === $this->base_currency) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->em->getRepository(Currency::class)->findOrCreateByISOCode($iso_code);
|
||||
}
|
||||
|
||||
}
|
|
@ -28,7 +28,9 @@ use App\Form\InfoProviderSystem\ProviderSelectType;
|
|||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class Element14Provider implements InfoProviderInterface
|
||||
|
@ -39,6 +41,8 @@ class Element14Provider implements InfoProviderInterface
|
|||
private const API_VERSION_NUMBER = '1.2';
|
||||
private const NUMBER_OF_RESULTS = 20;
|
||||
|
||||
public const DISTRIBUTOR_NAME = 'Farnell';
|
||||
|
||||
private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant',
|
||||
'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode'];
|
||||
|
||||
|
@ -105,15 +109,21 @@ class Element14Provider implements InfoProviderInterface
|
|||
mpn: $product['translatedManufacturerPartNumber'],
|
||||
preview_image_url: $this->toImageUrl($product['image'] ?? null),
|
||||
manufacturing_status: $this->releaseStatusCodeToManufacturingStatus($product['releaseStatusCode'] ?? null),
|
||||
provider_url: 'https://' . self::FARNELL_STORE_ID . '/' . $product['sku'],
|
||||
provider_url: $this->generateProductURL($product['sku']),
|
||||
datasheets: $this->parseDataSheets($product['datasheets'] ?? null),
|
||||
parameters: $this->attributesToParameters($product['attributes'] ?? null),
|
||||
vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function generateProductURL($sku): string
|
||||
{
|
||||
return 'https://' . self::FARNELL_STORE_ID . '/' . $sku;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[]|null $datasheets
|
||||
* @return FileDTO[]|null Array of FileDTOs
|
||||
|
@ -147,6 +157,90 @@ class Element14Provider implements InfoProviderInterface
|
|||
return 'https://' . self::FARNELL_STORE_ID . '/productimages/standard/' . $locale . $image['baseName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO
|
||||
* @param string $sku
|
||||
* @param array $prices
|
||||
* @return array
|
||||
*/
|
||||
private function pricesToVendorInfo(string $sku, array $prices): array
|
||||
{
|
||||
$price_dtos = [];
|
||||
|
||||
foreach ($prices as $price) {
|
||||
$price_dtos[] = new PriceDTO(
|
||||
minimum_discount_amount: $price['from'],
|
||||
price: (string) $price['cost'],
|
||||
currency_iso_code: $this->getUsedCurrency(),
|
||||
includes_tax: false,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
new PurchaseInfoDTO(
|
||||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $sku,
|
||||
prices: $price_dtos,
|
||||
product_url: $this->generateProductURL($sku)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public function getUsedCurrency(): string
|
||||
{
|
||||
//Decide based on the shop ID
|
||||
return match (self::FARNELL_STORE_ID) {
|
||||
'bg.farnell.com' => 'EUR',
|
||||
'cz.farnell.com' => 'CZK',
|
||||
'dk.farnell.com' => 'DKK',
|
||||
'at.farnell.com' => 'EUR',
|
||||
'ch.farnell.com' => 'CHF',
|
||||
'de.farnell.com' => 'EUR',
|
||||
'cpc.farnell.com' => 'GBP',
|
||||
'cpcireland.farnell.com' => 'EUR',
|
||||
'export.farnell.com' => 'GBP',
|
||||
'onecall.farnell.com' => 'GBP',
|
||||
'ie.farnell.com' => 'EUR',
|
||||
'il.farnell.com' => 'USD',
|
||||
'uk.farnell.com' => 'GBP',
|
||||
'es.farnell.com' => 'EUR',
|
||||
'ee.farnell.com' => 'EUR',
|
||||
'fi.farnell.com' => 'EUR',
|
||||
'fr.farnell.com' => 'EUR',
|
||||
'hu.farnell.com' => 'HUF',
|
||||
'it.farnell.com' => 'EUR',
|
||||
'lt.farnell.com' => 'EUR',
|
||||
'lv.farnell.com' => 'EUR',
|
||||
'be.farnell.com' => 'EUR',
|
||||
'nl.farnell.com' => 'EUR',
|
||||
'no.farnell.com' => 'NOK',
|
||||
'pl.farnell.com' => 'PLN',
|
||||
'pt.farnell.com' => 'EUR',
|
||||
'ro.farnell.com' => 'EUR',
|
||||
'ru.farnell.com' => 'RUB',
|
||||
'sk.farnell.com' => 'EUR',
|
||||
'si.farnell.com' => 'EUR',
|
||||
'se.farnell.com' => 'SEK',
|
||||
'tr.farnell.com' => 'TRY',
|
||||
'canada.newark.com' => 'CAD',
|
||||
'mexico.newark.com' => 'MXN',
|
||||
'www.newark.com' => 'USD',
|
||||
'cn.element14.com' => 'CNY',
|
||||
'au.element14.com' => 'AUD',
|
||||
'nz.element14.com' => 'NZD',
|
||||
'hk.element14.com' => 'HKD',
|
||||
'sg.element14.com' => 'SGD',
|
||||
'my.element14.com' => 'MYR',
|
||||
'ph.element14.com' => 'PHP',
|
||||
'th.element14.com' => 'THB',
|
||||
'in.element14.com' => 'INR',
|
||||
'tw.element14.com' => 'TWD',
|
||||
'kr.element14.com' => 'KRW',
|
||||
'vn.element14.com' => 'VND',
|
||||
default => throw new \RuntimeException('Unknown store ID: ' . self::FARNELL_STORE_ID)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $attributes
|
||||
* @return ParameterDTO[]|null
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue