mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-07-28 20:21:41 +00:00
Add CSV export for Project BOM tables (#1442)
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
Some checks are pending
Build assets artifact / Build assets artifact (push) Waiting to run
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build / merge (push) Blocked by required conditions
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Image Build (FrankenPHP) / merge (push) Blocked by required conditions
Static analysis / Static analysis (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Waiting to run
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Waiting to run
* add Export AS CSV button to the Project BOM Entry tab * added Export as CSV translation * add service file for BOM exporter * added controller for handling exporting project BOM data as CSV file * added tests for Export as CSV controller and exporter code * remove hierarchical paths from being exported in CSV to match datatable format * Fixed message id * Use stimulus functions to refer to stimulus controller --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
This commit is contained in:
parent
af2eae75d4
commit
be03d1f503
7 changed files with 1610 additions and 17 deletions
558
tests/Controller/ProjectControllerTest.php
Normal file
558
tests/Controller/ProjectControllerTest.php
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use App\Entity\UserSystem\User;
|
||||
|
||||
final class ProjectControllerTest extends WebTestCase
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private KernelBrowser $client;
|
||||
|
||||
/**
|
||||
* @var list<object>
|
||||
*/
|
||||
private array $entitiesToRemove = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->client = static::createClient();
|
||||
|
||||
$this->entityManager = self::getContainer()->get(
|
||||
EntityManagerInterface::class
|
||||
);
|
||||
|
||||
$user = $this->entityManager
|
||||
->getRepository(User::class)
|
||||
->findOneBy(['name' => 'admin']);
|
||||
|
||||
$this->assertInstanceOf(
|
||||
User::class,
|
||||
$user,
|
||||
'The admin fixture user was not found.'
|
||||
);
|
||||
|
||||
$this->client->loginUser($user);
|
||||
$this->client->catchExceptions(false);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach (array_reverse($this->entitiesToRemove) as $entity) {
|
||||
if ($this->entityManager->contains($entity)) {
|
||||
$this->entityManager->remove($entity);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->entitiesToRemove = [];
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testExportBOMAsCSV(): void
|
||||
{
|
||||
$project = $this->createProject('Controller CSV Export Test');
|
||||
|
||||
$this->createBOMEntry(
|
||||
$project,
|
||||
'10k resistor',
|
||||
'R1,R2',
|
||||
2.0,
|
||||
'Generic resistor'
|
||||
);
|
||||
|
||||
$this->createBOMEntry(
|
||||
$project,
|
||||
'100nF capacitor',
|
||||
'C1',
|
||||
1.0,
|
||||
'Ceramic capacitor'
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
sprintf(
|
||||
'/en/project/%d/bom/export',
|
||||
$project->getID()
|
||||
),
|
||||
$this->getDataTableRequest([
|
||||
'quantity',
|
||||
'name',
|
||||
'description',
|
||||
'mountnames',
|
||||
], [
|
||||
'BOM Qty.',
|
||||
'Name',
|
||||
'Description',
|
||||
'Mount names',
|
||||
])
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue(
|
||||
$response->isSuccessful(),
|
||||
sprintf(
|
||||
'The CSV export request failed with HTTP %d.',
|
||||
$response->getStatusCode()
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'text/csv; charset=UTF-8',
|
||||
$response->headers->get('Content-Type')
|
||||
);
|
||||
|
||||
$contentDisposition = (string) $response->headers->get(
|
||||
'Content-Disposition'
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'attachment',
|
||||
$contentDisposition
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'.csv',
|
||||
$contentDisposition
|
||||
);
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
$this->assertNotFalse($content);
|
||||
|
||||
/*
|
||||
* Parse the generated CSV so the test verifies the exported data rather
|
||||
* than depending on the exact quoting produced by Symfony's CSV encoder.
|
||||
*/
|
||||
$stream = fopen('php://memory', 'r+');
|
||||
|
||||
$this->assertIsResource($stream);
|
||||
|
||||
fwrite($stream, $content);
|
||||
rewind($stream);
|
||||
|
||||
$header = fgetcsv(
|
||||
$stream,
|
||||
separator: ';',
|
||||
enclosure: '"',
|
||||
escape: ''
|
||||
);
|
||||
|
||||
fclose($stream);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'BOM Qty.',
|
||||
'Name',
|
||||
'Description',
|
||||
'Mount names',
|
||||
],
|
||||
$header
|
||||
);
|
||||
|
||||
$this->assertStringContainsString('10k resistor', $content);
|
||||
$this->assertStringContainsString('Generic resistor', $content);
|
||||
$this->assertStringContainsString('R1, R2', $content);
|
||||
$this->assertStringContainsString('100nF capacitor', $content);
|
||||
$this->assertStringContainsString('Ceramic capacitor', $content);
|
||||
}
|
||||
|
||||
public function testExportBOMIgnoresPagination(): void
|
||||
{
|
||||
$project = $this->createProject(
|
||||
'Controller Unpaginated CSV Export Test'
|
||||
);
|
||||
|
||||
for ($index = 1; $index <= 11; ++$index) {
|
||||
$this->createBOMEntry(
|
||||
$project,
|
||||
sprintf('Pagination test part %02d', $index),
|
||||
sprintf('R%d', $index),
|
||||
1.0,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$request = $this->getDataTableRequest(
|
||||
['name'],
|
||||
['Name']
|
||||
);
|
||||
|
||||
/*
|
||||
* Simulate the browser currently displaying the second page with
|
||||
* ten rows per page. The controller must override these values.
|
||||
*/
|
||||
$request['start'] = 10;
|
||||
$request['length'] = 10;
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
sprintf(
|
||||
'/en/project/%d/bom/export',
|
||||
$project->getID()
|
||||
),
|
||||
$request
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue(
|
||||
$response->isSuccessful(),
|
||||
sprintf(
|
||||
'The CSV export request failed with HTTP %d.',
|
||||
$response->getStatusCode()
|
||||
)
|
||||
);
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
$this->assertNotFalse($content);
|
||||
|
||||
$rows = preg_split('/\R/', trim($content));
|
||||
|
||||
$this->assertIsArray($rows);
|
||||
|
||||
$rows = array_values(array_filter(
|
||||
$rows,
|
||||
static fn(string $row): bool => $row !== ''
|
||||
));
|
||||
|
||||
/*
|
||||
* One heading row plus all eleven BOM entries. If pagination were
|
||||
* applied, only one data row from the second page would be present.
|
||||
*/
|
||||
$this->assertCount(12, $rows);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Pagination test part 01',
|
||||
$content
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'Pagination test part 11',
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
public function testExportBOMPreservesCurrentSortOrder(): void
|
||||
{
|
||||
$project = $this->createProject(
|
||||
'Controller Sorted CSV Export Test'
|
||||
);
|
||||
|
||||
$this->createBOMEntry(
|
||||
$project,
|
||||
'Lower quantity component',
|
||||
'U1',
|
||||
1.0,
|
||||
''
|
||||
);
|
||||
|
||||
$this->createBOMEntry(
|
||||
$project,
|
||||
'Higher quantity component',
|
||||
'U2',
|
||||
5.0,
|
||||
''
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$request = $this->getDataTableRequest(
|
||||
[
|
||||
'quantity',
|
||||
'name',
|
||||
],
|
||||
[
|
||||
'BOM Qty.',
|
||||
'Name',
|
||||
]
|
||||
);
|
||||
|
||||
/*
|
||||
* Quantity is column index 2 in ProjectBomEntriesDataTable.
|
||||
*/
|
||||
$request['order'] = [
|
||||
[
|
||||
'column' => 2,
|
||||
'dir' => 'desc',
|
||||
'name' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
sprintf(
|
||||
'/en/project/%d/bom/export',
|
||||
$project->getID()
|
||||
),
|
||||
$request
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue(
|
||||
$response->isSuccessful(),
|
||||
sprintf(
|
||||
'The CSV export request failed with HTTP %d.',
|
||||
$response->getStatusCode()
|
||||
)
|
||||
);
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
$this->assertNotFalse($content);
|
||||
|
||||
$higherPosition = strpos(
|
||||
$content,
|
||||
'Higher quantity component'
|
||||
);
|
||||
|
||||
$lowerPosition = strpos(
|
||||
$content,
|
||||
'Lower quantity component'
|
||||
);
|
||||
|
||||
$this->assertNotFalse($higherPosition);
|
||||
$this->assertNotFalse($lowerPosition);
|
||||
|
||||
$this->assertLessThan(
|
||||
$lowerPosition,
|
||||
$higherPosition,
|
||||
'The CSV did not preserve descending quantity order.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testExportBOMRequiresExportColumns(): void
|
||||
{
|
||||
$project = $this->createProject(
|
||||
'Controller Missing Columns CSV Export Test'
|
||||
);
|
||||
|
||||
$this->createBOMEntry(
|
||||
$project,
|
||||
'Missing columns test entry',
|
||||
'U1',
|
||||
1.0,
|
||||
''
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'No columns were specified for BOM export.'
|
||||
);
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
sprintf(
|
||||
'/en/project/%d/bom/export',
|
||||
$project->getID()
|
||||
),
|
||||
[
|
||||
'_dt' => 'dt',
|
||||
'draw' => 1,
|
||||
'start' => 0,
|
||||
'length' => 10,
|
||||
'search' => [
|
||||
'value' => '',
|
||||
'regex' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function createProject(string $name): Project
|
||||
{
|
||||
$project = new Project();
|
||||
$project->setName($name);
|
||||
|
||||
$this->entityManager->persist($project);
|
||||
$this->entitiesToRemove[] = $project;
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function createBOMEntry(
|
||||
Project $project,
|
||||
string $name,
|
||||
string $mountnames,
|
||||
float $quantity,
|
||||
string $comment,
|
||||
): ProjectBOMEntry {
|
||||
$entry = new ProjectBOMEntry();
|
||||
$entry->setProject($project);
|
||||
$entry->setName($name);
|
||||
$entry->setMountnames($mountnames);
|
||||
$entry->setQuantity($quantity);
|
||||
$entry->setComment($comment);
|
||||
|
||||
$this->entityManager->persist($entry);
|
||||
$this->entitiesToRemove[] = $entry;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the DataTables state sent by the browser.
|
||||
*
|
||||
* The table callback uses the configured server-side column definitions,
|
||||
* but order indexes refer to the complete ProjectBomEntriesDataTable.
|
||||
*
|
||||
* @param list<string> $exportColumns
|
||||
* @param list<string> $exportLabels
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getDataTableRequest(
|
||||
array $exportColumns,
|
||||
array $exportLabels,
|
||||
): array {
|
||||
return [
|
||||
'_dt' => 'dt',
|
||||
'draw' => 1,
|
||||
'start' => 0,
|
||||
'length' => 10,
|
||||
'columns' => [
|
||||
$this->getColumnRequest(
|
||||
'picture',
|
||||
false,
|
||||
false
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'id',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'quantity',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'partId',
|
||||
false,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'name',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'ipn',
|
||||
false,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'description',
|
||||
false,
|
||||
false
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'category',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'footprint',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'manufacturer',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'manufacturing_status',
|
||||
false,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'mountnames',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'instockAmount',
|
||||
false,
|
||||
false
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'storelocation',
|
||||
false,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'price',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'ext_price',
|
||||
false,
|
||||
false
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'addedDate',
|
||||
true,
|
||||
true
|
||||
),
|
||||
$this->getColumnRequest(
|
||||
'lastModified',
|
||||
true,
|
||||
true
|
||||
),
|
||||
],
|
||||
'order' => [
|
||||
[
|
||||
'column' => 4,
|
||||
'dir' => 'asc',
|
||||
'name' => '',
|
||||
],
|
||||
],
|
||||
'search' => [
|
||||
'value' => '',
|
||||
'regex' => false,
|
||||
],
|
||||
'exportColumns' => $exportColumns,
|
||||
'exportLabels' => $exportLabels,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getColumnRequest(
|
||||
string $data,
|
||||
bool $searchable,
|
||||
bool $orderable,
|
||||
): array {
|
||||
return [
|
||||
'data' => $data,
|
||||
'name' => '',
|
||||
'searchable' => $searchable,
|
||||
'orderable' => $orderable,
|
||||
'search' => [
|
||||
'value' => '',
|
||||
'regex' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
377
tests/Services/ImportExportSystem/ProjectBomExporterTest.php
Normal file
377
tests/Services/ImportExportSystem/ProjectBomExporterTest.php
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Services\ImportExportSystem;
|
||||
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Services\ImportExportSystem\ProjectBomExporter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
final class ProjectBomExporterTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @var ProjectBomExporter
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* @var list<object>
|
||||
*/
|
||||
private array $entitiesToRemove = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->service = self::getContainer()->get(ProjectBomExporter::class);
|
||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach (array_reverse($this->entitiesToRemove) as $entity) {
|
||||
if ($this->entityManager->contains($entity)) {
|
||||
$this->entityManager->remove($entity);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->entitiesToRemove = [];
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testGetOrderedEntriesWithEmptyIDList(): void
|
||||
{
|
||||
$project = new Project();
|
||||
$project->setName('Empty BOM export test');
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$this->service->getOrderedEntries($project, [])
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetOrderedEntriesPreservesRequestedOrder(): void
|
||||
{
|
||||
$project = $this->createProject('Ordered BOM export test');
|
||||
|
||||
$first = $this->createBOMEntry(
|
||||
$project,
|
||||
'First export entry',
|
||||
'R1',
|
||||
1.0,
|
||||
'First comment'
|
||||
);
|
||||
|
||||
$second = $this->createBOMEntry(
|
||||
$project,
|
||||
'Second export entry',
|
||||
'R2',
|
||||
2.0,
|
||||
'Second comment'
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$result = $this->service->getOrderedEntries(
|
||||
$project,
|
||||
[
|
||||
$second->getID(),
|
||||
$first->getID(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertSame($second->getID(), $result[0]->getID());
|
||||
$this->assertSame($first->getID(), $result[1]->getID());
|
||||
}
|
||||
|
||||
public function testGetOrderedEntriesIgnoresEntriesFromAnotherProject(): void
|
||||
{
|
||||
$firstProject = $this->createProject('First BOM export project');
|
||||
$secondProject = $this->createProject('Second BOM export project');
|
||||
|
||||
$firstEntry = $this->createBOMEntry(
|
||||
$firstProject,
|
||||
'First project entry',
|
||||
'R1',
|
||||
1.0,
|
||||
''
|
||||
);
|
||||
|
||||
$secondEntry = $this->createBOMEntry(
|
||||
$secondProject,
|
||||
'Second project entry',
|
||||
'R2',
|
||||
1.0,
|
||||
''
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$result = $this->service->getOrderedEntries(
|
||||
$firstProject,
|
||||
[
|
||||
$secondEntry->getID(),
|
||||
$firstEntry->getID(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame($firstEntry->getID(), $result[0]->getID());
|
||||
}
|
||||
|
||||
public function testCreateRowForUnlinkedBOMEntry(): void
|
||||
{
|
||||
$entry = new ProjectBOMEntry();
|
||||
$entry->setName('10k resistor');
|
||||
$entry->setComment('Generic resistor used for testing');
|
||||
$entry->setMountnames('R1,R2, R3,,');
|
||||
$entry->setQuantity(3.0);
|
||||
|
||||
$row = $this->service->createRow(
|
||||
$entry,
|
||||
[
|
||||
'quantity',
|
||||
'partId',
|
||||
'name',
|
||||
'ipn',
|
||||
'description',
|
||||
'category',
|
||||
'footprint',
|
||||
'manufacturer',
|
||||
'manufacturing_status',
|
||||
'mountnames',
|
||||
'instockAmount',
|
||||
'storelocation',
|
||||
'addedDate',
|
||||
'lastModified',
|
||||
'unknown_column',
|
||||
],
|
||||
[
|
||||
'BOM Qty.',
|
||||
'Part ID',
|
||||
'Name',
|
||||
'IPN',
|
||||
'Description',
|
||||
'Category',
|
||||
'Footprint',
|
||||
'Manufacturer',
|
||||
'Manufacturing status',
|
||||
'Mount names',
|
||||
'In stock',
|
||||
'Storage locations',
|
||||
'Created at',
|
||||
'Last modified',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(3.0, $row['BOM Qty.']);
|
||||
$this->assertNull($row['Part ID']);
|
||||
$this->assertSame('10k resistor', $row['Name']);
|
||||
$this->assertSame('', $row['IPN']);
|
||||
$this->assertSame(
|
||||
'Generic resistor used for testing',
|
||||
$row['Description']
|
||||
);
|
||||
$this->assertSame('', $row['Category']);
|
||||
$this->assertSame('', $row['Footprint']);
|
||||
$this->assertSame('', $row['Manufacturer']);
|
||||
$this->assertSame('', $row['Manufacturing status']);
|
||||
$this->assertSame('R1, R2, R3', $row['Mount names']);
|
||||
$this->assertSame('', $row['In stock']);
|
||||
$this->assertSame('', $row['Storage locations']);
|
||||
$this->assertSame('', $row['Created at']);
|
||||
$this->assertSame('', $row['Last modified']);
|
||||
|
||||
/*
|
||||
* No corresponding label was supplied for this column, so the
|
||||
* exporter falls back to the column name.
|
||||
*/
|
||||
$this->assertArrayHasKey('unknown_column', $row);
|
||||
$this->assertSame('', $row['unknown_column']);
|
||||
}
|
||||
|
||||
public function testCreateRowForLinkedPart(): void
|
||||
{
|
||||
$category = $this->getDefaultCategory();
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('RC0402FR-071ML');
|
||||
$part->setDescription('10k ohm resistor');
|
||||
$part->setIpn('TEST-IPN-001');
|
||||
$part->setCategory($category);
|
||||
|
||||
$project = $this->createProject('Linked part BOM export test');
|
||||
|
||||
$entry = new ProjectBOMEntry();
|
||||
$entry->setProject($project);
|
||||
$entry->setPart($part);
|
||||
$entry->setName('10k ohm resistor');
|
||||
$entry->setComment('BOM entry comment');
|
||||
$entry->setMountnames('R1,R2');
|
||||
$entry->setQuantity(2.0);
|
||||
|
||||
$this->entityManager->persist($part);
|
||||
$this->entityManager->persist($entry);
|
||||
|
||||
$this->entitiesToRemove[] = $entry;
|
||||
$this->entitiesToRemove[] = $part;
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$row = $this->service->createRow(
|
||||
$entry,
|
||||
[
|
||||
'id',
|
||||
'quantity',
|
||||
'partId',
|
||||
'name',
|
||||
'ipn',
|
||||
'description',
|
||||
'category',
|
||||
'footprint',
|
||||
'manufacturer',
|
||||
'manufacturing_status',
|
||||
'mountnames',
|
||||
'instockAmount',
|
||||
'storelocation',
|
||||
'price',
|
||||
'ext_price',
|
||||
'addedDate',
|
||||
'lastModified',
|
||||
],
|
||||
[
|
||||
'BOM ID',
|
||||
'BOM Qty.',
|
||||
'Part ID',
|
||||
'Name',
|
||||
'IPN',
|
||||
'Description',
|
||||
'Category',
|
||||
'Footprint',
|
||||
'Manufacturer',
|
||||
'Manufacturing status',
|
||||
'Mount names',
|
||||
'In stock',
|
||||
'Storage locations',
|
||||
'Price',
|
||||
'Extended Price',
|
||||
'Created at',
|
||||
'Last modified',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame($entry->getID(), $row['BOM ID']);
|
||||
$this->assertSame($part->getID(), $row['Part ID']);
|
||||
$this->assertSame(
|
||||
'RC0402FR-071ML 10k ohm resistor',
|
||||
$row['Name']
|
||||
);
|
||||
$this->assertSame('TEST-IPN-001', $row['IPN']);
|
||||
$this->assertSame('10k ohm resistor', $row['Description']);
|
||||
$this->assertSame($category->getName(), $row['Category']);
|
||||
$this->assertSame('', $row['Footprint']);
|
||||
$this->assertSame('', $row['Manufacturer']);
|
||||
$this->assertSame('', $row['Manufacturing status']);
|
||||
$this->assertSame('R1, R2', $row['Mount names']);
|
||||
$this->assertSame('', $row['Storage locations']);
|
||||
|
||||
/*
|
||||
* The exact formatting depends on the configured part unit and
|
||||
* currency, so verify that the formatter produced scalar strings.
|
||||
*/
|
||||
$this->assertIsString($row['BOM Qty.']);
|
||||
$this->assertIsString($row['In stock']);
|
||||
$this->assertIsString($row['Price']);
|
||||
$this->assertIsString($row['Extended Price']);
|
||||
|
||||
$this->assertNotSame('', $row['Created at']);
|
||||
$this->assertNotSame('', $row['Last modified']);
|
||||
}
|
||||
|
||||
public function testCreateRowUsesColumnNameWhenLabelIsMissing(): void
|
||||
{
|
||||
$entry = new ProjectBOMEntry();
|
||||
$entry->setName('Label fallback test');
|
||||
$entry->setComment('');
|
||||
$entry->setMountnames('U1');
|
||||
$entry->setQuantity(1.0);
|
||||
|
||||
$row = $this->service->createRow(
|
||||
$entry,
|
||||
[
|
||||
'name',
|
||||
'mountnames',
|
||||
],
|
||||
[
|
||||
'Component',
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame('Label fallback test', $row['Component']);
|
||||
$this->assertSame('U1', $row['mountnames']);
|
||||
}
|
||||
|
||||
private function createProject(string $name): Project
|
||||
{
|
||||
$project = new Project();
|
||||
$project->setName($name);
|
||||
|
||||
$this->entityManager->persist($project);
|
||||
$this->entitiesToRemove[] = $project;
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function createBOMEntry(
|
||||
Project $project,
|
||||
string $name,
|
||||
string $mountnames,
|
||||
float $quantity,
|
||||
string $comment,
|
||||
): ProjectBOMEntry {
|
||||
$entry = new ProjectBOMEntry();
|
||||
$entry->setProject($project);
|
||||
$entry->setName($name);
|
||||
$entry->setMountnames($mountnames);
|
||||
$entry->setQuantity($quantity);
|
||||
$entry->setComment($comment);
|
||||
|
||||
$this->entityManager->persist($entry);
|
||||
$this->entitiesToRemove[] = $entry;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function getDefaultCategory(): Category
|
||||
{
|
||||
$category = $this->entityManager
|
||||
->getRepository(Category::class)
|
||||
->findOneBy([]);
|
||||
|
||||
if ($category instanceof Category) {
|
||||
return $category;
|
||||
}
|
||||
|
||||
$category = new Category();
|
||||
$category->setName('BOM Export Test Category');
|
||||
|
||||
$this->entityManager->persist($category);
|
||||
$this->entitiesToRemove[] = $category;
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $category;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue