diff --git a/composer.json b/composer.json index 75eb3490..cc3ba99e 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 3b594736..866da65e 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Command/SturaImportCommand.php b/src/Command/SturaImportCommand.php new file mode 100644 index 00000000..31fe02b2 --- /dev/null +++ b/src/Command/SturaImportCommand.php @@ -0,0 +1,286 @@ + '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; + } +} diff --git a/symfony.lock b/symfony.lock index 20f42753..9262c9d0 100644 --- a/symfony.lock +++ b/symfony.lock @@ -199,6 +199,9 @@ "lcobucci/jwt": { "version": "3.3.1" }, + "league/csv": { + "version": "9.6.2" + }, "league/html-to-markdown": { "version": "4.8.2" },