├── src ├── DataType │ ├── Reference │ │ ├── ContractReference.php │ │ ├── ProjectReference.php │ │ ├── SalesOrderReference.php │ │ ├── TenderOrLotReference.php │ │ ├── DespatchAdviceReference.php │ │ ├── PurchaseOrderReference.php │ │ ├── PrecedingInvoiceReference.php │ │ ├── ReceivingAdviceReference.php │ │ ├── PurchaseOrderLineReference.php │ │ ├── SupportingDocumentReference.php │ │ └── DocumentReference.php │ ├── Identifier │ │ ├── InvoiceIdentifier.php │ │ ├── BuyerItemIdentifier.php │ │ ├── InvoiceLineIdentifier.php │ │ ├── DebitedAccountIdentifier.php │ │ ├── MandateReferenceIdentifier.php │ │ ├── SellerItemIdentifier.php │ │ ├── BankAssignedCreditorIdentifier.php │ │ ├── PaymentAccountIdentifier.php │ │ ├── TaxRegistrationIdentifier.php │ │ ├── PaymentServiceProviderIdentifier.php │ │ ├── ElectronicAddressIdentifier.php │ │ ├── BuyerIdentifier.php │ │ ├── PayeeIdentifier.php │ │ ├── SellerIdentifier.php │ │ ├── LocationIdentifier.php │ │ ├── StandardItemIdentifier.php │ │ ├── ObjectIdentifier.php │ │ ├── LegalRegistrationIdentifier.php │ │ ├── ItemClassificationIdentifier.php │ │ ├── VatIdentifier.php │ │ └── SpecificationIdentifier.php │ ├── DateCode2005.php │ ├── DateCode2475.php │ ├── BinaryObject.php │ ├── Address.php │ ├── VatCategory.php │ ├── MimeCode.php │ ├── InvoiceTypeCode.php │ ├── AllowanceReasonCode.php │ ├── VatExoneration.php │ ├── PaymentMeansCode.php │ ├── ElectronicAddressScheme.php │ ├── CurrencyCode.php │ ├── ChargeReasonCode.php │ ├── CountryAlpha2Code.php │ ├── ItemTypeCode.php │ ├── InternationalCodeDesignator.php │ └── InvoiceNoteCode.php ├── Codelist │ ├── TimeReferencingCodeUNTDID2005.php │ ├── TimeReferencingCodeUNTDID2475.php │ ├── Generator │ │ ├── XlsxReaderResultItem.php │ │ ├── Generator.php │ │ ├── XlsxReader.php │ │ ├── XlsxReaderResult.php │ │ └── CodelistGenerator.php │ ├── MimeCode.php │ ├── DutyTaxFeeCategoryCodeUNTDID5305.php │ ├── AllowanceReasonCodeUNTDID5189.php │ ├── InvoiceTypeCodeUNTDID1001.php │ ├── PaymentMeansCodeUNTDID4461.php │ ├── ElectronicAddressSchemeCode.php │ ├── VatExemptionReasonCode.php │ ├── CurrencyCodeISO4217.php │ ├── ChargeReasonCodeUNTDID7161.php │ ├── CountryAlpha2Code.php │ ├── ItemTypeCodeUNTDID7143.php │ ├── InternationalCodeDesignator.php │ └── TextSubjectCodeUNTDID4451.php ├── SemanticDataType │ ├── Amount.php │ ├── Quantity.php │ ├── Percentage.php │ ├── UnitPriceAmount.php │ ├── Number.php │ ├── IntegerNumber.php │ └── DecimalNumber.php ├── Converter │ ├── DateCode2005ToDateCode2475Converter.php │ └── TimeReferencingCodeUNTDID2005ToTimeReferencingCodeUNTDID2475.php └── Helper │ └── InvoiceTypeCodeUNTDID1001Helper.php ├── README.md ├── bin └── generate-codelists.php ├── LICENSE └── composer.json /src/DataType/Reference/ContractReference.php: -------------------------------------------------------------------------------- 1 | name, $this->value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DataType/Identifier/ItemClassificationIdentifier.php: -------------------------------------------------------------------------------- 1 | DateCode2475::INVOICE_DATE, 19 | DateCode2005::DELIVERY_DATE => DateCode2475::DELIVERY_DATE, 20 | DateCode2005::PAYMENT_DATE => DateCode2475::PAYMENT_DATE 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DataType/VatCategory.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | 23 | // @todo : Verification for length ? 13 SellerTaxRepresentativeTradeParty / 15 Buyer / 14 Seller (Annexe 1) 24 | } 25 | 26 | public function getValue(): string 27 | { 28 | return $this->value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DataType/Identifier/SpecificationIdentifier.php: -------------------------------------------------------------------------------- 1 | ; 16 | 17 | enum : string 18 | { 19 | 20 | } 21 | TEMPLATE; 22 | 23 | public function generateCodelist( 24 | string $className, 25 | XlsxReaderResult $cases 26 | ): void { 27 | $code = []; 28 | 29 | /** @var XlsxReaderResultItem $case */ 30 | foreach ($cases->getHarmonized() as $case) { 31 | $code[] = (string) $case; 32 | } 33 | 34 | $replacements = [ 35 | '' => ' ' . implode("\n ", $code), 36 | '' => 'Tiime\EN16931\Codelist', 37 | '' => $className 38 | ]; 39 | 40 | $code = strtr(self::ENUM_TEMPLATE, $replacements); 41 | $code = preg_replace('/^ +$/m', '', $code); 42 | $path = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . $className . '.php'; 43 | 44 | file_put_contents($path, $code); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Helper/InvoiceTypeCodeUNTDID1001Helper.php: -------------------------------------------------------------------------------- 1 | setLoadSheetsOnly($sheetName) 19 | ->setReadEmptyCells(false) 20 | ->setIgnoreRowsWithNoCells(true) 21 | ; 22 | $spreadsheet = $reader->load($filename); 23 | 24 | $worksheet = $spreadsheet->getSheetByNameOrThrow($sheetName); 25 | $highestRow = $worksheet->getHighestRow(); 26 | 27 | $result = new XlsxReaderResult(); 28 | 29 | for ($row = $startLine; $row <= $highestRow; $row++) { 30 | $nameFromCell = $worksheet->getCell($nameColumn . $row)->getValue(); 31 | assert(is_string($nameFromCell)); 32 | $valueFromCell = $worksheet->getCell($valueColumn . $row)->getValue(); 33 | assert(is_string($valueFromCell)); 34 | $name = trim($nameFromCell); 35 | $value = trim($valueFromCell); 36 | 37 | $result->add(new XlsxReaderResultItem( 38 | name: $name, 39 | value: $value, 40 | )); 41 | } 42 | 43 | return $result; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Converter/TimeReferencingCodeUNTDID2005ToTimeReferencingCodeUNTDID2475.php: -------------------------------------------------------------------------------- 1 | TimeReferencingCodeUNTDID2475::DATE_OF_INVOICE, 16 | TimeReferencingCodeUNTDID2005::DELIVERY_DATE_TIME_ACTUAL => TimeReferencingCodeUNTDID2475::DATE_OF_DELIVERY_OF_GOODS_TO_ESTABLISHMENTS_DOMICILE_SITE, 17 | TimeReferencingCodeUNTDID2005::PAID_TO_DATE => TimeReferencingCodeUNTDID2475::PAYMENT_DATE 18 | }; 19 | } 20 | 21 | public static function convertToUNTDID2005(TimeReferencingCodeUNTDID2475 $code): TimeReferencingCodeUNTDID2005 22 | { 23 | return match ($code) { 24 | TimeReferencingCodeUNTDID2475::DATE_OF_INVOICE => TimeReferencingCodeUNTDID2005::INVOICE_DOCUMENT_ISSUE_DATE_TIME, 25 | TimeReferencingCodeUNTDID2475::DATE_OF_DELIVERY_OF_GOODS_TO_ESTABLISHMENTS_DOMICILE_SITE => TimeReferencingCodeUNTDID2005::DELIVERY_DATE_TIME_ACTUAL, 26 | TimeReferencingCodeUNTDID2475::PAYMENT_DATE => TimeReferencingCodeUNTDID2005::PAID_TO_DATE 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiime/en-16931", 3 | "description": "EN-16931 compliant invoices as PHP objects", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Flavien RODRIGUES", 9 | "email": "rodrigues.flavien@gmail.com" 10 | }, 11 | { 12 | "name": "Lucas GERARD", 13 | "email": "lucas.gerard.web@gmail.com" 14 | }, 15 | { 16 | "name": "Aurélien PILLEVESSE", 17 | "email": "aurelienpillevesse@hotmail.fr" 18 | } 19 | ], 20 | "minimum-stability": "stable", 21 | "require": { 22 | "php": ">=8.3", 23 | "ext-bcmath": "*", 24 | "ext-iconv": "*" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^10.0 || ^12.0", 28 | "squizlabs/php_codesniffer": "^3.7", 29 | "phpstan/phpstan": "^1.10", 30 | "staabm/annotate-pull-request-from-checkstyle": "^1.8", 31 | "phpoffice/phpspreadsheet": "^3.3 || ^4.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Tiime\\EN16931\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Tiime\\EN16931\\Tests\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/phpunit tests", 45 | "code_sniffer": "vendor/bin/phpcs -q --report=checkstyle --standard=PSR12 src/", 46 | "phpstan": "vendor/bin/phpstan analyse -l 9 src tests", 47 | "generate-codelists": "php bin/generate-codelists.php" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/DataType/MimeCode.php: -------------------------------------------------------------------------------- 1 | value; 17 | } 18 | 19 | public function add(Number $number, ?int $decimals = null): float 20 | { 21 | $result = (float) bcadd((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 22 | 23 | if (null !== $decimals) { 24 | return round($result, $decimals); 25 | } 26 | 27 | return $result; 28 | } 29 | 30 | public function subtract(Number $number, ?int $decimals = null): float 31 | { 32 | $result = (float) bcsub((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 33 | 34 | if (null !== $decimals) { 35 | return round($result, $decimals); 36 | } 37 | 38 | return $result; 39 | } 40 | 41 | public function multiply(Number $number, ?int $decimals = null): float 42 | { 43 | $result = (float) bcmul((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 44 | 45 | if (null !== $decimals) { 46 | return round($result, $decimals); 47 | } 48 | 49 | return $result; 50 | } 51 | 52 | public function divide(Number $number, ?int $decimals = null): float 53 | { 54 | $result = (float) bcdiv((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 55 | 56 | if (null !== $decimals) { 57 | return round($result, $decimals); 58 | } 59 | 60 | return $result; 61 | } 62 | 63 | public function __toString(): string 64 | { 65 | return (string) $this->getValue(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DataType/InvoiceTypeCode.php: -------------------------------------------------------------------------------- 1 | decimals !== null 15 | && !preg_match(sprintf('/^-?\d+(\.\d{1,%s})?$/', $decimals), (string) $value) 16 | ) { 17 | throw new \Exception('@todo'); 18 | } 19 | } 20 | 21 | public function getValue(): float 22 | { 23 | return $this->value; 24 | } 25 | 26 | public function getValueRounded(): float 27 | { 28 | if (null === $this->decimals) { 29 | return $this->value; 30 | } 31 | 32 | return round($this->value, $this->decimals); 33 | } 34 | 35 | public function getFormattedValueRounded(string $decimal_separator = '.', string $thousands_separator = ''): string 36 | { 37 | if (null === $this->decimals) { 38 | return number_format($this->value, 0, $decimal_separator, $thousands_separator); 39 | } 40 | 41 | return number_format( 42 | round($this->value, $this->decimals), 43 | $this->decimals, 44 | $decimal_separator, 45 | $thousands_separator 46 | ); 47 | } 48 | 49 | public function add(Number $number, ?int $decimals = null): float 50 | { 51 | $result = (float) bcadd((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 52 | 53 | if (null !== $decimals) { 54 | return round($result, $decimals); 55 | } 56 | 57 | return $result; 58 | } 59 | 60 | public function subtract(Number $number, ?int $decimals = null): float 61 | { 62 | $result = (float) bcsub((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 63 | 64 | if (null !== $decimals) { 65 | return round($result, $decimals); 66 | } 67 | 68 | return $result; 69 | } 70 | 71 | public function multiply(Number $number, ?int $decimals = null): float 72 | { 73 | $result = (float) bcmul((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 74 | 75 | if (null !== $decimals) { 76 | return round($result, $decimals); 77 | } 78 | 79 | return $result; 80 | } 81 | 82 | public function divide(Number $number, ?int $decimals = null): float 83 | { 84 | $result = (float) bcdiv((string) $this->getValue(), (string) $number->getValue(), Number::BC_MATH_ROUNDING); 85 | 86 | if (null !== $decimals) { 87 | return round($result, $decimals); 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | public function __toString(): string 94 | { 95 | return (string) $this->getValueRounded(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/DataType/AllowanceReasonCode.php: -------------------------------------------------------------------------------- 1 | result[] = $item; 17 | } 18 | 19 | /** 20 | * @return XlsxReaderResultItem[] 21 | */ 22 | public function get(): array 23 | { 24 | return $this->result; 25 | } 26 | 27 | /** 28 | * @return XlsxReaderResultItem[] 29 | */ 30 | public function getHarmonized(): array 31 | { 32 | $nameCount = []; 33 | $harmonizedResult = []; 34 | 35 | foreach ($this->result as $item) { 36 | $name = $item->name; 37 | 38 | $name = preg_replace('/§\s*27/', '_', $name); // Specific ICD sheet 39 | assert(is_string($name)); 40 | $name = preg_replace('/®/', '_', $name); 41 | assert(is_string($name)); 42 | $name = preg_replace('/@/', 'A', $name); 43 | assert(is_string($name)); 44 | 45 | $replacements = [ 46 | '–' => '_', 47 | '>' => 'GREATER', // Replace > with GREATER 48 | '%' => 'PERCENT', // Replace % with PERCENT 49 | '+' => 'PLUS', // Replace + with PLUS 50 | '°' => 'DEGREE' // Replace ° with DEGREE 51 | ]; 52 | 53 | $name = strtr($name, $replacements); 54 | 55 | $name = preg_replace('/[()]/', '', $name); // Remove parentheses 56 | assert(is_string($name)); 57 | $name = preg_replace('/\s*\[.*?\]\s*/', '', $name); // Remove brackets and content inside 58 | assert(is_string($name)); 59 | $name = preg_replace('/-/', '_', $name); // Replace - with underscores 60 | assert(is_string($name)); 61 | $name = preg_replace('/,/', '_', $name); // Replace , with underscores 62 | assert(is_string($name)); 63 | $name = preg_replace('/&/', '_', $name); // Replace & with underscores 64 | assert(is_string($name)); 65 | $name = preg_replace('/\//', '_', $name); // Replace / with underscores 66 | assert(is_string($name)); 67 | $name = preg_replace('/[.\'“”’:=*]/u', '', $name); 68 | assert(is_string($name)); 69 | 70 | $name = trim($name, '_'); // Ensure the result does not start or end with an underscore 71 | 72 | $name = preg_replace('/^(\d)/', '_$1', $name); // Add underscore at the beginning if starting by a number 73 | assert(is_string($name)); 74 | 75 | // Convert accented characters to non-accented 76 | $name = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name); 77 | assert(is_string($name)); 78 | $name = mb_strtoupper($name); 79 | assert(is_string($name)); 80 | $name = preg_replace('/\s+/', '_', $name); // Replace spaces with underscores 81 | assert(is_string($name)); 82 | $name = preg_replace('/_+/', '_', $name); // Replace multiple underscores with a single underscore 83 | assert(is_string($name)); 84 | 85 | // Initialize the count for this name if it doesn't exist 86 | if (!isset($nameCount[$name])) { 87 | $nameCount[$name] = 0; 88 | } 89 | 90 | $nameCount[$name]++; 91 | 92 | if ($nameCount[$name] > 1) { 93 | $name .= '_' . $this->getSuffix($nameCount[$name]); 94 | } 95 | 96 | $harmonizedResult[] = new XlsxReaderResultItem(name: $name, value: $item->value); 97 | } 98 | 99 | return $harmonizedResult; 100 | } 101 | 102 | private function getSuffix(int $count): string 103 | { 104 | return match ($count) { 105 | 2 => 'SECOND', 106 | 3 => 'THIRD', 107 | default => $count . 'TH', 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Codelist/Generator/CodelistGenerator.php: -------------------------------------------------------------------------------- 1 | generateCodelist(className: 'CountryAlpha2Code', cases: $countryList); 33 | $generator->generateCodelist(className: 'CurrencyCodeISO4217', cases: $currencyList); 34 | $generator->generateCodelist(className: 'InternationalCodeDesignator', cases: $internationalCodeDesignatorList); 35 | $generator->generateCodelist(className: 'InvoiceTypeCodeUNTDID1001', cases: $code1001List); 36 | $generator->generateCodelist(className: 'ReferenceQualifierCodeUNTDID1153', cases: $code1153List); 37 | $generator->generateCodelist(className: 'TimeReferencingCodeUNTDID2005', cases: $code2005List); 38 | $generator->generateCodelist(className: 'TimeReferencingCodeUNTDID2475', cases: $code2475List); 39 | $generator->generateCodelist(className: 'TextSubjectCodeUNTDID4451', cases: $code4451List); 40 | $generator->generateCodelist(className: 'PaymentMeansCodeUNTDID4461', cases: $code4461List); 41 | $generator->generateCodelist(className: 'DutyTaxFeeCategoryCodeUNTDID5305', cases: $code5305List); 42 | $generator->generateCodelist(className: 'AllowanceReasonCodeUNTDID5189', cases: $code5189List); 43 | $generator->generateCodelist(className: 'ItemTypeCodeUNTDID7143', cases: $code7143List); 44 | $generator->generateCodelist(className: 'ChargeReasonCodeUNTDID7161', cases: $code7161List); 45 | $generator->generateCodelist(className: 'MimeCode', cases: $mimeList); 46 | $generator->generateCodelist(className: 'ElectronicAddressSchemeCode', cases: $electronicAddressSchemeList); 47 | $generator->generateCodelist(className: 'VatExemptionReasonCode', cases: $vatExemptionReasonCodeList); 48 | $generator->generateCodelist(className: 'UnitOfMeasureCode', cases: $unitOfMeasure); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DataType/PaymentMeansCode.php: -------------------------------------------------------------------------------- 1 |