* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ namespace PrestaShopBundle\Command; use DOMDocument; use SimpleXMLElement; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * This script will update the tax rule groups for virtual products from all EU localization packs. * All it needs is that the correct tax in each localization pack is marked with `eu-tax-group="virtual"`. **. * * 1) * Parse all files under /localization, * looking for elements that have the attribute eu-tax-group="virtual". * * Store the list of files (`$euLocalizationFiles`) where such taxes have been found, * in a next step we'll store the new tax group in each of them. * * 2) * Remove all taxRulesGroup's that have the attribute eu-tax-group="virtual". * * 3) * Build a new taxRulesGroup containing all the taxes found in the first step. * * 4) * Inject the new taxRulesGroup into all packs of `$euLocalizationFiles`, not forgetting * to also inject the required taxes. * * Warning: do not duplicate the tax with attribute eu-tax-group="virtual" of the pack being updated. * * Mark the injected group with the attributes eu-tax-group="virtual" and auto-generated="1" * Mark the injected taxes witth the attributes from-eu-tax-group="virtual" and auto-generated="1" * * Clean things up by removing all the previous taxes that had the attributes eu-tax-group="virtual" and auto-generated="1" */ class UpdateEUTaxruleGroupsCommand extends Command { /** * @var OutputInterface */ private $output; /** * @var string */ private $localizationPath; public function __construct(string $localizationPath) { parent::__construct(); $this->localizationPath = $localizationPath; } protected function configure() { $this ->setName('prestashop:taxes:update-eu-tax-rule-groups') ->setDescription('Update EU Tax rule groups'); } public function execute(InputInterface $input, OutputInterface $output) { /* Tweak */ $this->output = $output; if (!file_exists($this->localizationPath)) { $output->writeln( "Could not find the folder containing the localization files (should be 'localization' at the root of the PrestaShop folder)" ); return 1; } $euLocalizationFiles = []; foreach (scandir($this->localizationPath, SCANDIR_SORT_ASCENDING) as $entry) { if (!preg_match('/\.xml$/', $entry)) { continue; } $localizationPackFile = $this->localizationPath . DIRECTORY_SEPARATOR . $entry; $localizationPack = @simplexml_load_file($localizationPackFile); // Some packs do not have taxes if (!($localizationPack instanceof SimpleXMLElement) || !isset($localizationPack->taxes->tax)) { continue; } foreach ($localizationPack->taxes->tax as $tax) { if ((string) $tax['eu-tax-group'] === 'virtual') { if (!isset($euLocalizationFiles[$localizationPackFile])) { $euLocalizationFiles[$localizationPackFile] = [ 'virtualTax' => $tax, 'pack' => $localizationPack, 'iso_code_country' => basename($entry, '.xml'), ]; } else { $output->writeln("Too many taxes with eu-tax-group=\"virtual\" found in `$localizationPackFile`."); return 1; } } } } foreach ($euLocalizationFiles as $path => $file) { $nodesToKill = []; // Get max tax id, and list of nodes to kill $taxId = 0; foreach ($file['pack']->taxes->tax as $tax) { if ((string) $tax['auto-generated'] === '1' && (string) $tax['from-eu-tax-group'] === 'virtual') { $nodesToKill[] = $tax; } else { // We only count the ids of the taxes we're not going to remove! $taxId = max($taxId, (int) $tax['id']); } } foreach ($file['pack']->taxes->taxRulesGroup as $trg) { if ((string) $trg['auto-generated'] === '1' && (string) $trg['eu-tax-group'] === 'virtual') { $nodesToKill[] = $trg; } } // This is the first tax id we're allowed to use. ++$taxId; // Prepare new taxRulesGroup $taxRulesGroup = $file['pack']->taxes->addChild('taxRulesGroup'); $taxRulesGroup->addAttribute('name', 'EU VAT For Virtual Products'); $taxRulesGroup->addAttribute('auto-generated', '1'); $taxRulesGroup->addAttribute('eu-tax-group', 'virtual'); $this->addTaxRule($taxRulesGroup, $file['virtualTax'], $file['iso_code_country']); foreach ($euLocalizationFiles as $foreignPath => $foreignFile) { if ($foreignPath === $path) { // We already added the tax that belongs to this pack continue; } $tax = $this->addTax($file['pack']->taxes, $foreignFile['virtualTax'], [ 'id' => (string) $taxId, 'auto-generated' => '1', 'from-eu-tax-group' => 'virtual', ], ['eu-tax-group']); $this->addTaxRule($taxRulesGroup, $tax, $foreignFile['iso_code_country']); ++$taxId; } foreach ($nodesToKill as $node) { unset($node[0]); } $dom = new DOMDocument('1.0'); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadXML($file['pack']->asXML()); file_put_contents($path, $dom->saveXML()); } $nUpdated = count($euLocalizationFiles); $output->writeln("Updated the virtual tax groups for $nUpdated localization files"); return 0; } protected function addTax(SimpleXMLElement $taxes, SimpleXMLElement $tax, array $attributesToUpdate = [], array $attributesToRemove = []) { $newTax = new SimpleXMLElement(''); $taxRulesGroups = $taxes->xpath('//taxRulesGroup[1]'); $insertBefore = $taxRulesGroups[0] ?? false; if (!$insertBefore) { return $this->output->writeln("Could not find any `taxRulesGroup`, don't know where to append the tax."); } /** * Add the `tax` node before the first `taxRulesGroup`. * Yes, the dom API is beautiful. */ $dom = dom_import_simplexml($taxes); $new = $dom->insertBefore( $dom->ownerDocument->importNode(dom_import_simplexml($newTax)), dom_import_simplexml($insertBefore) ); $newTax = simplexml_import_dom($new); $newAttributes = []; foreach ($tax->attributes() as $attribute) { $name = $attribute->getName(); // This attribute seems to cause trouble, skip it. if ($name === 'account_number' || in_array($name, $attributesToRemove)) { continue; } $value = (string) $attribute; $newAttributes[$name] = $value; } $newAttributes = array_merge($newAttributes, $attributesToUpdate); foreach ($newAttributes as $name => $value) { $newTax->addAttribute($name, $value); } return $newTax; } protected function addTaxRule(SimpleXMLElement $taxRulesGroup, SimpleXMLElement $tax, $iso_code_country) { $taxRule = $taxRulesGroup->addChild('taxRule'); $taxRule->addAttribute('iso_code_country', $iso_code_country); $taxRule->addAttribute('id_tax', (string) $tax['id']); return $taxRule; } }