diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb5d1a..5b5ce45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: ${{ matrix.experimental || false }} strategy: + fail-fast: false matrix: php-version: ['7.1', '7.2', '7.3', '7.4'] include: @@ -26,7 +27,7 @@ jobs: with: php-version: ${{ matrix.php-version }} coverage: none - extensions: ast + extensions: ast-stable # Setup Composer - name: Setup Composer @@ -35,7 +36,7 @@ jobs: # Run static analyzer - name: Run static analyzer if: ${{ success() && matrix.php-version != '7.1' }} - run: vendor/bin/phan + run: vendor/bin/phan --color --no-progress-bar # Run tests - name: Run tests @@ -51,9 +52,8 @@ jobs: # Deploy documentation - name: Deploy documentation if: ${{ success() && matrix.deploy || false }} - uses: JamesIves/github-pages-deploy-action@3.7.1 + uses: JamesIves/github-pages-deploy-action@4.1.4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: site - CLEAN: true # Automatically remove deleted files from the deploy branch + branch: gh-pages + folder: site + clean: true # Automatically remove deleted files from the deploy branch diff --git a/.phan/config.php b/.phan/config.php index bbbd20c..4be53c2 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -1,20 +1,42 @@ null, // A list of directories that should be parsed for class and @@ -26,15 +48,9 @@ // your application should be included in this list. 'directory_list' => [ 'src', - 'vendor/josemmo/uxml' + 'vendor', ], - // A regex used to match every file name that you want to - // exclude from parsing. Actual value will exclude every - // "test", "tests", "Test" and "Tests" folders found in - // "vendor/" directory. - 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', - // A directory list that defines files that will be excluded // from static analysis, but whose class and method // information should be included. @@ -47,28 +63,128 @@ // should be added to both the `directory_list` // and `exclude_analysis_directory_list` arrays. 'exclude_analysis_directory_list' => [ - 'vendor/' + 'vendor', ], - // Set to true in order to attempt to detect unused variables. - // `dead_code_detection` will also enable unused variable detection. + // If enabled, Phan will warn if **any** type in a method invocation's object + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => true, + + // If enabled, Phan will warn if **any** type in the argument's union type + // cannot be cast to a type in the parameter's expected union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_param_checking' => true, + + // If enabled, Phan will warn if **any** type in a property assignment's union type + // cannot be cast to a type in the property's declared union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_property_checking' => true, + + // If enabled, Phan will warn if **any** type in a returned value's union type + // cannot be cast to the declared return type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + // (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check) + 'strict_return_checking' => true, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => true, + + // If enabled, check all methods that override a + // parent method to make sure its signature is + // compatible with the parent's. This check + // can add quite a bit of time to the analysis. + // This will also check if final methods are overridden, etc. + 'analyze_signature_compatibility' => true, + + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. + 'check_docblock_signature_return_type_match' => true, + + // If true, check to make sure the param types declared + // in the doc-block (if any) matches the param types + // declared in the method signature. + 'check_docblock_signature_param_type_match' => true, + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. // - // This has a few known false positives, e.g. for loops or branches. - 'unused_variable_detection' => true, + // To more aggressively detect dead code, + // you may want to set `dead_code_detection_prefer_false_negative` to `false`. + 'dead_code_detection' => true, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => true, + + // Set to true in order to attempt to detect error-prone truthiness/falsiness checks. + // + // This is not suitable for all codebases. + 'error_prone_truthy_condition_detection' => true, + + // Enable or disable support for generic templated + // class types. + 'generic_types_enabled' => true, // If enabled, warn about throw statement where the exception types // are not documented in the PHPDoc of functions, methods, and closures. 'warn_about_undocumented_throw_statements' => true, - // If enabled (and warn_about_undocumented_throw_statements is enabled), - // warn about function/closure/method calls that have (at)throws - // without the invoking method documenting that exception. + // If enabled (and `warn_about_undocumented_throw_statements` is enabled), + // Phan will warn about function/closure/method invocations that have `@throws` + // that aren't caught or documented in the invoking method. 'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => true, - // A list of plugin files to execute - // NOTE: values can be the base name without the extension for plugins bundled with Phan (E.g. 'AlwaysReturnPlugin') - // or relative/absolute paths to the plugin (Relative to the project root). + // The minimum severity level to report on. This can be + // set to Issue::SEVERITY_LOW, Issue::SEVERITY_NORMAL or + // Issue::SEVERITY_CRITICAL. + 'minimum_severity' => Issue::SEVERITY_NORMAL, + + // Add any issue types (such as `'PhanUndeclaredMethod'`) + // to this list to inhibit them from being reported. + 'suppress_issue_types' => [ + 'PhanUnreferencedClass', + 'PhanUnreferencedPublicMethod', + ], + + // A list of plugin files to execute. + // Plugins which are bundled with Phan can be added here by providing their name + // (e.g. 'AlwaysReturnPlugin') + // + // Documentation about available bundled plugins can be found + // at https://github.com/phan/phan/tree/v4/.phan/plugins + // + // Alternately, you can pass in the full path to a PHP file + // with the plugin's implementation. + // (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php') 'plugins' => [ + 'AlwaysReturnPlugin', // Checks if a function, closure or method unconditionally returns. + 'DollarDollarPlugin', + 'DuplicateArrayKeyPlugin', + 'DuplicateExpressionPlugin', + 'EmptyStatementListPlugin', + 'InlineHTMLPlugin', + 'LoopVariableReusePlugin', 'PreferNamespaceUsePlugin', - ] + 'PregRegexCheckerPlugin', + 'PrintfCheckerPlugin', + 'SleepCheckerPlugin', + 'UnreachableCodePlugin', // Checks for syntactically unreachable statements in the global scope or function bodies. + 'UseReturnValuePlugin', + ], ]; diff --git a/README.md b/README.md index 0a7e1fb..e51662f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ -# European Invoicing (eInvoicing) -[![Build Status](https://github.com/josemmo/einvoicing/workflows/CI/badge.svg)](https://github.com/josemmo/einvoicing/actions) -[![Latest Version](https://img.shields.io/packagist/v/josemmo/einvoicing?include_prereleases)](https://packagist.org/packages/josemmo/einvoicing) -[![Minimum PHP Version](https://img.shields.io/packagist/php-v/josemmo/einvoicing)](#installation) -[![License](https://img.shields.io/github/license/josemmo/einvoicing)](LICENSE) -[![Documentation](https://img.shields.io/badge/online-docs-blueviolet)](https://josemmo.github.io/einvoicing/) +

+
European Invoicing (eInvoicing)
+

+

+ Build Status + Latest Version + Supported PHP Versions + License + Documentation +

+ +## About eInvoicing is a PHP library for creating and reading electronic invoices according to the [eInvoicing Directive and European standard](https://ec.europa.eu/cefdigital/wiki/display/CEFDIGITAL/eInvoicing). It aims to be 100% compliant with [EN 16931](https://ec.europa.eu/cefdigital/wiki/x/kwFVBg) as well as with the most popular CIUS and extensions, such as [PEPPOL BIS](https://docs.peppol.eu/poacc/billing/3.0/bis/). -> ⚠️ WARNING: This library is almost ready for production. Some features may not be available yet. ⚠️ - ## Installation First of all, make sure your environment meets the following requirements: @@ -24,9 +28,72 @@ composer require josemmo/einvoicing ``` ## Usage -For a quick guide on how to get started, visit the documentation website at +For a proper quick start guide, visit the documentation website at [https://josemmo.github.io/einvoicing/](https://josemmo.github.io/einvoicing/). +### Importing invoice documents +```php +use Einvoicing\Exceptions\ValidationException; +use Einvoicing\Readers\UblReader; + +$reader = new UblReader(); +$document = file_get_contents(__DIR__ . "/example.xml"); +$inv = $reader->import($document); +try { + $inv->validate(); +} catch (ValidationException $e) { + // Invoice is not EN 16931 complaint +} +``` + +### Exporting invoice documents +```php +use Einvoicing\Identifier; +use Einvoicing\Invoice; +use Einvoicing\InvoiceLine; +use Einvoicing\Party; +use Einvoicing\Presets; +use Einvoicing\Writers\UblWriter; + +// Create PEPPOL invoice instance +$inv = new Invoice(Presets\Peppol::class); +$inv->setNumber('F-202000012') + ->setIssueDate(new DateTime('2020-11-01')) + ->setDueDate(new DateTime('2020-11-30')); + +// Set seller +$seller = new Party(); +$seller->setElectronicAddress(new Identifier('9482348239847239874', '0088')) + ->setCompanyId(new Identifier('AH88726', '0183')) + ->setName('Seller Name Ltd.') + ->setTradingName('Seller Name') + ->setVatNumber('ESA00000000') + ->setAddress(['Fake Street 123', 'Apartment Block 2B']) + ->setCity('Springfield') + ->setCountry('DE'); +$inv->setSeller($seller); + +// Set buyer +$buyer = new Party(); +$buyer->setElectronicAddress(new Identifier('ES12345', '0002')) + ->setName('Buyer Name Ltd.') + ->setCountry('FR'); +$inv->setBuyer($buyer); + +// Add a product line +$line = new InvoiceLine(); +$line->setName('Product Name') + ->setPrice(100) + ->setVatRate(16) + ->setQuantity(1); +$inv->addLine($line); + +// Export invoice to a UBL document +header('Content-Type: text/xml'); +$writer = new UblWriter(); +echo $writer->export($inv); +``` + ## Roadmap These are the expected features for the library and how's it going so far: diff --git a/composer.json b/composer.json index 7e36479..d698634 100644 --- a/composer.json +++ b/composer.json @@ -29,12 +29,12 @@ }, "require": { "php": ">=7.1", - "josemmo/uxml": "^0.1.1" + "josemmo/uxml": "^0.1.2" }, "require-dev": { "ext-openssl": "*", - "phan/phan": "^4", + "phan/phan": "^5.0", "phpdocumentor/reflection": "^4.0", - "symfony/phpunit-bridge": "^5.2" + "symfony/phpunit-bridge": "^5.3" } } diff --git a/composer.lock b/composer.lock index 78fed34..c8809c1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7aac792b4c23a2b38656f2b6798fa17f", + "content-hash": "1cc14bd61534b65cbd3dd84fdc0c7465", "packages": [ { "name": "josemmo/uxml", - "version": "v0.1.1", + "version": "v0.1.2", "source": { "type": "git", "url": "https://github.com/josemmo/uxml.git", - "reference": "60b59961a3c7144f03c4d9971f04277dd9c0085d" + "reference": "be11039e103ad1a3d801ea67c03fdc30756f7992" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/josemmo/uxml/zipball/60b59961a3c7144f03c4d9971f04277dd9c0085d", - "reference": "60b59961a3c7144f03c4d9971f04277dd9c0085d", + "url": "https://api.github.com/repos/josemmo/uxml/zipball/be11039e103ad1a3d801ea67c03fdc30756f7992", + "reference": "be11039e103ad1a3d801ea67c03fdc30756f7992", "shasum": "" }, "require": { @@ -25,8 +25,8 @@ "php": ">=7.1" }, "require-dev": { - "phan/phan": "^4", - "symfony/phpunit-bridge": "^5.2" + "phan/phan": "^5.0", + "symfony/phpunit-bridge": "^5.3" }, "type": "library", "autoload": { @@ -52,9 +52,9 @@ ], "support": { "issues": "https://github.com/josemmo/uxml/issues", - "source": "https://github.com/josemmo/uxml/tree/v0.1.1" + "source": "https://github.com/josemmo/uxml/tree/v0.1.2" }, - "time": "2021-05-25T17:54:56+00:00" + "time": "2021-08-07T09:49:41+00:00" } ], "packages-dev": [ @@ -141,21 +141,21 @@ }, { "name": "composer/xdebug-handler", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496" + "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", - "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339", + "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpstan/phpstan": "^0.12.55", @@ -185,7 +185,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.1" + "source": "https://github.com/composer/xdebug-handler/tree/2.0.2" }, "funding": [ { @@ -201,24 +201,24 @@ "type": "tidelift" } ], - "time": "2021-05-05T19:37:51+00:00" + "time": "2021-07-31T17:03:58+00:00" }, { "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e" + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e", - "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", "shasum": "" }, "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "php": "^7.1 || ^8.0", "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" }, @@ -244,29 +244,29 @@ "description": "A more advanced JSONRPC implementation", "support": { "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", - "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.0" + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" }, - "time": "2021-01-10T17:48:47+00:00" + "time": "2021-06-11T22:34:44+00:00" }, { "name": "microsoft/tolerant-php-parser", - "version": "v0.0.23", + "version": "v0.1.1", "source": { "type": "git", "url": "https://github.com/microsoft/tolerant-php-parser.git", - "reference": "1d76657e3271754515ace52501d3e427eca42ad0" + "reference": "6a965617cf484355048ac6d2d3de7b6ec93abb16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/1d76657e3271754515ace52501d3e427eca42ad0", - "reference": "1d76657e3271754515ace52501d3e427eca42ad0", + "url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/6a965617cf484355048ac6d2d3de7b6ec93abb16", + "reference": "6a965617cf484355048ac6d2d3de7b6ec93abb16", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.2" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^8.5.15" }, "type": "library", "autoload": { @@ -289,22 +289,22 @@ "description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios", "support": { "issues": "https://github.com/microsoft/tolerant-php-parser/issues", - "source": "https://github.com/microsoft/tolerant-php-parser/tree/v0.0.23" + "source": "https://github.com/microsoft/tolerant-php-parser/tree/v0.1.1" }, - "time": "2020-09-13T17:29:12+00:00" + "time": "2021-07-16T21:28:12+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v2.1.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e" + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e0f1e33a71587aca81be5cffbb9746510e1fe04e", - "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", "shasum": "" }, "require": { @@ -312,10 +312,10 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "php": ">=5.6" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0", + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", "squizlabs/php_codesniffer": "~3.5" }, "type": "library", @@ -340,22 +340,22 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/master" + "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0" }, - "time": "2020-04-16T18:48:43+00:00" + "time": "2020-12-01T19:48:11+00:00" }, { "name": "nikic/php-parser", - "version": "v4.10.5", + "version": "v4.12.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" + "reference": "6608f01670c3cc5079e18c1dab1104e002579143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143", + "reference": "6608f01670c3cc5079e18c1dab1104e002579143", "shasum": "" }, "require": { @@ -396,22 +396,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.5" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0" }, - "time": "2021-05-03T19:11:20+00:00" + "time": "2021-07-21T10:44:31+00:00" }, { "name": "phan/phan", - "version": "4.0.6", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/phan/phan.git", - "reference": "4caaa97195dcea549021cb773a5dc30bb46d1fab" + "reference": "f36b6b9a2f4143a25f35ce94d712ceb0527e9d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phan/phan/zipball/4caaa97195dcea549021cb773a5dc30bb46d1fab", - "reference": "4caaa97195dcea549021cb773a5dc30bb46d1fab", + "url": "https://api.github.com/repos/phan/phan/zipball/f36b6b9a2f4143a25f35ce94d712ceb0527e9d90", + "reference": "f36b6b9a2f4143a25f35ce94d712ceb0527e9d90", "shasum": "" }, "require": { @@ -421,23 +421,25 @@ "ext-json": "*", "ext-tokenizer": "*", "felixfbecker/advanced-json-rpc": "^3.0.4", - "microsoft/tolerant-php-parser": "0.0.23", - "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0", + "microsoft/tolerant-php-parser": "^0.1.0", + "netresearch/jsonmapper": "^1.6.0|^2.0|^3.0|^4.0", "php": "^7.2.0|^8.0.0", "sabre/event": "^5.0.3", "symfony/console": "^3.2|^4.0|^5.0", "symfony/polyfill-mbstring": "^1.11.0", - "symfony/polyfill-php80": "^1.20.0" + "symfony/polyfill-php80": "^1.20.0", + "tysonandre/var_representation_polyfill": "^0.0.2" }, "require-dev": { "phpunit/phpunit": "^8.5.0" }, "suggest": { - "ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.10+ is recommended.", + "ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.14+ is recommended.", "ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", "ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable", "ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8", - "ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions." + "ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions.", + "ext-var_representation": "Suggested for converting values to strings in issue messages" }, "bin": [ "phan", @@ -473,9 +475,9 @@ ], "support": { "issues": "https://github.com/phan/phan/issues", - "source": "https://github.com/phan/phan/tree/4.0.6" + "source": "https://github.com/phan/phan/tree/5.0.0" }, - "time": "2021-05-19T23:27:16+00:00" + "time": "2021-08-01T18:17:28+00:00" }, { "name": "phpdocumentor/reflection", @@ -857,27 +859,29 @@ }, { "name": "symfony/console", - "version": "v5.2.8", + "version": "v5.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "864568fdc0208b3eba3638b6000b69d2386e6768" + "reference": "51b71afd6d2dc8f5063199357b9880cea8d8bfe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/864568fdc0208b3eba3638b6000b69d2386e6768", - "reference": "864568fdc0208b3eba3638b6000b69d2386e6768", + "url": "https://api.github.com/repos/symfony/console/zipball/51b71afd6d2dc8f5063199357b9880cea8d8bfe2", + "reference": "51b71afd6d2dc8f5063199357b9880cea8d8bfe2", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", + "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2", "symfony/string": "^5.1" }, "conflict": { + "psr/log": ">=3", "symfony/dependency-injection": "<4.4", "symfony/dotenv": "<5.1", "symfony/event-dispatcher": "<4.4", @@ -885,10 +889,10 @@ "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", + "psr/log": "^1|^2", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/event-dispatcher": "^4.4|^5.0", @@ -934,7 +938,74 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.8" + "source": "https://github.com/symfony/console/tree/v5.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-07-27T19:10:22+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" }, "funding": [ { @@ -950,30 +1021,30 @@ "type": "tidelift" } ], - "time": "2021-05-11T15:45:21+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v5.2.9", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "ea24e42c1ee04792f5d814da6f0814b20ece2907" + "reference": "bc368b765a651424b19f5759953ce2873e7d448b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/ea24e42c1ee04792f5d814da6f0814b20ece2907", - "reference": "ea24e42c1ee04792f5d814da6f0814b20ece2907", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/bc368b765a651424b19f5759953ce2873e7d448b", + "reference": "bc368b765a651424b19f5759953ce2873e7d448b", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=7.1.3", + "symfony/deprecation-contracts": "^2.1" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0|9.1.2" + "phpunit/phpunit": "<7.5|9.1.2" }, "require-dev": { - "symfony/deprecation-contracts": "^2.1", "symfony/error-handler": "^4.4|^5.0" }, "suggest": { @@ -1017,7 +1088,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v5.2.9" + "source": "https://github.com/symfony/phpunit-bridge/tree/v5.3.4" }, "funding": [ { @@ -1033,20 +1104,20 @@ "type": "tidelift" } ], - "time": "2021-05-16T13:07:46+00:00" + "time": "2021-07-15T21:37:44+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", "shasum": "" }, "require": { @@ -1058,7 +1129,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1096,7 +1167,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" }, "funding": [ { @@ -1112,20 +1183,20 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.22.1", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" + "reference": "16880ba9c5ebe3642d1995ab866db29270b36535" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", - "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535", + "reference": "16880ba9c5ebe3642d1995ab866db29270b36535", "shasum": "" }, "require": { @@ -1137,7 +1208,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1177,7 +1248,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1" }, "funding": [ { @@ -1193,20 +1264,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2021-05-27T12:26:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", "shasum": "" }, "require": { @@ -1218,7 +1289,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1261,7 +1332,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" }, "funding": [ { @@ -1277,20 +1348,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.22.1", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", + "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", "shasum": "" }, "require": { @@ -1302,7 +1373,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1341,7 +1412,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" }, "funding": [ { @@ -1357,20 +1428,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2021-05-27T12:26:48+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", + "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", "shasum": "" }, "require": { @@ -1379,7 +1450,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1420,7 +1491,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" }, "funding": [ { @@ -1436,20 +1507,20 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", "shasum": "" }, "require": { @@ -1458,7 +1529,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1503,7 +1574,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" }, "funding": [ { @@ -1519,7 +1590,7 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-07-28T13:41:28+00:00" }, { "name": "symfony/service-contracts", @@ -1602,16 +1673,16 @@ }, { "name": "symfony/string", - "version": "v5.2.8", + "version": "v5.3.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db" + "reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", - "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", + "url": "https://api.github.com/repos/symfony/string/zipball/bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1", + "reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1", "shasum": "" }, "require": { @@ -1665,7 +1736,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.8" + "source": "https://github.com/symfony/string/tree/v5.3.3" }, "funding": [ { @@ -1681,7 +1752,58 @@ "type": "tidelift" } ], - "time": "2021-05-10T14:56:10+00:00" + "time": "2021-06-27T11:44:38+00:00" + }, + { + "name": "tysonandre/var_representation_polyfill", + "version": "0.0.2", + "source": { + "type": "git", + "url": "https://github.com/TysonAndre/var_representation_polyfill.git", + "reference": "3f17999ee1f257319ddc6721dd26ebbc5d175f33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TysonAndre/var_representation_polyfill/zipball/3f17999ee1f257319ddc6721dd26ebbc5d175f33", + "reference": "3f17999ee1f257319ddc6721dd26ebbc5d175f33", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.2.0|^8.0.0" + }, + "require-dev": { + "phan/phan": "^4.0", + "phpunit/phpunit": "^8.5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "VarRepresentation\\": "src/VarRepresentation" + }, + "files": [ + "src/var_representation.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tyson Andre" + } + ], + "description": "Polyfill for var_representation", + "keywords": [ + "var_export", + "var_representation" + ], + "support": { + "issues": "https://github.com/TysonAndre/var_representation_polyfill/issues", + "source": "https://github.com/TysonAndre/var_representation_polyfill/tree/0.0.2" + }, + "time": "2021-06-26T18:55:02+00:00" }, { "name": "webmozart/assert", diff --git a/docs/favicon.png b/docs/favicon.png new file mode 100644 index 0000000..6028c76 Binary files /dev/null and b/docs/favicon.png differ diff --git a/docs/getting-started/creating-custom-presets.md b/docs/getting-started/creating-custom-presets.md index 9663598..1cfc622 100644 --- a/docs/getting-started/creating-custom-presets.md +++ b/docs/getting-started/creating-custom-presets.md @@ -91,7 +91,7 @@ class CustomPreset extends AbstractPreset { public function setupInvoice(Invoice $invoice) { $invoice->setRoundingMatrix([ "line/netAmount" => 4, - null => 2 + "" => 2 ]); } } diff --git a/docs/logo-white.svg b/docs/logo-white.svg new file mode 100644 index 0000000..2f504a7 --- /dev/null +++ b/docs/logo-white.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000..cb20b35 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 28273cb..6e0835e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ # Project configuration -site_name: eInvoicing +site_name: European Invoicing (eInvoicing) site_url: https://josemmo.github.io/einvoicing/ repo_name: josemmo/einvoicing repo_url: https://github.com/josemmo/einvoicing @@ -8,11 +8,11 @@ edit_uri: "" # Theme customization theme: name: material - icon: - repo: fontawesome/brands/github + logo: logo-white.svg + favicon: favicon.png palette: scheme: slate - primary: light blue + primary: blue accent: yellow features: - navigation.tabs diff --git a/src/Invoice.php b/src/Invoice.php index 2777517..101d614 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -31,6 +31,8 @@ class Invoice { protected $taxPointDate = null; protected $note = null; protected $buyerReference = null; + protected $purchaseOrderReference = null; + protected $salesOrderReference = null; protected $paidAmount = 0; protected $roundingAmount = 0; protected $seller = null; @@ -72,7 +74,7 @@ public function __construct(?string $preset=null) { * @return int Number of decimal places */ public function getDecimals(string $field): int { - return $this->roundingMatrix[$field] ?? $this->roundingMatrix[null] ?? self::DEFAULT_DECIMALS; + return $this->roundingMatrix[$field] ?? $this->roundingMatrix[''] ?? self::DEFAULT_DECIMALS; } @@ -287,6 +289,46 @@ public function setBuyerReference(?string $buyerReference): self { } + /** + * Get purchase order reference + * @return string|null Purchase order reference + */ + public function getPurchaseOrderReference(): ?string { + return $this->purchaseOrderReference; + } + + + /** + * Set purchase order reference + * @param string|null $purchaseOrderReference Purchase order reference + * @return self Invoice instance + */ + public function setPurchaseOrderReference(?string $purchaseOrderReference): self { + $this->purchaseOrderReference = $purchaseOrderReference; + return $this; + } + + + /** + * Get sales order reference + * @return string|null Sales order reference + */ + public function getSalesOrderReference(): ?string { + return $this->salesOrderReference; + } + + + /** + * Set sales order reference + * @param string|null $salesOrderReference Sales order reference + * @return self Invoice instance + */ + public function setSalesOrderReference(?string $salesOrderReference): self { + $this->salesOrderReference = $salesOrderReference; + return $this; + } + + /** * Get invoice prepaid amount * NOTE: may be rounded according to the CIUS specification diff --git a/src/InvoiceLine.php b/src/InvoiceLine.php index f6a9d3a..fe3bf55 100644 --- a/src/InvoiceLine.php +++ b/src/InvoiceLine.php @@ -192,19 +192,19 @@ public function setSellerIdentifier(?string $identifier): self { /** * Get quantity - * @return int|float Quantity + * @return float Quantity */ - public function getQuantity() { + public function getQuantity(): float { return $this->quantity; } /** * Set quantity - * @param int|float $quantity Quantity - * @return self Invoice line instance + * @param float $quantity Quantity + * @return self Invoice line instance */ - public function setQuantity($quantity): self { + public function setQuantity(float $quantity): self { $this->quantity = $quantity; return $this; } @@ -241,11 +241,11 @@ public function getPrice(): ?float { /** * Set price - * @param float $price Price - * @param int|float|null $baseQuantity Base quantity - * @return self Invoice line instance + * @param float $price Price + * @param float|null $baseQuantity Base quantity + * @return self Invoice line instance */ - public function setPrice(float $price, $baseQuantity=null): self { + public function setPrice(float $price, ?float $baseQuantity=null): self { $this->price = $price; if ($baseQuantity !== null) { $this->setBaseQuantity($baseQuantity); @@ -256,19 +256,19 @@ public function setPrice(float $price, $baseQuantity=null): self { /** * Get base quantity - * @return int|float Base quantity + * @return float Base quantity */ - public function getBaseQuantity() { + public function getBaseQuantity(): float { return $this->baseQuantity; } /** * Set base quantity - * @param int|float $baseQuantity Base quantity - * @return self Invoice line instance + * @param float $baseQuantity Base quantity + * @return self Invoice line instance */ - public function setBaseQuantity($baseQuantity): self { + public function setBaseQuantity(float $baseQuantity): self { $this->baseQuantity = $baseQuantity; return $this; } diff --git a/src/Models/InvoiceTotals.php b/src/Models/InvoiceTotals.php index fd0aef8..05992f2 100644 --- a/src/Models/InvoiceTotals.php +++ b/src/Models/InvoiceTotals.php @@ -2,6 +2,7 @@ namespace Einvoicing\Models; use Einvoicing\Invoice; +use Einvoicing\Traits\VatTrait; use function array_values; use function round; @@ -88,7 +89,7 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { foreach ($inv->getLines() as $line) { $lineNetAmount = $line->getNetAmount($inv->getDecimals('line/netAmount')) ?? 0; $totals->netAmount += $lineNetAmount; - self::updateVatMap($vatMap, $line->getVatCategory(), $line->getVatRate(), $lineNetAmount); + self::updateVatMap($vatMap, $line, $lineNetAmount); } // Apply allowance and charge totals @@ -96,12 +97,12 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { foreach ($inv->getAllowances() as $item) { $allowanceAmount = $item->getEffectiveAmount($totals->netAmount, $allowancesChargesDecimals); $totals->allowancesAmount += $allowanceAmount; - self::updateVatMap($vatMap, $item->getVatCategory(), $item->getVatRate(), -$allowanceAmount); + self::updateVatMap($vatMap, $item, -$allowanceAmount); } foreach ($inv->getCharges() as $item) { $chargeAmount = $item->getEffectiveAmount($totals->netAmount, $allowancesChargesDecimals); $totals->chargesAmount += $chargeAmount; - self::updateVatMap($vatMap, $item->getVatCategory(), $item->getVatRate(), $chargeAmount); + self::updateVatMap($vatMap, $item, $chargeAmount); } // Calculate VAT amounts @@ -127,18 +128,34 @@ static public function fromInvoice(Invoice $inv): InvoiceTotals { /** * Update VAT map - * @param array &$vatMap VAT map reference - * @param string $category VAT category - * @param int|null $rate VAT rate - * @param float $addTaxableAmount Taxable amount to add + * @param VatBreakdown[string] &$vatMap VAT map reference + * @param VatTrait $item Item instance + * @param float|null $rate VAT rate + * @param float $addTaxableAmount Taxable amount to add */ - static private function updateVatMap(array &$vatMap, string $category, ?int $rate, float $addTaxableAmount) { + static private function updateVatMap(array &$vatMap, $item, float $addTaxableAmount) { + $category = $item->getVatCategory(); + $rate = $item->getVatRate(); $key = "$category:$rate"; + + // Initialize VAT breakdown if (!isset($vatMap[$key])) { $vatMap[$key] = new VatBreakdown(); $vatMap[$key]->category = $category; $vatMap[$key]->rate = $rate; } + + // Update exemption reason (last item overwrites previous ones) + $exemptionReasonCode = $item->getVatExemptionReasonCode(); + $exemptionReason = $item->getVatExemptionReason(); + if ($exemptionReasonCode !== null) { + $vatMap[$key]->exemptionReasonCode = $exemptionReasonCode; + } + if ($exemptionReason !== null) { + $vatMap[$key]->exemptionReason = $exemptionReason; + } + + // Increase taxable amount $vatMap[$key]->taxableAmount += $addTaxableAmount; } } diff --git a/src/Models/VatBreakdown.php b/src/Models/VatBreakdown.php index 61ed98f..26e85a4 100644 --- a/src/Models/VatBreakdown.php +++ b/src/Models/VatBreakdown.php @@ -10,10 +10,22 @@ class VatBreakdown { /** * VAT rate as a percentage - * @var int|null + * @var float|null */ public $rate; + /** + * VAT exemption reason code + * @var string|null + */ + public $exemptionReasonCode = null; + + /** + * VAT exemption reason as text + * @var string|null + */ + public $exemptionReason = null; + /** * Sum of all taxable amounts * @var float diff --git a/src/Presets/AbstractPreset.php b/src/Presets/AbstractPreset.php index 18a584b..e391499 100644 --- a/src/Presets/AbstractPreset.php +++ b/src/Presets/AbstractPreset.php @@ -25,6 +25,6 @@ public function getRules(): array { * @param Invoice $invoice Invoice instance */ public function setupInvoice(Invoice $invoice) { - $invoice->setRoundingMatrix([null => 2]); + $invoice->setRoundingMatrix(['' => 2]); } } diff --git a/src/Presets/Peppol.php b/src/Presets/Peppol.php index 39de6c7..ac0c29a 100644 --- a/src/Presets/Peppol.php +++ b/src/Presets/Peppol.php @@ -3,6 +3,8 @@ use Einvoicing\Invoice; +// @phan-file-suppress PhanPossiblyNonClassMethodCall + /** * PEPPOL BIS Billing 3.0 * @author OpenPEPPOL @@ -23,6 +25,11 @@ public function getSpecification(): string { public function getRules(): array { $res = []; + $res['PEPPOL-EN16931-R003'] = static function(Invoice $inv) { + if ($inv->getBuyerReference() !== null) return; + if ($inv->getPurchaseOrderReference() !== null) return; + return "A buyer reference or purchase order reference MUST be provided."; + }; $res['PEPPOL-EN16931-R061'] = static function(Invoice $inv) { if ($inv->getPayment() === null) return; if ($inv->getPayment()->getMandate() === null) return; diff --git a/src/Readers/UblReader.php b/src/Readers/UblReader.php index 19e562b..d0f69d4 100644 --- a/src/Readers/UblReader.php +++ b/src/Readers/UblReader.php @@ -13,6 +13,7 @@ use Einvoicing\Payments\Mandate; use Einvoicing\Payments\Payment; use Einvoicing\Payments\Transfer; +use Einvoicing\Traits\VatTrait; use Einvoicing\Writers\UblWriter; use InvalidArgumentException; use UXML\UXML; @@ -32,7 +33,7 @@ public function import(string $document): Invoice { $cac = UblWriter::NS_CAC; $cbc = UblWriter::NS_CBC; - // BT-24: Specification indentifier + // BT-24: Specification identifier $specificationNode = $xml->get("{{$cbc}}CustomizationID"); if ($specificationNode !== null) { $specification = $specificationNode->asText(); @@ -45,6 +46,26 @@ public function import(string $document): Invoice { } } + // Index tax exemption reasons + /** @var array */ + $taxExemptions = []; + foreach ($xml->getAll("{{$cac}}TaxTotal/{{$cac}}TaxSubtotal/{{$cac}}TaxCategory") as $node) { + $exemptionReasonCodeNode = $node->get("{{$cbc}}TaxExemptionReasonCode"); + $exemptionReasonNode = $node->get("{{$cbc}}TaxExemptionReason"); + if ($exemptionReasonCodeNode === null && $exemptionReasonNode === null) continue; + + // Get tax subtotal key + $category = $node->get("{{$cbc}}ID")->asText(); // @phan-suppress-current-line PhanPossiblyNonClassMethodCall + $rate = (float) $node->get("{{$cbc}}Percent")->asText(); // @phan-suppress-current-line PhanPossiblyNonClassMethodCall + $key = "$category:$rate"; + + // Save reasons + $taxExemptions[$key] = [ + "code" => ($exemptionReasonCodeNode === null) ? null : $exemptionReasonCodeNode->asText(), + "reason" => ($exemptionReasonNode === null) ? null : $exemptionReasonNode->asText(), + ]; + } + // BT-23: Business process type $businessProcessNode = $xml->get("{{$cbc}}ProfileID"); if ($businessProcessNode !== null) { @@ -108,6 +129,18 @@ public function import(string $document): Invoice { // BG-14: Invoice period $this->parsePeriodFields($xml, $invoice); + // BT-13: Purchase order reference + $purchaseOrderReferenceNode = $xml->get("{{$cac}}OrderReference/{{$cbc}}ID"); + if ($purchaseOrderReferenceNode !== null) { + $invoice->setPurchaseOrderReference($purchaseOrderReferenceNode->asText()); + } + + // BT-14: Sales order reference + $salesOrderReferenceNode = $xml->get("{{$cac}}OrderReference/{{$cbc}}SalesOrderID"); + if ($salesOrderReferenceNode !== null) { + $invoice->setSalesOrderReference($salesOrderReferenceNode->asText()); + } + // Seller node $sellerNode = $xml->get("{{$cac}}AccountingSupplierParty/{{$cac}}Party"); if ($sellerNode !== null) { @@ -138,12 +171,24 @@ public function import(string $document): Invoice { // Allowances and charges foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $node) { - $this->addAllowanceOrCharge($invoice, $node); + $this->addAllowanceOrCharge($invoice, $node, $taxExemptions); + } + + // BT-113: Paid amount + $paidAmountNode = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}PrepaidAmount"); + if ($paidAmountNode !== null) { + $invoice->setPaidAmount((float) $paidAmountNode->asText()); + } + + // BT-114: Rounding amount + $roundingAmountNode = $xml->get("{{$cac}}LegalMonetaryTotal/{{$cbc}}PayableRoundingAmount"); + if ($roundingAmountNode !== null) { + $invoice->setRoundingAmount((float) $roundingAmountNode->asText()); } // Invoice lines foreach ($xml->getAll("{{$cac}}InvoiceLine") as $node) { - $invoice->addLine($this->parseInvoiceLine($node)); + $invoice->addLine($this->parseInvoiceLine($node, $taxExemptions)); } return $invoice; @@ -525,10 +570,11 @@ private function parsePaymentMandateNode(UXML $xml): Mandate { /** * Set VAT attributes - * @param AllowanceOrCharge|InvoiceLine $target Target instance - * @param UXML $xml XML node + * @param VatTrait $target Target instance + * @param UXML $xml XML node + * @param array &$taxExemptions Tax exemption reasons */ - private function setVatAttributes($target, UXML $xml) { + private function setVatAttributes($target, UXML $xml, array $taxExemptions) { $cbc = UblWriter::NS_CBC; // Tax category @@ -542,21 +588,27 @@ private function setVatAttributes($target, UXML $xml) { if ($taxRateNode !== null) { $target->setVatRate((float) $taxRateNode->asText()); } + + // Tax exemption reasons + $key = "{$target->getVatCategory()}:{$target->getVatRate()}"; + $target->setVatExemptionReasonCode($taxExemptions[$key]['code'] ?? null); + $target->setVatExemptionReason($taxExemptions[$key]['reason'] ?? null); } /** * Add allowance or charge - * @param Invoice|InvoiceLine $target Target instance - * @param UXML $xml XML node + * @param Invoice|InvoiceLine $target Target instance + * @param UXML $xml XML node + * @param array &$taxExemptions Tax exemption reasons */ - private function addAllowanceOrCharge($target, UXML $xml) { + private function addAllowanceOrCharge($target, UXML $xml, array &$taxExemptions) { $allowanceOrCharge = new AllowanceOrCharge(); $cac = UblWriter::NS_CAC; $cbc = UblWriter::NS_CBC; // Add instance to invoice - if ($xml->get("{{$cbc}}ChargeIndicator")->asText() === "true") { + if ($xml->get("{{$cbc}}ChargeIndicator")->asText() === "true") { // @phan-suppress-current-line PhanPossiblyNonClassMethodCall $target->addCharge($allowanceOrCharge); } else { $target->addAllowance($allowanceOrCharge); @@ -577,7 +629,7 @@ private function addAllowanceOrCharge($target, UXML $xml) { // Amount $factorNode = $xml->get("{{$cbc}}MultiplierFactorNumeric"); if ($factorNode === null) { - $amount = (float) $xml->get("{{$cbc}}Amount")->asText(); + $amount = (float) $xml->get("{{$cbc}}Amount")->asText(); // @phan-suppress-current-line PhanPossiblyNonClassMethodCall $allowanceOrCharge->setAmount($amount); } else { $percent = (float) $factorNode->asText(); @@ -587,17 +639,18 @@ private function addAllowanceOrCharge($target, UXML $xml) { // VAT attributes $vatNode = $xml->get("{{$cac}}TaxCategory"); if ($vatNode !== null) { - $this->setVatAttributes($allowanceOrCharge, $vatNode); + $this->setVatAttributes($allowanceOrCharge, $vatNode, $taxExemptions); } } /** * Parse invoice line - * @param UXML $xml XML node - * @return InvoiceLine Invoice line instance + * @param UXML $xml XML node + * @param array &$taxExemptions Tax exemption reasons + * @return InvoiceLine Invoice line instance */ - private function parseInvoiceLine(UXML $xml): InvoiceLine { + private function parseInvoiceLine(UXML $xml, array &$taxExemptions): InvoiceLine { $line = new InvoiceLine(); $cac = UblWriter::NS_CAC; $cbc = UblWriter::NS_CBC; @@ -632,7 +685,7 @@ private function parseInvoiceLine(UXML $xml): InvoiceLine { // Allowances and charges foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $node) { - $this->addAllowanceOrCharge($line, $node); + $this->addAllowanceOrCharge($line, $node, $taxExemptions); } // BT-154: Item description @@ -692,7 +745,7 @@ private function parseInvoiceLine(UXML $xml): InvoiceLine { // VAT attributes $vatNode = $xml->get("{{$cac}}Item/{{$cac}}ClassifiedTaxCategory"); if ($vatNode !== null) { - $this->setVatAttributes($line, $vatNode); + $this->setVatAttributes($line, $vatNode, $taxExemptions); } // BG-32: Item attributes diff --git a/src/Traits/InvoiceValidationTrait.php b/src/Traits/InvoiceValidationTrait.php index 25bc50c..d50a4b3 100644 --- a/src/Traits/InvoiceValidationTrait.php +++ b/src/Traits/InvoiceValidationTrait.php @@ -6,6 +6,8 @@ use function array_merge; use function in_array; +// @phan-file-suppress PhanPossiblyNonClassMethodCall + trait InvoiceValidationTrait { /** * Validate invoice @@ -97,11 +99,6 @@ private function getDefaultRules(): array { if ($line->getPrice() < 0) return "The Item net price (BT-146) shall NOT be negative"; } }; - $res['BR-28'] = static function(Invoice $inv) { - foreach ($inv->getLines() as $line) { - if ($line->getNetAmount() < 0) return "The Item gross price (BT-148) shall NOT be negative"; - } - }; $res['BR-31'] = static function(Invoice $inv) { foreach ($inv->getAllowances() as $allowance) { if ($allowance->getAmount() === null) { diff --git a/src/Traits/VatTrait.php b/src/Traits/VatTrait.php index 36e2754..8e39969 100644 --- a/src/Traits/VatTrait.php +++ b/src/Traits/VatTrait.php @@ -4,6 +4,8 @@ trait VatTrait { protected $vatCategory = "S"; // TODO: add constants protected $vatRate = null; + protected $vatExemptionReasonCode = null; + protected $vatExemptionReason = null; /** * Get VAT category code @@ -43,4 +45,44 @@ public function setVatRate(?float $rate): self { $this->vatRate = $rate; return $this; } + + + /** + * Get VAT exemption reason code + * @return string|null VAT exemption reason code + */ + public function getVatExemptionReasonCode(): ?string { + return $this->vatExemptionReasonCode; + } + + + /** + * Set VAT exemption reason code + * @param string|null $reasonCode VAT exemption reason code + * @return self This instance + */ + public function setVatExemptionReasonCode(?string $reasonCode): self { + $this->vatExemptionReasonCode = $reasonCode; + return $this; + } + + + /** + * Get VAT exemption reason + * @return string|null VAT exemption reason expressed as text + */ + public function getVatExemptionReason(): ?string { + return $this->vatExemptionReason; + } + + + /** + * Set VAT exemption reason + * @param string|null $reason VAT exemption reason expressed as text + * @return self This instance + */ + public function setVatExemptionReason(?string $reason): self { + $this->vatExemptionReason = $reason; + return $this; + } } diff --git a/src/Writers/UblWriter.php b/src/Writers/UblWriter.php index 458f5b2..b026e5b 100644 --- a/src/Writers/UblWriter.php +++ b/src/Writers/UblWriter.php @@ -29,7 +29,7 @@ public function export(Invoice $invoice): string { 'xmlns:cbc' => self::NS_CBC ]); - // BT-24: Specification indentifier + // BT-24: Specification identifier $specificationIdentifier = $invoice->getSpecification(); if ($specificationIdentifier !== null) { $xml->add('cbc:CustomizationID', $specificationIdentifier); @@ -92,6 +92,9 @@ public function export(Invoice $invoice): string { // BG-14: Invoice period $this->addPeriodNode($xml, $invoice); + // Order reference node + $this->addOrderReferenceNode($xml, $invoice); + // Seller node $seller = $invoice->getSeller(); if ($seller !== null) { @@ -138,7 +141,7 @@ public function export(Invoice $invoice): string { // Invoice lines $lines = $invoice->getLines(); foreach ($lines as $i=>$line) { - $this->addLineNode($xml, $line, $i+1, $invoice); + $this->addLineNode($xml, $line, $i+1, $invoice); // @phan-suppress-current-line PhanPartialTypeMismatchArgument } return $xml->asXML(); @@ -177,12 +180,36 @@ private function addPeriodNode(UXML $parent, $source) { } // Period end date - if ($startDate !== null) { + if ($endDate !== null) { $xml->add('cbc:EndDate', $endDate->format('Y-m-d')); } } + /** + * Add order reference node + * @param UXML $parent Parent element + * @param Invoice $invoice Invoice instance + */ + private function addOrderReferenceNode(UXML $parent, Invoice $invoice) { + $purchaseOrderReference = $invoice->getPurchaseOrderReference(); + $salesOrderReference = $invoice->getSalesOrderReference(); + if ($purchaseOrderReference === null && $salesOrderReference === null) return; + + $orderReferenceNode = $parent->add('cac:OrderReference'); + + // BT-13: Purchase order reference + if ($purchaseOrderReference !== null) { + $orderReferenceNode->add('cbc:ID', $purchaseOrderReference); + } + + // BT-14: Sales order reference + if ($salesOrderReference !== null) { + $orderReferenceNode->add('cbc:SalesOrderID', $salesOrderReference); + } + } + + /** * Add amount node * @param UXML $parent Parent element @@ -197,12 +224,17 @@ private function addAmountNode(UXML $parent, string $name, float $amount, string /** * Add VAT node - * @param UXML $parent Parent element - * @param string $name New node name - * @param string $category VAT category - * @param int|null $rate VAT rate + * @param UXML $parent Parent element + * @param string $name New node name + * @param string $category VAT category + * @param float|null $rate VAT rate + * @param string|null $exemptionReasonCode VAT exemption reason code + * @param string|null $exemptionReason VAT exemption reason as text */ - private function addVatNode(UXML $parent, string $name, string $category, ?int $rate) { + private function addVatNode( + UXML $parent, string $name, string $category, ?float $rate, + ?string $exemptionReasonCode=null, ?string $exemptionReason=null + ) { $xml = $parent->add($name); // VAT category @@ -213,6 +245,16 @@ private function addVatNode(UXML $parent, string $name, string $category, ?int $ $xml->add('cbc:Percent', (string) $rate); } + // Exemption reason code + if ($exemptionReasonCode !== null) { + $xml->add('cbc:TaxExemptionReasonCode', $exemptionReasonCode); + } + + // Exemption reason (as text) + if ($exemptionReason !== null) { + $xml->add('cbc:TaxExemptionReason', $exemptionReason); + } + // Tax scheme $xml->add('cac:TaxScheme')->add('cbc:ID', 'VAT'); } @@ -589,7 +631,7 @@ private function addAllowanceOrCharge( // Amount $baseAmount = $atDocumentLevel ? $invoice->getTotals()->netAmount : - $line->getNetAmount($invoice->getDecimals('line/netAmount')) ?? 0; + $line->getNetAmount($invoice->getDecimals('line/netAmount')) ?? 0; // @phan-suppress-current-line PhanPossiblyNonClassMethodCall $amount = $item->getEffectiveAmount($baseAmount, $invoice->getDecimals('line/allowanceChargeAmount')); $this->addAmountNode($xml, 'cbc:Amount', $amount, $invoice->getCurrency()); @@ -621,7 +663,8 @@ private function addTaxTotalNode(UXML $parent, InvoiceTotals $totals) { $vatBreakdownNode = $xml->add('cac:TaxSubtotal'); $this->addAmountNode($vatBreakdownNode, 'cbc:TaxableAmount', $item->taxableAmount, $totals->currency); $this->addAmountNode($vatBreakdownNode, 'cbc:TaxAmount', $item->taxAmount, $totals->currency); - $this->addVatNode($vatBreakdownNode, 'cac:TaxCategory', $item->category, $item->rate); + $this->addVatNode($vatBreakdownNode, 'cac:TaxCategory', $item->category, $item->rate, + $item->exemptionReasonCode, $item->exemptionReason); } } @@ -633,7 +676,7 @@ private function addTaxTotalNode(UXML $parent, InvoiceTotals $totals) { */ private function addDocumentTotalsNode(UXML $parent, InvoiceTotals $totals) { $xml = $parent->add('cac:LegalMonetaryTotal'); - + // Build totals matrix $totalsMatrix = [ "cbc:LineExtensionAmount" => $totals->netAmount, diff --git a/tests/Integration/IntegrationTest.php b/tests/Integration/IntegrationTest.php index 6124032..331b8e6 100644 --- a/tests/Integration/IntegrationTest.php +++ b/tests/Integration/IntegrationTest.php @@ -45,4 +45,8 @@ public function testCanRecreatePeppolBaseExample(): void { public function testCanRecreatePeppolVatExample(): void { $this->importAndExportInvoice(__DIR__ . "/peppol-vat-s.xml"); } + + public function testCanRecreatePeppolAllowanceExample(): void { + $this->importAndExportInvoice(__DIR__ . "/peppol-allowance.xml"); + } } diff --git a/tests/Integration/peppol-allowance.xml b/tests/Integration/peppol-allowance.xml new file mode 100644 index 0000000..2fe6b63 --- /dev/null +++ b/tests/Integration/peppol-allowance.xml @@ -0,0 +1,323 @@ + + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + Snippet1 + 2017-11-13 + 2017-12-01 + 380 + Please note we have a new phone number: 22 22 22 22 + 2017-12-01 + EUR + 4025:123:4343 + 0150abc + + 2017-12-01 + 2017-12-31 + + + + 7300010000001 + + 99887766 + + + SupplierTradingName Ltd. + + + Main street 1 + Postbox 123 + London + GB 123 EW + + GB + + + + GB1232434 + + VAT + + + + SupplierOfficialName Ltd + GB983294 + + + + + + 4598375937 + + 4598375937 + + + BuyerTradingName AS + + + Hovedgatan 32 + Po box 878 + Stockholm + 456 34 + Södermalm + + SE + + + + SE4598375937 + + VAT + + + + Buyer Official Name + 39937423947 + + + Lisa Johnson + 23434234 + lj@buyer.se + + + + + 2017-11-01 + + 7300010000001 + + Delivery street 2 + Building 56 + Stockholm + 21234 + Södermalm + + Gate 15 + + + SE + + + + + + Delivery party Name + + + + + 30 + Snippet1 + + IBAN32423940 + AccountName + + BIC324098 + + + + + Payment within 10 days, 2% discount + + + false + 95 + Discount + 200 + + S + 25 + + VAT + + + + + true + CG + Cleaning + 20 + 1189.8 + 5949 + + S + 25 + + VAT + + + + + 1484.7 + + 5938.8 + 1484.7 + + S + 25 + + VAT + + + + + 1000 + 0 + + E + 0 + Reason for tax exempt + + VAT + + + + + + 5949 + 6938.8 + 8423.5 + 200 + 1189.8 + 1000 + 7423.5 + + + 1 + Testing note on line level + 10 + 4040 + Konteringsstreng + + false + 95 + Discount + 101 + + + true + CG + Cleaning + 1 + 40.4 + 4040 + + + Description of item + item name + + 97iugug876 + + + NO + + + 09348023 + + + S + 25 + + VAT + + + + + 410 + + + + 2 + Testing note on line level + 10 + 1000 + Konteringsstreng + + 2017-12-01 + 2017-12-05 + + + 124 + + + Description of item + item name + + 97iugug876 + + + 86776 + + + E + 0 + + VAT + + + + AdditionalItemName + AdditionalItemValue + + + + 200 + 2 + + + + 3 + Testing note on line level + 10 + 909 + Konteringsstreng + + 2017-12-01 + 2017-12-05 + + + 124 + + + false + 95 + Discount + 101 + + + true + CG + Charge + 1 + 9.09 + 909 + + + Description of item + item name + + 97iugug876 + + + 86776 + + + S + 25 + + VAT + + + + AdditionalItemName + AdditionalItemValue + + + + 100 + + + diff --git a/tests/Integration/peppol-base.xml b/tests/Integration/peppol-base.xml index 3a3f7ad..dd1e96b 100644 --- a/tests/Integration/peppol-base.xml +++ b/tests/Integration/peppol-base.xml @@ -12,6 +12,9 @@ EUR 4025:123:4343 0150abc + + 854777 + 9482348239847239874 diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php index 92bd17f..b01c856 100644 --- a/tests/InvoiceTest.php +++ b/tests/InvoiceTest.php @@ -17,7 +17,7 @@ final class InvoiceTest extends TestCase { private $line; protected function setUp(): void { - $this->invoice = (new Invoice)->setRoundingMatrix([null => 2]); + $this->invoice = (new Invoice)->setRoundingMatrix(['' => 2]); $this->line = new InvoiceLine(); } @@ -56,7 +56,7 @@ public function testDecimalMatrixIsUsed(): void { $this->invoice->setRoundingMatrix([ "invoice/paidAmount" => 4, "line/netAmount" => 8, - null => 3 + "" => 3 ])->setPaidAmount(123.456789) ->setRoundingAmount(987.654321) ->addLine((new InvoiceLine)->setPrice(12.121212121)) diff --git a/tests/Models/InvoiceTotalsTest.php b/tests/Models/InvoiceTotalsTest.php new file mode 100644 index 0000000..7f6b551 --- /dev/null +++ b/tests/Models/InvoiceTotalsTest.php @@ -0,0 +1,63 @@ +invoice = new Invoice(); + } + + public function testClassConstructors(): void { + $line = (new InvoiceLine()) + ->setName('Test Line') + ->setPrice(100); + $this->invoice->addLine($line); + + $totalsA = InvoiceTotals::fromInvoice($this->invoice); + $totalsB = $this->invoice->getTotals(); + $this->assertInstanceOf(InvoiceTotals::class, $totalsA); + $this->assertInstanceOf(InvoiceTotals::class, $totalsB); + $this->assertEquals(100, $totalsA->payableAmount); + $this->assertEquals(100, $totalsB->payableAmount); + } + + public function testVatExemptionReasons(): void { + $firstLine = (new InvoiceLine()) + ->setName('Line #1') + ->setVatCategory('E') + ->setVatExemptionReasonCode('VATEX-EU-O') + ->setVatExemptionReason('Not subject to VAT'); + $secondLine = (new InvoiceLine()) + ->setName('Line #2') + ->setVatCategory('E') + ->setVatRate(0) + ->setVatExemptionReasonCode('VATEX-EU-132-1P'); + $thirdLine = (clone $firstLine) + ->setName('Line #3'); + $allowance = (new AllowanceOrCharge()) + ->setReason('Allowance') + ->setAmount(100) + ->setVatCategory('E') + ->setVatExemptionReason('Another reason expressed as text'); + + $this->invoice + ->addLine($firstLine) + ->addLine($secondLine) + ->addLine($thirdLine) + ->addAllowance($allowance); + $totals = $this->invoice->getTotals(); + + $this->assertEquals('VATEX-EU-O', $totals->vatBreakdown[0]->exemptionReasonCode); + $this->assertEquals('Another reason expressed as text', $totals->vatBreakdown[0]->exemptionReason); + $this->assertEquals(null, $totals->vatBreakdown[1]->exemptionReason); + $this->assertEquals('VATEX-EU-132-1P', $totals->vatBreakdown[1]->exemptionReasonCode); + } +} diff --git a/tests/Readers/UblReaderTest.php b/tests/Readers/UblReaderTest.php index 0c001db..b2135e8 100644 --- a/tests/Readers/UblReaderTest.php +++ b/tests/Readers/UblReaderTest.php @@ -17,6 +17,7 @@ protected function setUp(): void { public function testCanReadInvoice(): void { $invoice = $this->reader->import(file_get_contents(self::DOCUMENT_PATH)); + $invoice->validate(); $totals = $invoice->getTotals(); $this->assertEquals(1300, $totals->netAmount); $this->assertEquals(1325, $totals->taxExclusiveAmount); diff --git a/tests/Traits/VatTraitTest.php b/tests/Traits/VatTraitTest.php index 1a841f2..3cddfd0 100644 --- a/tests/Traits/VatTraitTest.php +++ b/tests/Traits/VatTraitTest.php @@ -18,4 +18,16 @@ public function testCanReadAndWriteRate(): void { $this->line->setVatRate(0); $this->assertEquals(0, $this->line->getVatRate()); } + + public function testCanReadAndWriteExemptions(): void { + $category = "E"; + $reason = "Supply of transport services for sick or injured persons"; + $reasonCode = "VATEX-EU-132-1P"; + $this->line->setVatCategory($category); + $this->assertEquals($category, $this->line->getVatCategory()); + $this->line->setVatExemptionReason($reason); + $this->assertEquals($reason, $this->line->getVatExemptionReason()); + $this->line->setVatExemptionReasonCode($reasonCode); + $this->assertEquals($reasonCode, $this->line->getVatExemptionReasonCode()); + } }