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
+
+