From 395a30ce83bd6cdf4340a5326b8263e28ef54453 Mon Sep 17 00:00:00 2001 From: Okan Date: Wed, 12 Jun 2024 12:21:01 +0300 Subject: [PATCH] Added option to add classification line numbers to squashed invoice rows. Removed xsi:schemaLocation from the generated xml. Updated docs. Added more tests. --- docs/index.md | 1 + docs/invoices/complete-example.md | 2 +- docs/squashing-invoice-rows.md | 119 ++++++++++++++++++++++++++++++ docs/upgrade-guide.md | 49 ++++++++++++ src/Actions/SquashInvoiceRows.php | 47 +++++++++--- src/Models/Invoice.php | 12 ++- src/Xml/InvoicesDocWriter.php | 2 +- tests/InvoiceTest.php | 17 +++++ tests/SquashInvoiceRowsTest.php | 63 ++++++++++++++-- 9 files changed, 292 insertions(+), 20 deletions(-) create mode 100644 docs/squashing-invoice-rows.md diff --git a/docs/index.md b/docs/index.md index 19c86ac..d3e71d8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,6 +3,7 @@ | Εισαγωγή | ΑΑΔΕ myDATA REST API | getting-started | | Εισαγωγή | Εγκατάσταση | installation | | Εισαγωγή | Αναβάθμιση | upgrade-guide | +| Εισαγωγή | Σύνοψη Γραμμών | squashing-invoice-rows | | Εισαγωγή | Σφάλματα | exceptions | | Εισαγωγή | Contributing | contributing | | Περιγραφή λειτουργιών | Αναζήτηση ΑΦΜ | http/search-vat | diff --git a/docs/invoices/complete-example.md b/docs/invoices/complete-example.md index 6819c2c..d98ae64 100644 --- a/docs/invoices/complete-example.md +++ b/docs/invoices/complete-example.md @@ -2,7 +2,7 @@ [👉 Προβολή στο GitHub][1] -[1]: https://github.com/firebed/aade-mydata/blob/4.x/docs/samples/complete-example.php +[1]: https://github.com/firebed/aade-mydata/blob/5.x/docs/samples/complete-example.php ```php use Firebed\AadeMyData\Models\Issuer; diff --git a/docs/squashing-invoice-rows.md b/docs/squashing-invoice-rows.md new file mode 100644 index 0000000..3ee9f79 --- /dev/null +++ b/docs/squashing-invoice-rows.md @@ -0,0 +1,119 @@ +# Σύνοψη Γραμμών Παραστατικού + +Ο Πάροχος ηλεκτρονικής τιμολόγησης και τα ERP διαβιβάζουν υποχρεωτικά μόνο τη σύνοψη +γραμμών και χαρακτηρισμών των παραστατικών. Οι λεπτομέρειες των γραμμών παραστατικού +θα πρέπει να ομαδοποιηθούν και να αθροιστούν κατάλληλα πριν την αποστολή στο myDATA. + +Η σύνοψη γραμμών παραστατικού είναι μια σύνθεση διαδικασία και αναλύεται βάση της: +- κατηγορίας ΦΠΑ γραμμής +- κατηγορίας εξαίρεσης ΦΠΑ γραμμής +- κατηγορίας παρακρατούμενων φόρων γραμμής +- κατηγορίας λοιπών φόρων γραμμής +- κατηγορίας χαρτοσήμου γραμμής +- κατηγορίας τελών γραμμής +- τύπου (recType) γραμμής + +Μετά τη σύνοψη γραμμών παραστατικού, το API υπολογίζει αυτόματα τους χαρακτηρισμών ανά γραμμή. + +### Σύνοψη Γραμμών + +Για τη σύνοψη των γραμμών του παραστατικού αρκεί να καλέσετε τη μέθοδο `squashInvoiceRows` της κλάσης `Invoice`. +Η διαδικασία σύνοψης θα αντικαταστήσει τις λεπτομέρειες των γραμμών με τις συνολικές τους τιμές. + +```php +use Firebed\AadeMyData\Models\Invoice; +use Firebed\AadeMyData\Models\InvoiceDetails; + +$invoice = new Invoice(); +$invoice->addInvoiceDetails(new InvoiceDetails(...)); +$invoice->addInvoiceDetails(new InvoiceDetails(...)); +$invoice->addInvoiceDetails(new InvoiceDetails(...)); +$invoice->addInvoiceDetails(new InvoiceDetails(...)); +// ... set more details + +$invoice->squashInvoiceRows(); +$invoice->summarizeInvoice(); + +print_r($invoice->toXml()); +``` + +> [!CAUTION] +> Η σύνοψη γραμμών δεν πρέπει να εφαρμοστεί για όλα τα είδη των παραστατικών. +> Για παράδειγμα, τα δελτία αποστολής δε χρειάζονται σύνοψη γραμμών. + +## Παράδειγμα + +### XML παραστατικού πριν τη σύνοψη + +```xml + + ... + + 4.03 + 1 + 0.97 + + E3_561_001 + category1_1 + 4.03 + + + + 4.03 + 1 + 0.97 + + E3_561_001 + category1_1 + 2.02 + + + E3_561_007 + category1_1 + 2.01 + + + + 4.03 + 1 + 0.97 + + E3_561_001 + category1_1 + 2.02 + + + E3_561_007 + category1_1 + 2.01 + + + ... + + ``` + +### XML παραστατικού μετά τη σύνοψη + +```xml + + ... + + 12.09 + 1 + 2.91 + + E3_561_001 + category1_1 + 8.07 + 1 + + + E3_561_007 + category1_1 + 4.02 + 2 + + + ... + +``` \ No newline at end of file diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md index 041838f..6ca0bc5 100644 --- a/docs/upgrade-guide.md +++ b/docs/upgrade-guide.md @@ -1,5 +1,54 @@ # Οδηγός Αναβάθμισης +## Upgrade guide from 4.x to 5.x + +### Breaking changes +- Removed vat-registry dependency from composer. You can include vat-registry by running `composer require firebed/vat-registry`. +- [See vat-registry](https://github.com/firebed/vat-registry) documentation for more information. +- If you were not using vat search, you can safely ignore this change. + +### Features + +- Ability to "squash" invoice rows `$invoice->squashInvoiceRows()`. + > Ο Πάροχος ηλεκτρονικής τιμολόγησης και τα ERP διαβιβάζουν υποχρεωτικά μόνο τη σύνοψη + γραμμών και χαρακτηρισμών των παραστατικών και όχι αναλυτικά τις γραμμές. [Δείτε Σύνοψη Γραμμών Παραστατικού](../docs/squashing-invoice-rows) για περισσότερες λεπτομέρειες. +- Ability to validate invoices against xsd files before sending them to myDATA. + - `$invoice->validate()`. +- Ability to preview invoice xml before sending it to myDATA. + - `$invoice->toXml()`. +- Ability to populate model attributes within constructor by using **mixed** array values as parameter. + ```php + use Firebed\AadeMyData\Models\InvoiceDetails; + use Firebed\AadeMyData\Enums\RecType; + use Firebed\AadeMyData\Enums\IncomeClassificationType; + use Firebed\AadeMyData\Enums\IncomeClassificationCategory; + + new InvoiceDetails([ + 'lineNumber' => 1, + 'netValue' => 5, + 'recType' => RecType::TYPE_2, + 'incomeClassification' => [ + [ + 'classificationType' => IncomeClassificationType::E3_561_001, + 'classificationCategory' => IncomeClassificationCategory::CATEGORY_1_1, + 'amount' => '5' + ] + ] + ]) + ``` +- Model setters are now fluent (chainable). + - `$invoice->setIssuer(...)->setCounterpart(...)`. +- New methods: Invoice::setTaxesTotals, Invoice::setOtherTransportDetails. +- Implemented `add_` methods to add an amount to InvoiceDetails and Classifications attributes (e.g. `$row->addNetValue(5)`, `$row->addVatAmount(1.2)` etc). +- Implemented endpoints for electronic invoice providers. + +### Fixes + +- Fixed tax calculation when summarizing invoice. +- Removed ext-soap dependency from composer. +- Fixed InvoiceDetails::setOtherMeasurementUnitQuantity +- Fixed InvoiceDetails::setOtherMeasurementUnitTitle + ## Αναβάθμιση από 3.x σε 4.x Η έκδοση 4.x περιέχει αρκετές αλλαγές, μεταξύ των οποίων αρκετές diff --git a/src/Actions/SquashInvoiceRows.php b/src/Actions/SquashInvoiceRows.php index 77e0587..3a9d3d5 100644 --- a/src/Actions/SquashInvoiceRows.php +++ b/src/Actions/SquashInvoiceRows.php @@ -28,18 +28,23 @@ class SquashInvoiceRows */ private array $squashedEcls = []; + private array $options = []; + /** * Groups similar rows and returns a new array with the rows summed. * * @param InvoiceDetails[]|null $invoiceRows An array of invoice rows. + * @param array $options Additional options. * @return array|null An array of squashed invoice rows. */ - public function handle(?array $invoiceRows): ?array + public function handle(?array $invoiceRows, array $options = []): ?array { if ($invoiceRows === null) { return null; } + $this->options = $options; + return $this->squashInvoiceRows($invoiceRows); } @@ -192,12 +197,14 @@ private function aggregateExpenseClassifications(string $rowKey, ?array $classif private function mergeAndRoundResults(): array { foreach ($this->squashedRows as $key => $row) { + $clsLineNumber = 1; + if (isset($this->squashedIcls[$key])) { - $row->setIncomeClassification(array_values($this->squashedIcls[$key])); + $row->setIncomeClassification($this->mapClassifications($this->squashedIcls[$key], $clsLineNumber)); } if (isset($this->squashedEcls[$key])) { - $row->setExpensesClassification(array_values($this->squashedEcls[$key])); + $row->setExpensesClassification($this->mapClassifications($this->squashedEcls[$key], $clsLineNumber)); } $this->roundRow($row); @@ -208,9 +215,27 @@ private function mergeAndRoundResults(): array return array_merge(array_values($this->squashedRows), $this->rowsWithRecType); } + /** + * @param ExpensesClassification[]|IncomeClassification[] $classifications + * @param int $lineNumber + * @return array + */ + private function mapClassifications(array $classifications, int &$lineNumber = 1): array + { + // If clsLineNumber option is set to true, we will add a line number to each classification. + if (isset($this->options['clsLineNumber']) && $this->options['clsLineNumber'] === true) { + return array_map(function ($cls) use (&$lineNumber) { + $cls->setId($lineNumber++); + return $cls; + }, array_values($classifications)); + } + + return array_values($classifications); + } + /** * Rounds the values of a row. - * + * * @param InvoiceDetails $row * @return void */ @@ -219,27 +244,27 @@ private function roundRow(InvoiceDetails $row): void if ($row->getNetValue() !== null) { $row->setNetValue(round($row->getNetValue(), 2)); } - + if ($row->getVatAmount() !== null) { $row->setVatAmount(round($row->getVatAmount(), 2)); } - + if ($row->getWithheldAmount() !== null) { $row->setWithheldAmount(round($row->getWithheldAmount(), 2)); } - + if ($row->getFeesAmount() !== null) { $row->setFeesAmount(round($row->getFeesAmount(), 2)); } - + if ($row->getOtherTaxesAmount() !== null) { $row->setOtherTaxesAmount(round($row->getOtherTaxesAmount(), 2)); } - + if ($row->getStampDutyAmount() !== null) { $row->setStampDutyAmount(round($row->getStampDutyAmount(), 2)); } - + if ($row->getDeductionsAmount() !== null) { $row->setDeductionsAmount(round($row->getDeductionsAmount(), 2)); } @@ -277,7 +302,7 @@ private function adjustClassificationAmount(InvoiceDetails $row): void $classificationSum = array_sum(array_map(fn($item) => $item->getAmount(), $classifications)); $diff = round($row->getNetValue() - $classificationSum, 2); - + if ($diff != 0) { end($classifications); $lastKey = key($classifications); diff --git a/src/Models/Invoice.php b/src/Models/Invoice.php index 6364ed6..c0e261b 100644 --- a/src/Models/Invoice.php +++ b/src/Models/Invoice.php @@ -220,10 +220,18 @@ public function setInvoiceSummary(InvoiceSummary $invoiceSummary): static return $this->set('invoiceSummary', $invoiceSummary); } - public function squashInvoiceRows(): static + /** + * "Squashes" similar invoice lines and sums up their values. + * + * @param array{clsLineNumber: bool} $options Squashing options. + * If 'clsLineNumber' == true the process will add line numbers to classifications. + * + * @return $this + */ + public function squashInvoiceRows(array $options = []): static { $squash = new SquashInvoiceRows(); - $this->setInvoiceDetails($squash->handle($this->getInvoiceDetails())); + $this->setInvoiceDetails($squash->handle($this->getInvoiceDetails(), $options)); return $this; } diff --git a/src/Xml/InvoicesDocWriter.php b/src/Xml/InvoicesDocWriter.php index 47b546e..c71022b 100644 --- a/src/Xml/InvoicesDocWriter.php +++ b/src/Xml/InvoicesDocWriter.php @@ -28,7 +28,7 @@ public function asXML(InvoicesDoc $invoicesDoc): string $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', self::XSI); $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:icls', self::ICLS); $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ecls', self::ECLS); - $rootNode->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', self::SCHEMA_LOCATION); +// $rootNode->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', self::SCHEMA_LOCATION); $this->buildArray($rootNode, 'invoice', iterator_to_array($invoicesDoc)); diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php index 823f955..dd84cdc 100644 --- a/tests/InvoiceTest.php +++ b/tests/InvoiceTest.php @@ -2,7 +2,11 @@ namespace Tests; +use Firebed\AadeMyData\Enums\IncomeClassificationCategory; +use Firebed\AadeMyData\Enums\IncomeClassificationType; +use Firebed\AadeMyData\Enums\RecType; use Firebed\AadeMyData\Models\Invoice; +use Firebed\AadeMyData\Models\InvoiceDetails; use PHPUnit\Framework\TestCase; use Tests\Traits\HandlesInvoiceXml; @@ -12,6 +16,19 @@ class InvoiceTest extends TestCase public function test_invoice_xml() { + new InvoiceDetails([ + 'lineNumber' => 1, + 'netValue' => 5, + 'recType' => RecType::TYPE_2, + 'incomeClassification' => [ + [ + 'classificationType' => IncomeClassificationType::E3_561_001, + 'classificationCategory' => IncomeClassificationCategory::CATEGORY_1_1, + 'amount' => '5' + ] + ] + ]); + $invoice = Invoice::factory()->make(); $this->assertNotEmpty($invoice->toXml()); } diff --git a/tests/SquashInvoiceRowsTest.php b/tests/SquashInvoiceRowsTest.php index aa1fa37..ef72a76 100644 --- a/tests/SquashInvoiceRowsTest.php +++ b/tests/SquashInvoiceRowsTest.php @@ -2,6 +2,8 @@ namespace Tests; +use Firebed\AadeMyData\Enums\ExpenseClassificationCategory; +use Firebed\AadeMyData\Enums\ExpenseClassificationType; use Firebed\AadeMyData\Enums\FeesPercentCategory; use Firebed\AadeMyData\Enums\IncomeClassificationCategory; use Firebed\AadeMyData\Enums\IncomeClassificationType; @@ -69,9 +71,8 @@ public function test_same_vat_category_rows_are_squashed() ] ] ])); - - $invoice->squashInvoiceRows()->summarizeInvoice(); - + $invoice->squashInvoiceRows(['clsLineNumber' => true])->summarizeInvoice(); + $rows = $invoice->getInvoiceDetails(); $this->assertNotNull($rows); $this->assertCount(1, $rows); @@ -86,10 +87,12 @@ public function test_same_vat_category_rows_are_squashed() $this->assertEquals(IncomeClassificationCategory::CATEGORY_1_1, $icls[0]->getClassificationCategory()); $this->assertEquals(IncomeClassificationType::E3_561_001, $icls[0]->getClassificationType()); $this->assertEquals(8.07, $rows[0]->getIncomeClassification()[0]->getAmount()); + $this->assertEquals(1, $rows[0]->getIncomeClassification()[0]->getId()); $this->assertEquals(IncomeClassificationCategory::CATEGORY_1_1, $icls[1]->getClassificationCategory()); $this->assertEquals(IncomeClassificationType::E3_561_007, $icls[1]->getClassificationType()); $this->assertEquals(4.02, $rows[0]->getIncomeClassification()[1]->getAmount()); + $this->assertEquals(2, $rows[0]->getIncomeClassification()[1]->getId()); $this->assertEquals(15, $invoice->getInvoiceSummary()->getTotalGrossValue()); $this->assertEquals(8.07, $invoice->getInvoiceSummary()->getIncomeClassifications()[0]->getAmount()); @@ -127,7 +130,7 @@ public function test_same_vat_exemption_category_rows_are_squashed() ] ] ])); - + $invoice->squashInvoiceRows(); $rows = $invoice->getInvoiceDetails(); @@ -143,7 +146,57 @@ public function test_same_vat_exemption_category_rows_are_squashed() $this->assertEquals(IncomeClassificationCategory::CATEGORY_1_1, $rows[0]->getIncomeClassification()[0]->getClassificationCategory()); $this->assertEquals(IncomeClassificationType::E3_106, $rows[0]->getIncomeClassification()[0]->getClassificationType()); $this->assertEquals(20, $rows[0]->getIncomeClassification()[0]->getAmount()); - $this->assertEquals(20, $rows[0]->getIncomeClassification()[0]->getAmount()); + } + + public function test_same_vat_category_with_income_and_expense_classifications() + { + $invoice = new Invoice(); + + $invoice->addInvoiceDetails(new InvoiceDetails([ + 'vatCategory' => VatCategory::VAT_1, + 'netValue' => 10, + 'vatAmount' => 2.4, + 'incomeClassification' => [ + [ + 'classificationCategory' => IncomeClassificationCategory::CATEGORY_1_1, + 'classificationType' => IncomeClassificationType::E3_106, + 'amount' => 10, + ] + ] + ])); + + $invoice->addInvoiceDetails(new InvoiceDetails([ + 'vatCategory' => VatCategory::VAT_1, + 'netValue' => 10, + 'vatAmount' => 2.4, + 'expensesClassification' => [ + [ + 'classificationCategory' => ExpenseClassificationCategory::CATEGORY_2_1, + 'classificationType' => ExpenseClassificationType::E3_101, + 'amount' => 10, + ] + ] + ])); + + $invoice->squashInvoiceRows(); + + $rows = $invoice->getInvoiceDetails(); + $this->assertNotNull($rows); + $this->assertCount(1, $rows); + $this->assertEquals(VatCategory::VAT_1, $rows[0]->getVatCategory()); + $this->assertEquals(20, $rows[0]->getNetValue()); + + $this->assertIsArray($rows[0]->getIncomeClassification()); + $this->assertCount(1, $rows[0]->getIncomeClassification()); + $this->assertEquals(IncomeClassificationCategory::CATEGORY_1_1, $rows[0]->getIncomeClassification()[0]->getClassificationCategory()); + $this->assertEquals(IncomeClassificationType::E3_106, $rows[0]->getIncomeClassification()[0]->getClassificationType()); + $this->assertEquals(10, $rows[0]->getIncomeClassification()[0]->getAmount()); + + $this->assertIsArray($rows[0]->getExpensesClassification()); + $this->assertCount(1, $rows[0]->getExpensesClassification()); + $this->assertEquals(ExpenseClassificationCategory::CATEGORY_2_1, $rows[0]->getExpensesClassification()[0]->getClassificationCategory()); + $this->assertEquals(ExpenseClassificationType::E3_101, $rows[0]->getExpensesClassification()[0]->getClassificationType()); + $this->assertEquals(10, $rows[0]->getExpensesClassification()[0]->getAmount()); } public function test_same_mixed_vat_exemption_category_rows_are_squashed()