diff --git a/composer.json b/composer.json
index ad91a22..2702a2c 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
},
"require": {
"php": ">=7.4",
+
"asika/simple-console": "^1.0",
"kwn/number-to-words": "^1.9",
"mpdf/mpdf": "^8.0",
diff --git a/config/parameters.yaml.dist b/config/parameters.yaml.dist
index 2ba4e7d..58610ae 100644
--- a/config/parameters.yaml.dist
+++ b/config/parameters.yaml.dist
@@ -1,118 +1,24 @@
configuration:
- date_format:
- source: ''
- target: ''
locales:
source: ru
target: en
+ currency_code: 'USD'
images:
- signature: ''
- stamp: ''
- global_substitutions:
- # TODO extract to parameters
- '%invoice_number%': '999'
- '%invoice_date%': '01.01.1970'
+ signature: 'images/sign.png'
+ stamp: 'images/stamp.png'
+
+contract:
+ # Must be set in format parsable by DateTime
+ date_from: '01.01.1970'
services:
-
name:
- source: 'Работа'
- target: 'Work'
- amount: 1000
+ source: 'Работа на работе по договору от %contract_date% за %period_end_date%'
+ target: 'Working on the job for the contract from %contract_date% for %period_end_date%'
+ amount: 9000
units:
- source: 'часа'
- target: 'hour'
- price: 1000
+ source: 'часы'
+ target: 'hours'
+ price: 100
-translations:
- source:
- variables:
- title: 'Счёт на оплату №%invoice_number% от %invoice_date%'
- currency: 'EUR'
- rows:
- supplier: 'Поставщик'
- buyer: 'Покупатель'
- # service table
- th_number: '№'
- th_name: 'Наименование работ, услуг'
- th_amount: 'Кол-во'
- th_units: 'Ед.'
- th_price: 'Цена'
- th_sum: 'Сумма'
- tf_total: 'Итого'
- # bank data
- account_data: 'Банковские реквизиты поставщика'
- account_number: 'Номер счёта'
- bank: 'Банк'
- bank_address: 'Адрес'
- swift: 'SWIFT'
- corr_bank: 'Банк-посредник'
- total_to_pay: 'Всего к оплате'
- signature: 'Подпись'
- supplier:
- title: '123456'
- short_title: '123'
- address: 'some address'
- extra: []
- bank:
- account: '9999999999999999999'
- name: 'Some Bank of some Country'
- address: 'Some address'
- swift: 'SOMESWIFT'
- corr_bank:
- name: 'SOME CORR BANK'
- swift: 'SOMESWIFT2'
- buyer:
- title: '"Some Client" LLC'
- address: 'Some Address'
- extra:
- - 'VAT: EU12313123'
- - 'IBAN: EU123131231231232112'
- static_substitutions:
- '%contract_date%': '01.01.1970'
-
- target:
- variables:
- title: 'Счёт на оплату №%invoice_number% от %invoice_date%'
- currency: 'EUR'
- rows:
- supplier: 'Поставщик'
- buyer: 'Покупатель'
- # service table
- th_number: '№'
- th_name: 'Наименование работ, услуг'
- th_amount: 'Кол-во'
- th_units: 'Ед.'
- th_price: 'Цена'
- th_sum: 'Сумма'
- tf_total: 'Итого'
- # bank data
- account_data: 'Банковские реквизиты поставщика'
- account_number: 'Номер счёта'
- bank: 'Банк'
- bank_address: 'Адрес'
- swift: 'SWIFT'
- corr_bank: 'Банк-посредник'
- total_to_pay: 'Всего к оплате'
- signature: 'Подпись'
- supplier:
- title: '123456'
- short_title: '123'
- address: 'some address'
- extra: []
- bank:
- account: '9999999999999999999'
- name: 'Some Bank of some Country'
- address: 'Some address'
- swift: 'SOMESWIFT'
- corr_bank:
- name: 'SOME CORR BANK'
- swift: 'SOMESWIFT2'
- buyer:
- title: '"Some Client" LLC'
- address: 'Some Address'
- extra:
- - 'VAT: EU12313123'
- - 'IBAN: EU123131231231232112'
- static_substitutions:
- '%contract_date%': '01.01.1970'
\ No newline at end of file
diff --git a/show.php b/show.php
index b494d1c..509f510 100644
--- a/show.php
+++ b/show.php
@@ -3,17 +3,26 @@
require_once __DIR__.'/vendor/autoload.php';
use App\Generator\InvoiceGenerator;
+use App\Invoice\InvoiceData;
+use App\Kernel;
use Mpdf\Mpdf;
use Symfony\Component\Yaml\Yaml;
-$config = Yaml::parseFile(__DIR__.'/config/parameters.yaml');
+$config = Yaml::parseFile(Kernel::getProjectRoot().'/config/parameters.yaml');
+
+$invoice = new InvoiceData(
+ 123,
+ new \DateTime(),
+ new \DateTime($config['contract']['date_from']),
+ new \DateTime
+);
$pdf = $_GET['pdf'] ?? false;
if ($pdf) {
$mpdf = new Mpdf();
- $mpdf->WriteHTML(InvoiceGenerator::generate($config));
+ $mpdf->WriteHTML(InvoiceGenerator::generate($invoice, $config));
$mpdf->Output();
} else {
- echo InvoiceGenerator::generate($config);
+ echo InvoiceGenerator::generate($invoice, $config);
}
diff --git a/src/Generator/InvoiceGenerator.php b/src/Generator/InvoiceGenerator.php
index 9f65bfb..1d7a059 100644
--- a/src/Generator/InvoiceGenerator.php
+++ b/src/Generator/InvoiceGenerator.php
@@ -2,48 +2,32 @@
namespace App\Generator;
+use App\Invoice\InvoiceData;
use App\Kernel;
-use App\Twig\NumberToWordsExtension;
-use App\Util\StringReplacer;
+use App\Text\PlaceholderProcessor;
+use App\Text\Replacer\{LocalizedDateReplacer, StaticReplacer, LocalizedYearMonthReplacer};
+use App\Twig\{NumberToWordsExtension, TextProcessingExtension};
+use Symfony\Component\Yaml\Yaml;
use Twig\Environment as Twig;
use Twig\Extra\{Html\HtmlExtension, Intl\IntlExtension};
use Twig\Loader\FilesystemLoader;
class InvoiceGenerator
{
- public static function generate(array $config): string
+ public static function generate(InvoiceData $invoice, array $config): string
{
- $twig = static::createTwig();
+ $locales = $config['configuration']['locales'];
- $sourceStaticSubstitutions = array_merge(
- $config['translations']['source']['static_substitutions'],
- $config['configuration']['global_substitutions']
- );
- $targetStaticSubstitutions = array_merge(
- $config['translations']['target']['static_substitutions'],
- $config['configuration']['global_substitutions']
- );
+ $postprocessor = static::createPostprocessor($invoice);
- $source = StringReplacer::recursiveReplace(
- $config['translations']['source']['variables'],
- $sourceStaticSubstitutions
- );
- $target = StringReplacer::recursiveReplace(
- $config['translations']['target']['variables'],
- $targetStaticSubstitutions
- );
+ $twig = static::createTwig($postprocessor, $locales['source'], $locales['target']);
- // @TODO fix multilingual substitution depending on the context
- $services = StringReplacer::recursiveReplace($config['services'], $sourceStaticSubstitutions);
$images = static::getImagesContent($config['configuration']['images']);
return $twig->render('invoice.html.twig', [
- 'configuration' => $config['configuration'],
- 'trans_data' => [
- 'source' => $source,
- 'target' => $target,
- ],
- 'services' => $services,
+ 'locales' => $locales,
+ 'currency' => $config['configuration']['currency_code'],
+ 'services' => $config['services'],
'images' => $images,
]);
}
@@ -59,15 +43,48 @@ class InvoiceGenerator
return $new;
}
- private static function createTwig(): Twig
- {
- $loader = new FilesystemLoader(__DIR__.'/../../templates');
+ private static function createTwig(
+ PlaceholderProcessor $placeholderProcessor,
+ string $sourceLocale,
+ string $targetLocale
+ ): Twig {
+ $loader = new FilesystemLoader(Kernel::getProjectRoot().'/templates');
$twig = new Twig($loader);
$twig->addExtension(new NumberToWordsExtension());
$twig->addExtension(new IntlExtension());
$twig->addExtension(new HtmlExtension());
+ $localeData = static::loadLocaleData($sourceLocale, $targetLocale);
+ $twig->addExtension(new TextProcessingExtension($placeholderProcessor, $localeData));
+
return $twig;
}
-}
\ No newline at end of file
+
+ private static function createPostprocessor(InvoiceData $invoice): PlaceholderProcessor
+ {
+ $processor = new PlaceholderProcessor();
+
+ $contractDate = new LocalizedDateReplacer($invoice->getContractStartDate());
+ $processor->addReplacer('%contract_date%', $contractDate);
+
+ $invoiceNumber = new StaticReplacer($invoice->getNumber());
+ $processor->addReplacer('%invoice_number%', $invoiceNumber);
+
+ $invoiceDate = new LocalizedDateReplacer($invoice->getIssueDate());
+ $processor->addReplacer('%invoice_date%', $invoiceDate);
+
+ $periodEnd = new LocalizedYearMonthReplacer($invoice->getAccountedMonthDate());
+ $processor->addReplacer('%period_end_date%', $periodEnd);
+
+ return $processor;
+ }
+
+ private static function loadLocaleData(string $source, string $target): array
+ {
+ return [
+ $source => Yaml::parseFile(Kernel::getProjectRoot().'/translation/source.yaml'),
+ $target => Yaml::parseFile(Kernel::getProjectRoot().'/translation/target.yaml'),
+ ];
+ }
+}
diff --git a/src/Invoice/InvoiceData.php b/src/Invoice/InvoiceData.php
new file mode 100644
index 0000000..32b98a8
--- /dev/null
+++ b/src/Invoice/InvoiceData.php
@@ -0,0 +1,43 @@
+number = $number;
+ $this->issueDate = $issueDate;
+ $this->contractStartDate = $contractStartDate;
+ $this->accountedMonthDate = $accountedMonthDate;
+ }
+
+ public function getNumber(): int
+ {
+ return $this->number;
+ }
+
+ public function getIssueDate(): \DateTime
+ {
+ return $this->issueDate;
+ }
+
+ public function getContractStartDate(): \DateTime
+ {
+ return $this->contractStartDate;
+ }
+
+ public function getAccountedMonthDate(): \DateTime
+ {
+ return $this->accountedMonthDate;
+ }
+}
diff --git a/src/Text/PlaceholderProcessor.php b/src/Text/PlaceholderProcessor.php
new file mode 100644
index 0000000..1a01a4a
--- /dev/null
+++ b/src/Text/PlaceholderProcessor.php
@@ -0,0 +1,49 @@
+replacers = $replacers;
+ }
+
+ public function addReplacer(string $placeholder, ReplacerInterface $replacer): void
+ {
+ if (array_key_exists($placeholder, $this->replacers)) {
+ throw new \RuntimeException('Handler for \''.$placeholder.'\' already exists.');
+ }
+
+ $this->replacers[$placeholder] = $replacer;
+ }
+
+ public function process(
+ string $string,
+ string $locale
+ ): string {
+ foreach ($this->replacers as $placeholder => $replacer) {
+ if (!isset($this->replaceCache[$locale][$placeholder])) {
+ $this->replaceCache[$locale][$placeholder] = $replacer->generateReplace($locale);
+ }
+
+ $string = str_replace(
+ $placeholder,
+ $this->replaceCache[$locale][$placeholder],
+ $string
+ );
+ }
+
+ return $string;
+ }
+}
diff --git a/src/Text/Replacer/LocalizedDateReplacer.php b/src/Text/Replacer/LocalizedDateReplacer.php
new file mode 100644
index 0000000..8586a11
--- /dev/null
+++ b/src/Text/Replacer/LocalizedDateReplacer.php
@@ -0,0 +1,21 @@
+date = $date ?? new \DateTime();
+ }
+
+ public function generateReplace(string $locale): string
+ {
+ $formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::LONG, \IntlDateFormatter::LONG);
+ $formatter->setPattern('d MMMM yyyy');
+
+ return $formatter->format($this->date);
+ }
+}
diff --git a/src/Text/Replacer/LocalizedYearMonthReplacer.php b/src/Text/Replacer/LocalizedYearMonthReplacer.php
new file mode 100644
index 0000000..d00afd9
--- /dev/null
+++ b/src/Text/Replacer/LocalizedYearMonthReplacer.php
@@ -0,0 +1,21 @@
+date = $dateInMonth ?? new \DateTime();
+ }
+
+ public function generateReplace(string $locale): string
+ {
+ $formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::LONG, \IntlDateFormatter::LONG);
+ $formatter->setPattern('LLLL yyyy');
+
+ return $formatter->format($this->date);
+ }
+}
diff --git a/src/Text/Replacer/ReplacerInterface.php b/src/Text/Replacer/ReplacerInterface.php
new file mode 100644
index 0000000..deeaee1
--- /dev/null
+++ b/src/Text/Replacer/ReplacerInterface.php
@@ -0,0 +1,8 @@
+replace = $replace;
+ }
+
+ public function generateReplace(string $locale): string
+ {
+ return $this->replace;
+ }
+}
diff --git a/src/Twig/TextProcessingExtension.php b/src/Twig/TextProcessingExtension.php
new file mode 100644
index 0000000..4fbe53b
--- /dev/null
+++ b/src/Twig/TextProcessingExtension.php
@@ -0,0 +1,52 @@
+ ['find' => 'replace']]
+ */
+ public function __construct(PlaceholderProcessor $replacer, array $translationsByLocale)
+ {
+ $this->replacer = $replacer;
+ $this->translationsByLocale = $translationsByLocale;
+ }
+
+
+ public function getFilters(): array
+ {
+ return [
+ new TwigFilter('trans', [$this, 'translate']),
+ new TwigFilter('process', [$this, 'process']),
+ ];
+ }
+
+ /** Translates the message if possible or returns original text. */
+ public function translate(string $message, string $locale, bool $postprocess = false): string
+ {
+ if (!isset($this->translationsByLocale[$locale][$message])) {
+ return $message;
+ }
+
+ $text = $this->translationsByLocale[$locale][$message];
+
+ if ($postprocess) {
+ $text = $this->replacer->process($text, $locale);
+ }
+
+ return $text;
+ }
+
+ public function process(string $message, string $locale): string
+ {
+ return $this->replacer->process($message, $locale);
+ }
+}
diff --git a/src/Util/StringReplacer.php b/src/Util/StringReplacer.php
deleted file mode 100644
index ee3fdb5..0000000
--- a/src/Util/StringReplacer.php
+++ /dev/null
@@ -1,54 +0,0 @@
- &$value) {
- if (is_string($value)) {
- $value = static::replaceString($value, $staticReplaces);
- } elseif (is_int($value)) {
- continue;
- } elseif (is_array($value)) {
- $value = static::recursiveReplace($value, $staticReplaces);
- } else {
- throw new \InvalidArgumentException(sprintf(
- 'Invalid value. string/array allowed, %s (%s) given.',
- gettype($value),
- print_r($value, true)
- ));
- }
- }
-
- return $variables;
- }
-
- private static function replaceString(
- string $string,
- array $staticReplaces
- ): string {
- // Process static replaces
- $string = str_replace(
- array_keys($staticReplaces),
- array_values($staticReplaces),
- $string
- );
-
- // TBI
- // ...
-
- return $string;
- }
-}
diff --git a/templates/invoice.html.twig b/templates/invoice.html.twig
index 6ea5347..4f7196b 100644
--- a/templates/invoice.html.twig
+++ b/templates/invoice.html.twig
@@ -17,20 +17,20 @@
{{ include ('invoice_side.html.twig', {
- t: trans_data.source,
services: services,
images: images,
context: 'source',
- locale: configuration.locales.source
+ locale: locales.source,
+ currency: currency
}, with_context = false) }}
|
{{ include ('invoice_side.html.twig', {
- t: trans_data.target,
services: services,
images: images,
context: 'target',
- locale: configuration.locales.target
+ locale: locales.target,
+ currency: currency
}, with_context = false) }}
|
diff --git a/templates/invoice_side.html.twig b/templates/invoice_side.html.twig
index 6759604..dc1b928 100644
--- a/templates/invoice_side.html.twig
+++ b/templates/invoice_side.html.twig
@@ -1,4 +1,4 @@
-{{ t.title }}
+{{ 'label_title'|trans(locale, true) }}
@@ -6,36 +6,34 @@
|
- {{ t.rows.supplier }}: |
- {{ t.supplier.title }} |
+ {{ 'label_supplier'|trans(locale) }}: |
+ {{ 'supplier_title'|trans(locale) }} |
- {{ t.supplier.address }} |
+ {{ 'supplier_address'|trans(locale) }} |
- {{ t.rows.buyer }}: |
- {{ t.buyer.title }} |
+ {{ 'label_buyer'|trans(locale) }}: |
+ {{ 'buyer_title'|trans(locale) }} |
- {{ t.buyer.address }} |
+ {{ 'buyer_address'|trans(locale) }} |
- {% for item in t.buyer.extra %}
- {{ item }} |
+ {{ 'buyer_extra'|trans(locale)|nl2br }} |
- {% endfor %}
- {{ t.rows.th_number }} |
- {{ t.rows.th_name }} |
- {{ t.rows.th_amount }} |
- {{ t.rows.th_units }} |
- {{ t.rows.th_price }} |
- {{ t.rows.th_sum }} |
+ {{ 'label_th_number'|trans(locale) }} |
+ {{ 'label_th_name'|trans(locale) }} |
+ {{ 'label_th_amount'|trans(locale) }} |
+ {{ 'label_th_units'|trans(locale) }} |
+ {{ 'label_th_price'|trans(locale) }} |
+ {{ 'label_th_sum'|trans(locale) }} |
@@ -43,20 +41,20 @@
{% for service in services %}
{{ loop.index }} |
- {{ service.name[context] }} |
+ {{ service.name[context]|process(locale) }} |
{{ service.amount }} |
{{ service.units[context] }} |
- {{ service.amount }} {{ t.currency }} |
+ {{ service.price }} {{ currency }} |
{% set sum = service.amount * service.price %}
- {{ sum }} {{ t.currency }} |
+ {{ sum }} {{ currency }} |
{% set total = total + sum %}
{% endfor %}
- {{ t.rows.tf_total }} |
- {{ total }} {{ t.currency }} |
+ {{ 'label_tf_total'|trans(locale) }} |
+ {{ total }} {{ currency }} |
@@ -65,7 +63,7 @@
- {{ t.rows.total_to_pay }}: {{ total|ntw(locale) }} {{ t.currency|currency_name(locale) }}.
+ {{ 'label_total_to_pay'|trans(locale) }}: {{ total|ntw(locale) }} {{ currency|currency_name(locale) }}.
|
@@ -73,33 +71,33 @@
- {{ t.rows.account_data }}: |
+ {{ 'label_supplier_account_data'|trans(locale) }}: |
- {{ t.rows.account_number }} |
- {{ t.supplier.bank.account }} |
+ {{ 'label_supplier_account_number'|trans(locale) }} |
+ {{ 'supplier_bank_account'|trans(locale) }} |
- {{ t.rows.bank }} |
+ {{ 'label_supplier_bank'|trans(locale) }} |
- {{ t.supplier.bank.name }} |
+ {{ 'supplier_bank_name'|trans(locale) }} |
- {{ t.rows.bank_address }} |
- {{ t.supplier.bank.address }} |
+ {{ 'label_supplier_bank_address'|trans(locale) }} |
+ {{ 'supplier_bank_address'|trans(locale) }} |
- {{ t.rows.swift }} |
- {{ t.supplier.bank.swift }} |
+ {{ 'label_swift'|trans(locale) }} |
+ {{ 'supplier_bank_swift'|trans(locale) }} |
- {{ t.rows.corr_bank }} |
- {{ t.supplier.bank.corr_bank.name }} |
+ {{ 'label_supplier_corr_bank'|trans(locale) }} |
+ {{ 'supplier_bank_corr_bank_name'|trans(locale) }} |
- {{ t.rows.swift }} |
- {{ t.supplier.bank.corr_bank.swift }} |
+ {{ 'label_swift'|trans(locale) }} |
+ {{ 'supplier_bank_corr_bank_swift'|trans(locale) }} |
|
@@ -111,7 +109,7 @@
|
- {{ t.rows.signature }} |
+ {{ 'label_signature'|trans(locale) }} |
|
diff --git a/translation/.gitignore b/translation/.gitignore
new file mode 100644
index 0000000..6d2c4b4
--- /dev/null
+++ b/translation/.gitignore
@@ -0,0 +1,2 @@
+*.yaml
+!*.yaml.dist
diff --git a/translation/source.yaml.dist b/translation/source.yaml.dist
new file mode 100644
index 0000000..32ccdec
--- /dev/null
+++ b/translation/source.yaml.dist
@@ -0,0 +1,41 @@
+# Labels
+label_title: 'Счёт на оплату №%invoice_number% от %invoice_date%'
+label_supplier: 'Поставщик'
+label_buyer: 'Покупатель'
+label_th_number: '№'
+label_th_name: 'Наименование работ, услуг'
+label_th_amount: 'Кол-во'
+label_th_units: 'Ед.'
+label_th_price: 'Цена'
+label_th_sum: 'Сумма'
+label_tf_total: 'Итого'
+label_total_to_pay: 'Всего к оплате'
+label_supplier_account_data: 'Банковские реквизиты поставщика'
+label_supplier_account_number: 'Номер счёта'
+label_supplier_bank: 'Банк'
+label_supplier_bank_address: 'Адрес'
+label_swift: 'SWIFT'
+label_supplier_corr_bank: 'Банк-посредник'
+label_signature: 'Подпись'
+
+# Generic data
+
+# Supplier data
+supplier_title: 'ИП/ООО XXX'
+supplier_short_title: 'ИП/ООО XXX'
+supplier_address: 'Россия, г. Мухосранск, ул. Узкая 10'
+#supplier_extra: ''
+supplier_bank_account: '1234567890123456'
+supplier_bank_name: 'BANK OF MOTHER RUSSIA'
+supplier_bank_address: 'KREMLIN, RUSSIA'
+supplier_bank_swift: 'COOLSWIFT'
+supplier_bank_corr_bank_name: 'SOME CORRESPONDENT BANK'
+supplier_bank_corr_bank_swift: 'SADSWIFT'
+
+# Buyer data
+buyer_title: '"Buyer" LLC'
+buyer_address: 'Washington DC, USA'
+buyer_extra: "VAT: US12312312312\n
+IBAN: US1234567890123456"
+
+
diff --git a/translation/target.yaml.dist b/translation/target.yaml.dist
new file mode 100644
index 0000000..89d556d
--- /dev/null
+++ b/translation/target.yaml.dist
@@ -0,0 +1,39 @@
+# Labels
+label_title: 'Invoice %invoice_number% for %invoice_date%'
+label_supplier: 'Supplier'
+label_buyer: 'Buyer'
+label_th_number: '#'
+label_th_name: 'Service'
+label_th_amount: 'Amount'
+label_th_units: 'Units'
+label_th_price: 'Price'
+label_th_sum: 'Sum'
+label_tf_total: 'Total'
+label_total_to_pay: 'Total to pay'
+label_supplier_account_data: 'Supplier bank account'
+label_supplier_account_number: 'Account number'
+label_supplier_bank: 'Beneficiary bank'
+label_supplier_bank_address: 'Bank address'
+label_swift: 'SWIFT'
+label_supplier_corr_bank: 'Correspondent Bank'
+label_signature: 'Подпис'
+
+# Generic data
+
+# Supplier data
+supplier_title: 'IP/LLC XXX'
+supplier_short_title: 'IP/LLC XXX'
+supplier_address: 'Russia, Mukhosransk city, Uzkaya st. 10'
+#supplier_extra: ''
+supplier_bank_account: '1234567890123456'
+supplier_bank_name: 'BANK OF MOTHER RUSSIA'
+supplier_bank_address: 'KREMLIN, RUSSIA'
+supplier_bank_swift: 'COOLSWIFT'
+supplier_bank_corr_bank_name: 'SOME CORRESPONDENT BANK'
+supplier_bank_corr_bank_swift: 'SADSWIFT'
+
+# Buyer data
+buyer_title: '"Buyer" LLC'
+buyer_address: 'Washington DC, USA'
+buyer_extra: "VAT: US12312312312\n
+IBAN: US1234567890123456"