diff --git a/docs/configuration.md b/docs/configuration.md index 1ef30c11d..f509fca2a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -181,7 +181,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept * `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first time). Also specify the default order of the columns. This is a comma separated list of column names. Available columns - are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. + are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `supplier`, `supplier_part_number`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. ### History/Eventlog-related settings diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php index c10979cc6..a4f16a653 100644 --- a/src/DataTables/Helpers/PartDataTableHelper.php +++ b/src/DataTables/Helpers/PartDataTableHelper.php @@ -24,6 +24,7 @@ namespace App\DataTables\Helpers; use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; @@ -118,6 +119,61 @@ public function renderStorageLocations(Part $context): string return implode('
', $tmp); } + /** + * Renders the names of all suppliers of this part (taken from its orderdetails), linked to their part lists. + * Each supplier is only listed once, even if the part has multiple orderdetails for it. + */ + public function renderSuppliers(Part $context): string + { + $tmp = []; + foreach ($context->getOrderdetails() as $orderdetail) { + $supplier = $orderdetail->getSupplier(); + //Ignore orderdetails without supplier + if (!$supplier instanceof Supplier) { + continue; + } + $tmp[$supplier->getID()] = sprintf( + '%s', + $this->entityURLGenerator->listPartsURL($supplier), + htmlspecialchars($this->pathCache->getFullPath($supplier)), + htmlspecialchars($supplier->getName()) + ); + } + + return implode(', ', $tmp); + } + + /** + * Renders the supplier part numbers of all orderdetails of this part. A number is linked to the product page of + * the supplier, if one is known. + */ + public function renderSupplierPartNumbers(Part $context): string + { + $tmp = []; + foreach ($context->getOrderdetails() as $orderdetail) { + $number = $orderdetail->getSupplierPartNr(); + if ($number === '') { + continue; + } + + $title = $orderdetail->getSupplier()?->getName() ?? ''; + $url = $orderdetail->getSupplierProductUrl(); + //The URL can be entered freely by users, so only link to real web addresses + if (preg_match('#^https?://#i', $url) === 1) { + $tmp[$number] = sprintf( + '%s', + htmlspecialchars($url), + htmlspecialchars($title), + htmlspecialchars($number) + ); + } else { + $tmp[$number] ??= sprintf('%s', htmlspecialchars($title), htmlspecialchars($number)); + } + } + + return implode(', ', $tmp); + } + /** * Renders an EDA/KiCad completeness indicator for the given part. * Shows icons for symbol, footprint, and value status. diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index 66fda24d1..bfe683b9f 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -181,6 +181,21 @@ public function configure(DataTable $dataTable, array $options): void 'orderable' => false, 'data' => fn(Part $context) => $this->partDataTableHelper->renderSupplierAvailableAmount($context), ]) + ->add('supplier', HTMLColumn::class, [ + 'label' => $this->translator->trans('part.table.supplier'), + //A part can have many suppliers, so we sort by the first one (see storelocation above). The leading + //expression keeps the parts without any supplier at the end, regardless of the sort direction and + //of how the DBMS orders NULLs (the direction is only appended to the last expression). + 'orderField' => 'CASE WHEN MIN(_suppliers.name) IS NULL THEN 1 ELSE 0 END, NATSORT(MIN(_suppliers.name))', + 'data' => fn(Part $context) => $this->partDataTableHelper->renderSuppliers($context), + ]) + ->add('supplier_part_number', HTMLColumn::class, [ + 'label' => $this->translator->trans('part.table.supplier_part_number'), + //Same as for the supplier column. Empty supplier part numbers are treated like missing ones. + 'orderField' => 'CASE WHEN MIN(NULLIF(_orderdetails.supplierpartnr, \'\')) IS NULL THEN 1 ELSE 0 END, ' + . 'NATSORT(MIN(NULLIF(_orderdetails.supplierpartnr, \'\')))', + 'data' => fn(Part $context) => $this->partDataTableHelper->renderSupplierPartNumbers($context), + ]) ->add('minamount', TextColumn::class, [ 'label' => $this->translator->trans('part.table.minamount'), 'data' => fn(Part $context, $value): string => $this->amountFormatter->format( @@ -370,6 +385,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->addSelect('footprint_attachment') ->addSelect('partLots') ->addSelect('orderdetails') + ->addSelect('suppliers') ->addSelect('attachments') ->addSelect('storelocations') ->addSelect('projectBomEntries') @@ -388,6 +404,7 @@ private function getDetailQuery(QueryBuilder $builder, array $filter_results): v ->leftJoin('footprint.master_picture_attachment', 'footprint_attachment') ->leftJoin('part.manufacturer', 'manufacturer') ->leftJoin('part.orderdetails', 'orderdetails') + ->leftJoin('orderdetails.supplier', 'suppliers') ->leftJoin('part.attachments', 'attachments') ->leftJoin('part.partUnit', 'partUnit') ->leftJoin('part.partCustomState', 'partCustomState') diff --git a/src/Settings/BehaviorSettings/PartTableColumns.php b/src/Settings/BehaviorSettings/PartTableColumns.php index e2005ea12..41310cb3f 100644 --- a/src/Settings/BehaviorSettings/PartTableColumns.php +++ b/src/Settings/BehaviorSettings/PartTableColumns.php @@ -39,6 +39,8 @@ enum PartTableColumns : string implements TranslatableInterface case LOCATION = "storage_location"; case AMOUNT = "amount"; case SUPPLIER_AVAILABLE_AMOUNT = "supplier_available_amount"; + case SUPPLIER = "supplier"; + case SUPPLIER_PART_NUMBER = "supplier_part_number"; case MIN_AMOUNT = "minamount"; case PART_UNIT = "partUnit"; case ADDED_DATE = "addedDate"; diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index a701aac7f..3eff7092f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -15926,5 +15926,17 @@ Buerklin-API Authentication server: This info provider has to throttle its requests heavily or needs a lot of processing time, so requests can take a while. + + + part.table.supplier + Supplier + + + + + part.table.supplier_part_number + Supplier part number + +