Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Jan Böhmer
407d78b151 Added a simple tool to import stura CSV lists. 2021-10-03 23:26:24 +02:00
4 changed files with 375 additions and 1 deletions

View file

@ -22,6 +22,7 @@
"florianv/swap-bundle": "dev-master",
"friendsofsymfony/ckeditor-bundle": "^2.0",
"gregwar/captcha-bundle": "^2.1.0",
"league/csv": "^9.6",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
"nelmio/security-bundle": "^2.9",

86
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8baea752d676ffb53b92c4cda2e2150e",
"content-hash": "667c4b8cd1e3beb7ad1dab32ecd9e874",
"packages": [
{
"name": "beberlei/assert",
@ -2707,6 +2707,90 @@
],
"time": "2021-09-28T19:18:28+00:00"
},
{
"name": "league/csv",
"version": "9.6.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "f28da6e483bf979bac10e2add384c90ae9983e4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/f28da6e483bf979bac10e2add384c90ae9983e4e",
"reference": "f28da6e483bf979bac10e2add384c90ae9983e4e",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=7.2.5"
},
"require-dev": {
"ext-curl": "*",
"ext-dom": "*",
"friendsofphp/php-cs-fixer": "^2.16",
"phpstan/phpstan": "^0.12.0",
"phpstan/phpstan-phpunit": "^0.12.0",
"phpstan/phpstan-strict-rules": "^0.12.0",
"phpunit/phpunit": "^8.5"
},
"suggest": {
"ext-dom": "Required to use the XMLConverter and or the HTMLConverter classes",
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Csv\\": "src"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "CSV data manipulation made easy in PHP",
"homepage": "http://csv.thephpleague.com",
"keywords": [
"convert",
"csv",
"export",
"filter",
"import",
"read",
"transform",
"write"
],
"support": {
"docs": "https://csv.thephpleague.com",
"issues": "https://github.com/thephpleague/csv/issues",
"rss": "https://github.com/thephpleague/csv/releases.atom",
"source": "https://github.com/thephpleague/csv"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"time": "2020-12-10T19:40:30+00:00"
},
{
"name": "league/html-to-markdown",
"version": "5.0.1",

View file

@ -0,0 +1,286 @@
<?php
namespace App\Command;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use Brick\Math\BigDecimal;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use League\Csv\CharsetConverter;
use League\Csv\Reader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class SturaImportCommand extends Command
{
protected static $defaultName = 'app:stura-import';
protected static $defaultDescription = 'Add a short description for your command';
protected $entityManager;
protected $dry_run;
const NORMALIZE_MAP = [
'Tag der Buchung' => 'booking_date',
'Beleg-Nr.' => 'reference_id',
'Anzahl' => 'amount',
'Bezeichnung' => 'name',
'Lieferant/Empfänger' => 'supplier',
'Stückpreis in Euro' => 'price',
'Standort' => 'location',
'Anmerkungen' => 'comment',
'Zugang' => 'incoming_date',
'Abgang' => 'outcoming_date',
];
public function __construct(string $name = null, EntityManagerInterface $entityManager)
{
parent::__construct($name);
$this->entityManager = $entityManager;
}
protected function configure(): void
{
$this
->addArgument('input', InputArgument::REQUIRED, 'The input electoral register as CSV')
->addOption('dry', null, InputOption::VALUE_NONE, 'Dry run (Dont write changes to databse)')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$filename = $input->getArgument('input');
$dry = $input->getOption('dry');
$this->dry_run = $dry;
$csv = Reader::createFromPath($filename, 'r');
$csv->setDelimiter(';');
$csv->setHeaderOffset(0);
$stream = (new CharsetConverter())->inputEncoding('iso-8859-15')->convert($csv);
$io->info('Use file ' . $csv->getPathname());
$io->info(sprintf('File contains %d entries', $csv->count()));
$progressBar = new ProgressBar($output, $csv->count());
$progressBar->start();
foreach ($stream as $entry)
{
$data = $this->normalizeData($entry);
//Skip empty or not existing things
if(empty($data['name']) || $data['amount'] === '0') {
continue;
}
$existing_part = $this->tryToFindExistingPart($data);
$io->info(sprintf('Try to find "%s" from %s.', $data['name'], $data['location']));
if ($existing_part !== null) {
$io->info(sprintf('Found existing part. ID: %d', $existing_part->getID()));
$this->updateExistingPart($existing_part, $data);
} else {
$io->info('Part not found. Create a new one.');
$new_part = $this->createPartFromData($data);
$this->entityManager->persist($new_part);
}
$progressBar->advance();
}
$progressBar->finish();
if (!$dry) {
$this->entityManager->flush();
$io->success('Successfully wrote changes to database!');
} else {
$io->warning('Dry run mode activated. Changes were not written to DB.');
}
return Command::SUCCESS;
}
protected function normalizeData(array $data): array
{
$return = [];
foreach ($data as $key => $value) {
if (isset(self::NORMALIZE_MAP[$key])) {
$return[self::NORMALIZE_MAP[$key]] = $value;
}
}
return $return;
}
protected function tryToFindExistingPart(array $data): ?Part
{
//Always return null
return null;
//Find location
$repo = $this->entityManager->getRepository(Storelocation::class);
$location = $repo->findOneBy(['name' => $data['location']]);
//Return early if no location was created yet
if ($location === null) {
return null;
}
$qb = new QueryBuilder($this->entityManager);
$qb->select('part')
->from(Part::class, 'part')
->leftJoin('part.partLots', 'lots')
//->leftJoin('part.orderdetails', 'orderdetails')
//->leftJoin('orderdetails.pricedetails', 'pricedetails')
//->andWhere('pricedetails.price')
->where('lots.storage_location = ?1')
->andWhere('part.name = ?2')
->setParameter(1, $location)
->setParameter(2, $data['name'])
;
$result = $qb->getQuery()->getResult();
if (empty($result)) {
return null;
}
$price_str = $this->getPriceAsFormattedString($data['amount']);
//Check price to ensure it is the same part
foreach ($result as $part) {
/** @var Part $part */
if (!empty($price_str)) {
if (!$part->getOrderdetails()[0]->getPricedetails()[0]->getPrice() !== null) {
return null;
}
}
}
//Assume that part is always not existing
return null;
}
protected function updateExistingPart(Part $existing_part, array $data): void
{
}
protected function getPriceAsFormattedString(string $amount): string
{
$price_str = trim($amount, " \n\r\t\v\0");
//Use decimal point instead of comma
$price_str = trim(str_replace(',', '.', $price_str));
return $price_str;
}
protected function createPartFromData(array $data): Part
{
$part = new Part();
$part->setName($data['name']);
$part_lot = new PartLot();
$part_lot->setAmount((float) $data['amount']);
$part_lot->setStorageLocation($this->getStorageLocation($data['location']));
$part->addPartLot($part_lot);
$part->setCategory($this->getDummyCategory());
//Add price information
$price_str = $this->getPriceAsFormattedString($data['amount']);
if (!empty($price_str)) {
$orderdetail = new Orderdetail();
$orderdetail->setSupplier($this->getDummySupplier());
$orderdetail->addPricedetail(
(new Pricedetail())
->setMinDiscountQuantity(1)
->setPrice(BigDecimal::of((float) $price_str))
);
$part->addOrderdetail($orderdetail);
}
//Add comment
$comment = '';
if (!empty($data['comment'])) {
$comment .= $data['comment'] . "\n\n";
}
if (!empty($data['booking_date'])) {
$comment .= 'Buchungsdatum: ' . $data['booking_date'] . "\n\n";
}
if (!empty($data['incoming_date'])) {
$comment .= 'Zugang: ' . $data['incoming_date'] . "\n\n";
}
if (!empty($data['outcoming_date_date'])) {
$comment .= 'Ausgang: ' . $data['outcoming_date_date'] . "\n\n";
}
if (!empty($data['reference_id'])) {
$comment .= 'Beleg-Nr.: ' . $data['reference_id'] . "\n\n";
}
$part->setComment($comment);
return $part;
}
protected function getStorageLocation(string $name)
{
$repo = $this->entityManager->getRepository(Storelocation::class);
$existing = $repo->findOneBy(['name' => $name]);
if ($existing) {
return $existing;
}
$new = (new Storelocation())->setName($name);
$this->entityManager->persist($new);
if(!$this->dry_run) {
$this->entityManager->flush();
}
return $new;
}
protected function getDummyCategory()
{
return $this->getDummyNamedEntity('Unsortiert', Category::class);
}
protected function getDummySupplier()
{
return $this->getDummyNamedEntity('Buchwert', Supplier::class);
}
protected function getDummyNamedEntity(string $name, string $class): AbstractNamedDBElement
{
$repo = $this->entityManager->getRepository($class);
$existing = $repo->findOneBy(['name' => $name]);
if($existing) {
return $existing;
}
$new = (new $class)->setName($name);
$this->entityManager->persist($new);
if(!$this->dry_run) {
$this->entityManager->flush();
}
return $new;
}
}

View file

@ -199,6 +199,9 @@
"lcobucci/jwt": {
"version": "3.3.1"
},
"league/csv": {
"version": "9.6.2"
},
"league/html-to-markdown": {
"version": "4.8.2"
},