From 08594cb33b44be2304afd726fa4e16f8ac6870a4 Mon Sep 17 00:00:00 2001 From: rahal Date: Thu, 6 Feb 2025 20:48:39 +0100 Subject: [PATCH] Added more tests while ensuring schematron tests pass for all profiles (and fixed formating) --- .gitignore | 1 + src/FacturX.php | 154 +++++++++++++++++++----------------- src/Invoice.php | 92 +++++++++++----------- src/Types.php | 19 ++--- src/Ubl.php | 81 +++++++++++-------- src/XmlGenerator.php | 40 +++++++--- src/Zugferd.php | 181 +++++++++++++++++++++++-------------------- test/InvoiceTest.php | 37 +++++---- test/TypesTest.php | 4 - 9 files changed, 344 insertions(+), 265 deletions(-) diff --git a/.gitignore b/.gitignore index 4a4395d..2943cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor .idea .DS_Store .vscode +test/examples/basic-* diff --git a/src/FacturX.php b/src/FacturX.php index aaf33cf..62c02d7 100644 --- a/src/FacturX.php +++ b/src/FacturX.php @@ -10,7 +10,6 @@ use Easybill\ZUGFeRD211\Model\CreditorFinancialInstitution; use Easybill\ZUGFeRD211\Model\CrossIndustryInvoice; use Easybill\ZUGFeRD211\Model\DateTime; - use Easybill\ZUGFeRD211\Model\DocumentContextParameter; use Easybill\ZUGFeRD211\Model\DocumentLineDocument; use Easybill\ZUGFeRD211\Model\ExchangedDocument; @@ -19,23 +18,21 @@ use Easybill\ZUGFeRD211\Model\HeaderTradeDelivery; use Easybill\ZUGFeRD211\Model\HeaderTradeSettlement; use Easybill\ZUGFeRD211\Model\Id; +use Easybill\ZUGFeRD211\Model\LegalOrganization; use Easybill\ZUGFeRD211\Model\LineTradeAgreement; use Easybill\ZUGFeRD211\Model\LineTradeDelivery; use Easybill\ZUGFeRD211\Model\LineTradeSettlement; -use Easybill\ZUGFeRD211\Model\LegalOrganization; use Easybill\ZUGFeRD211\Model\Note; use Easybill\ZUGFeRD211\Model\Quantity; - use Easybill\ZUGFeRD211\Model\ReferencedDocument; use Easybill\ZUGFeRD211\Model\SupplyChainEvent; use Easybill\ZUGFeRD211\Model\SupplyChainTradeLineItem; use Easybill\ZUGFeRD211\Model\SupplyChainTradeTransaction; use Easybill\ZUGFeRD211\Model\TaxRegistration; use Easybill\ZUGFeRD211\Model\TradeAddress; - use Easybill\ZUGFeRD211\Model\TradeContact; - use Easybill\ZUGFeRD211\Model\TradeParty; +use Easybill\ZUGFeRD211\Model\TradePaymentTerms; use Easybill\ZUGFeRD211\Model\TradePrice; use Easybill\ZUGFeRD211\Model\TradeProduct; use Easybill\ZUGFeRD211\Model\TradeSettlementHeaderMonetarySummation; @@ -43,14 +40,11 @@ use Easybill\ZUGFeRD211\Model\TradeSettlementPaymentMeans; use Easybill\ZUGFeRD211\Model\TradeTax; use Easybill\ZUGFeRD211\Model\UniversalCommunication; - - - use Easybill\ZUGFeRD211\Validator; use Milo\Schematron; - -class FacturX extends XmlGenerator { +class FacturX extends XmlGenerator +{ public const MINIMUM = 'urn:factur-x.eu:1p0:minimum'; public const BASIC_WL = 'urn:factur-x.eu:1p0:basicwl'; public const BASIC = 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic'; @@ -65,22 +59,22 @@ class FacturX extends XmlGenerator { public const LEVELS = [ FacturX::MINIMUM => self::LEVEL_MINIMUM , - FacturX::BASIC_WL => self::LEVEL_BASIC_WL , + FacturX::BASIC_WL => self::LEVEL_BASIC_WL , // will define thos later - FacturX::BASIC => self::LEVEL_BASIC , - FacturX::EN16931 => self::LEVEL_EN16931 , - FacturX::EXTENDED => self::LEVEL_EN16931 , - FacturX::XRECHNUNG => self::LEVEL_EN16931, + FacturX::BASIC => self::LEVEL_BASIC , + FacturX::EN16931 => self::LEVEL_EN16931 , + FacturX::EXTENDED => self::LEVEL_EN16931 , + FacturX::XRECHNUNG => self::LEVEL_EN16931, ]; - - - protected static function convertDate(\DateTime $date) { return DateTime::create(102, $date->format('Ymd')); } - public function initDocument( $invoiceId , \DateTime $issueDateTime, $invoiceType, ?\DateTime $deliveryDate=null ){ $this->invoice = new CrossIndustryInvoice(); + + public function initDocument($invoiceId, \DateTime $issueDateTime, $invoiceType, ?\DateTime $deliveryDate = null) + { + $this->invoice = new CrossIndustryInvoice(); $this->invoice->exchangedDocumentContext = new ExchangedDocumentContext(); $this->invoice->exchangedDocumentContext->documentContextParameter = new DocumentContextParameter(); @@ -95,37 +89,46 @@ public function initDocument( $invoiceId , \DateTime $issueDateTime, $invoiceTy $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeAgreement = new HeaderTradeAgreement(); - //$this->invoice->supplyChainTradeTransaction->applicableHeaderTradeAgreement->specifiedProcuringProject = ProcuringProject::create('1234', 'Projekt'); - $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeDelivery = new HeaderTradeDelivery(); if ($deliveryDate) { $this->hasDelivery = true; $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeDelivery->chainEvent = new SupplyChainEvent(); - $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeDelivery->chainEvent->date = self::convertDate($deliveryDate); + $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeDelivery->chainEvent->date = self::convertDate($deliveryDate); } $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement = new HeaderTradeSettlement(); $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->currency = $this->currency->value ; + return $this->invoice; } - public function setSeller(string $id, InternationalCodeDesignator $idType, string $name, $tradingName = null ) + public function setPaymentTerms(\DateTime $dueDate, ?string $description = null) + { + if ($this->getProfileLevel() >= self::LEVEL_BASIC_WL) { + $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->specifiedTradePaymentTerms[] = $paymentTerms = new TradePaymentTerms(); + $paymentTerms->dueDate = self::convertDate($dueDate); + if ($this->getProfileLevel() > self::LEVEL_BASIC) { + $paymentTerms->description = $description; + } + } + } + + public function setSeller(string $id, InternationalCodeDesignator $idType, string $name, $tradingName = null) { $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeAgreement->sellerTradeParty = $this->seller = new TradeParty(); //$sellerTradeParty->globalID[] = Id::create($id, $idType->value); $this->seller->legalOrganization = LegalOrganization::create($id, $idType->value, $tradingName); $this->seller->name = $name; - - } - public function setPayee(){ - // Pas de payeeTradeParty dans le minimum - if ($this->getProfileLevel() > self::LEVEL_MINIMUM) { + public function setPayee() + { + // Pas de payeeTradeParty dans le minimum + if ($this->getProfileLevel() > self::LEVEL_MINIMUM) { // The Payee name (BT-59) shall be provided in the Invoice, if the Payee (BG-10) is different from the Seller (BG-4). $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->payeeTradeParty = $this->seller; @@ -134,54 +137,54 @@ public function setPayee(){ public function setSellerContact(?string $personName = null, ?string $departmentName = null, ?string $telephone = null, ?string $email = null) { - if ($this->getProfileLevel() >= self::LEVEL_EN16931){ + if ($this->getProfileLevel() >= self::LEVEL_EN16931) { $this->seller->definedTradeContact = new TradeContact(); $this->seller->definedTradeContact->personName = $personName; - if ( $telephone){ + if ($telephone) { $this->seller->definedTradeContact->telephoneUniversalCommunication = new UniversalCommunication(); $this->seller->definedTradeContact->telephoneUniversalCommunication->completeNumber = $telephone; } - if ($email){ + if ($email) { $this->seller->definedTradeContact->emailURIUniversalCommunication = new UniversalCommunication(); $this->seller->definedTradeContact->emailURIUniversalCommunication->uriid = Id::create($email); } - if ( $departmentName){ + if ($departmentName) { $this->seller->definedTradeContact->departmentName = $departmentName; } } } - public function addPaymentMean(PaymentMeansCode $typeCode , ?string $ibanId = null,?string $accountName = null, ?string $bicId = null){ - if($this->getProfileLevel() >= self::LEVEL_BASIC_WL ){ - + public function addPaymentMean(PaymentMeansCode $typeCode, ?string $ibanId = null, ?string $accountName = null, ?string $bicId = null) + { + if ($this->getProfileLevel() >= self::LEVEL_BASIC_WL) { $mean = new TradeSettlementPaymentMeans(); $mean->typeCode = $typeCode->value ; // $mean->information = 'get info from type code??'; $mean->payeePartyCreditorFinancialAccount = new CreditorFinancialAccount(); $mean->payeePartyCreditorFinancialAccount->ibanId = Id::create($ibanId); - if ($this->getProfileLevel() > self::LEVEL_BASIC){ + if ($this->getProfileLevel() > self::LEVEL_BASIC) { $mean->payeePartyCreditorFinancialAccount->AccountName = $accountName; } - if ($bicId){ + if ($bicId) { $mean->payeeSpecifiedCreditorFinancialInstitution = new CreditorFinancialInstitution(); $mean->payeeSpecifiedCreditorFinancialInstitution->bicId = Id::create($bicId); } - $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->specifiedTradeSettlementPaymentMeans[]=$mean; + $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->specifiedTradeSettlementPaymentMeans[] = $mean; } } public function setSellerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null) { $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeAgreement->sellerTradeParty->postalTradeAddress = $this->createAddress($postCode, $city, $countryCode, $lineOne, $lineTwo, $lineThree); + return $this; } - - public function setBuyerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null) { $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeAgreement->buyerTradeParty->postalTradeAddress = $this->createAddress($postCode, $city, $countryCode, $lineOne, $lineTwo, $lineThree); + return $this; } @@ -198,10 +201,11 @@ public function setBuyer(string $buyerReference, string $name, string $id = null if ($this->getProfileLevel() > self::LEVEL_MINIMUM && $id) { $buyerTradeParty->id = Id::create($id); } - $buyerTradeParty->name = $name ; + $buyerTradeParty->name = $name ; if ($this->hasDelivery) { $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeDelivery->shipToTradeParty = $buyerTradeParty; } + return $this; } @@ -249,7 +253,7 @@ protected function calculateTotals() $tradeTax->rateApplicablePercent = self::decimalFormat($rate) ; $tax += $calculated = $sum * $rate / 100; $tradeTax->calculatedAmount = Amount::create(self::decimalFormat($calculated)); - if ($this->getProfileLevel() >= self::LEVEL_BASIC_WL){ + if ($this->getProfileLevel() >= self::LEVEL_BASIC_WL) { $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->tradeTaxes[] = $tradeTax; } } @@ -257,9 +261,9 @@ protected function calculateTotals() if (in_array($this->profile, [self::BASIC, self::EN16931, self::EXTENDED ])) { throw new \Exception('You need to set invoice items using setItem'); } - if ($this->profile == self::MINIMUM || $this->profile == self::BASIC_WL) { - if(!isset($this->totalBasis)) { - throw new \Exception('You should call setPrice to set taxBasisTotal and taxTotal'); + if ($this->profile == self::MINIMUM || $this->profile == self::BASIC_WL) { + if (! isset($this->totalBasis)) { + throw new \Exception('You should call tax to set totalBasis and taxTotal'); } } $totalBasis = $this->totalBasis ; @@ -283,41 +287,50 @@ protected function calculateTotals() } $summation->duePayableAmount = Amount::create(self::decimalFormat($grand)); $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->specifiedTradeSettlementHeaderMonetarySummation = $summation; - } - public function getXml() { // calculate tradeTaxes $this->calculateTotals(); + return Builder::create()->transform($this->invoice); } - - - public function validate(string $xml, $schematron) { + public function validate(string $xml, $schematron) + { switch ($this->profile) { - case self::MINIMUM: $against = Validator::SCHEMA_MINIMUM; + case self::MINIMUM: + $against = Validator::SCHEMA_MINIMUM; + break; - case self::BASIC: $against = Validator::SCHEMA_BASIC; + case self::BASIC: + $against = Validator::SCHEMA_BASIC; + break; - case self::BASIC_WL: $against = Validator::SCHEMA_BASIC_WL; + case self::BASIC_WL: + $against = Validator::SCHEMA_BASIC_WL; + break; - case self::EN16931: $against = Validator::SCHEMA_EN16931; + case self::EN16931: + $against = Validator::SCHEMA_EN16931; + break; case self::EXTENDED: - case self::XRECHNUNG: $against = Validator::SCHEMA_EXTENDED; + case self::XRECHNUNG: + $against = Validator::SCHEMA_EXTENDED; + break; - default: $against = Validator::SCHEMA_MINIMUM; + default: + $against = Validator::SCHEMA_MINIMUM; } if ($schematron) { $against = str_replace([ '.xsd', - 'FACTUR-X' + 'FACTUR-X', ], [ '.sch', - 'Schematron/FACTUR-X' + 'Schematron/FACTUR-X', ], $against); } @@ -327,15 +340,18 @@ public function validate(string $xml, $schematron) { $schematron->load($against); $document = new \DOMDocument(); $document->loadXml($xml); + return @$schematron->validate($document, Schematron::RESULT_COMPLEX); } + return (new Validator())->validateAgainstXsd($xml, $against); } - - public function addItem(string $name, float $price, float $taxRatePercent, float $quantity , UnitOfMeasurement $unit , ?string $globalID = null, string $globalIDCode = null) : float + public function addItem(string $name, float $price, float $taxRatePercent, float $quantity, UnitOfMeasurement $unit, ?string $globalID = null, string $globalIDCode = null): float { - + if ($this->getProfileLevel() < self::LEVEL_BASIC) { + return 0; + } $item = new SupplyChainTradeLineItem(); $lineNumber = count($this->items) + 1; @@ -352,7 +368,7 @@ public function addItem(string $name, float $price, float $taxRatePercent, float $item->tradeAgreement = new LineTradeAgreement(); - if ($this->getProfileLevel() >= self::LEVEL_EN16931){ + if ($this->getProfileLevel() >= self::LEVEL_EN16931) { $item->tradeAgreement->grossPrice = TradePrice::create(self::decimalFormat($price)); } $item->tradeAgreement->netPrice = TradePrice::create(self::decimalFormat($price)); @@ -364,7 +380,7 @@ public function addItem(string $name, float $price, float $taxRatePercent, float $item->specifiedLineTradeSettlement->tradeTax[] = $itemtax = new TradeTax(); $itemtax->typeCode = TaxTypeCodeContent::VAT->value; $itemtax->categoryCode = VatCategory::STANDARD->value ; - $itemtax->rateApplicablePercent = self::decimalFormat($taxRatePercent); + $itemtax->rateApplicablePercent = self::decimalFormat($taxRatePercent); $totalLineBasis = $price * $quantity; @@ -372,13 +388,12 @@ public function addItem(string $name, float $price, float $taxRatePercent, float $item->specifiedLineTradeSettlement->monetarySummation = TradeSettlementLineMonetarySummation::create(self::decimalFormat($totalLineBasis)); - $this->items[] = $item; - if ($this->getProfileLevel() >= self::LEVEL_BASIC){ + $this->items[] = $item; + if ($this->getProfileLevel() >= self::LEVEL_BASIC) { $this->invoice->supplyChainTradeTransaction->lineItems[] = $item; } return $totalLineBasis; - } public function addNote(string $content, ?string $subjectCode = null, ?string $contentCode = null) @@ -386,8 +401,9 @@ public function addNote(string $content, ?string $subjectCode = null, ?string $c $this->invoice->exchangedDocument->notes[] = Note::create($content, $subjectCode, $contentCode); } - public function addEmbeddedAttachment( ?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description ){ - // The attachement is correctly added but schematron fails, need to + public function addEmbeddedAttachment(?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description) + { + // The attachement is correctly added but schematron fails, need to // $attachment = ReferencedDocument::create($id); // $attachment->name = $description; // $binary = new BinaryObject(); @@ -401,4 +417,4 @@ public function addEmbeddedAttachment( ?string $id, ?string $scheme, ?string $fi // default : $this->invoice->supplyChainTradeTransaction->applicableHeaderTradeSettlement->invoiceReferencedDocument = $attachment; // } } -} \ No newline at end of file +} diff --git a/src/Invoice.php b/src/Invoice.php index baa86f4..eda4280 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -2,10 +2,8 @@ namespace DigitalInvoice; - use Easybill\ZUGFeRD211\Model\DateTime; - class Invoice { public const FACTURX_MINIMUM = FacturX::MINIMUM; @@ -21,7 +19,7 @@ class Invoice public const UBL_NLCIUS = Ubl::NLCIUS; public const UBL_CIUS_RO = Ubl::CIUS_RO; public const UBL_CIUS_IT = Ubl::CIUS_IT; - public const UBL_CIUS_ES_FACE = Ubl::CIUS_ES_FACE; + public const UBL_CIUS_ES_FACE = Ubl::CIUS_ES_FACE; public const UBL_CIUS_AT_GOV = Ubl::CIUS_AT_GOV; public const UBL_CIUS_AT_NAT = Ubl::CIUS_AT_NAT; @@ -37,7 +35,7 @@ class Invoice public XmlGenerator $xmlGenerator; - protected $invoiceInformations=[]; + protected $invoiceInformations = []; public function __construct( string $invoiceId, @@ -47,14 +45,14 @@ public function __construct( $profile = self::FACTURX_MINIMUM, string|InvoiceTypeCode $invoiceType = InvoiceTypeCode::COMMERCIAL_INVOICE ) { - if(is_string($currency)) { + if (is_string($currency)) { try { $currency = CurrencyCode::from($currency); } catch (\ValueError $e) { throw new \Exception("$currency is not a valid Currency"); } } - if(is_string($invoiceType)) { + if (is_string($invoiceType)) { try { $invoiceType = InvoiceTypeCode::from($invoiceType); } catch (\ValueError $e) { @@ -62,20 +60,19 @@ public function __construct( } } $this->profile = $profile; - if ( in_array($profile, [Ubl::PEPPOL, Ubl::NLCIUS, Ubl::CIUS_RO, Ubl::CIUS_IT, Ubl::CIUS_ES_FACE, Ubl::CIUS_AT_GOV, Ubl::CIUS_AT_NAT] ) ){ + if (in_array($profile, [Ubl::PEPPOL, Ubl::NLCIUS, Ubl::CIUS_RO, Ubl::CIUS_IT, Ubl::CIUS_ES_FACE, Ubl::CIUS_AT_GOV, Ubl::CIUS_AT_NAT])) { $this->xmlGenerator = new Ubl($profile, $currency); - $this->profile = (new $profile)->getSpecification(); - } elseif ( in_array($profile , [FacturX::MINIMUM , FacturX::BASIC_WL, FacturX::BASIC, FacturX::EN16931, FacturX::EXTENDED, FacturX::XRECHNUNG] ) ){ + $this->profile = (new $profile())->getSpecification(); + } elseif (in_array($profile, [FacturX::MINIMUM , FacturX::BASIC_WL, FacturX::BASIC, FacturX::EN16931, FacturX::EXTENDED, FacturX::XRECHNUNG])) { $this->xmlGenerator = new FacturX($profile, $currency); } else { - $this->xmlGenerator = new Zugferd($profile , $currency); + $this->xmlGenerator = new Zugferd($profile, $currency); } - $this->invoiceInformations['profile'] = $profile; - $this->invoiceInformations['invoiceId'] = $invoiceId; - $this->invoiceInformations['date'] = $issueDate->format('Y-m-d'); - $this->invoiceInformations['docTypeName'] = $invoiceType->value; - $this->xmlGenerator->initDocument( $invoiceId , $issueDate, $invoiceType, $deliveryDate); - + $this->invoiceInformations['profile'] = $profile; + $this->invoiceInformations['invoiceId'] = $invoiceId; + $this->invoiceInformations['date'] = $issueDate->format('Y-m-d'); + $this->invoiceInformations['docTypeName'] = $invoiceType->value; + $this->xmlGenerator->initDocument($invoiceId, $issueDate, $invoiceType, $deliveryDate); } public function getProfileLevel() @@ -83,13 +80,11 @@ public function getProfileLevel() return $this->xmlGenerator->getProfileLevel(); } - public function setPrice(float $totalBasis, float $tax = 0) { - $this->xmlGenerator->setPrice( $totalBasis, $tax ); + $this->xmlGenerator->setPrice($totalBasis, $tax); } - public function getXml() { return $this->xmlGenerator->getXml(); @@ -100,23 +95,20 @@ protected static function convertDate(\DateTime $date) return DateTime::create(102, $date->format('Ymd')); } - public function setBuyer(string $buyerReference, string $name, string $id = null) { - $this->xmlGenerator->setBuyer( $buyerReference, $name, $id); - + $this->xmlGenerator->setBuyer($buyerReference, $name, $id); } public function createAddress(string $postCode, string $city, string $countryCode, string $lineOne, ?string $lineTwo = null, ?string $lineThree = null) { - return $this->xmlGenerator->createAddress( $postCode, $city, $countryCode, $lineOne, $lineTwo, $lineThree); + return $this->xmlGenerator->createAddress($postCode, $city, $countryCode, $lineOne, $lineTwo, $lineThree); } - public function setBuyerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null) { - $this->xmlGenerator->setBuyerAddress( $lineOne, $postCode, $city, $countryCode, $lineTwo, $lineThree); + $this->xmlGenerator->setBuyerAddress($lineOne, $postCode, $city, $countryCode, $lineTwo, $lineThree); } public function setSeller(string $id, string $idType, string $name, $tradingName = null) @@ -127,13 +119,12 @@ public function setSeller(string $id, string $idType, string $name, $tradingName throw new \Exception("$idType is an Invalide InternationalCodeDesignator"); } $this->invoiceInformations['seller'] = $name; - $this->xmlGenerator->setSeller( $id, $idType, $name, $tradingName); - + $this->xmlGenerator->setSeller($id, $idType, $name, $tradingName); } + public function setSellerContact(?string $personName = null, ?string $telephone = null, ?string $email = null, ?string $departmentName = null) { - $this->xmlGenerator->setSellerContact( $personName , $telephone, $email, $departmentName ); - + $this->xmlGenerator->setSellerContact($personName, $telephone, $email, $departmentName); } public function setSellerTaxRegistration(string $id, string $schemeID) @@ -143,7 +134,8 @@ public function setSellerTaxRegistration(string $id, string $schemeID) public function setSellerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null) { - $this->xmlGenerator->setSellerAddress( $lineOne, $postCode, $city, $countryCode, $lineTwo, $lineThree); + $this->xmlGenerator->setSellerAddress($lineOne, $postCode, $city, $countryCode, $lineTwo, $lineThree); + return $this; } @@ -155,12 +147,14 @@ public function addItem(string $name, float $price, float $taxRatePercent, float throw new \Exception("$unit is not a valide Unit of Unit Of Measurement"); } - $totalLineBasis = $this->xmlGenerator->addItem( $name, $price, $taxRatePercent, $quantity, $unit , $globalID , $globalIDCode); - // To be able to calc easily the invoice totals - $this->xmlGenerator->addTaxLine($taxRatePercent, $totalLineBasis); + $totalLineBasis = $this->xmlGenerator->addItem($name, $price, $taxRatePercent, $quantity, $unit, $globalID, $globalIDCode); + if ($totalLineBasis != 0) { + // To be able to calc easily the invoice totals + $this->xmlGenerator->addTaxLine($taxRatePercent, $totalLineBasis); + } } - public function addPaymentMean(string $typeCode, ?string $ibanId = null,?string $accountName = null, ?string $bicId = null) + public function addPaymentMean(string $typeCode, ?string $ibanId = null, ?string $accountName = null, ?string $bicId = null) { try { $typeCode = PaymentMeansCode::from($typeCode); @@ -168,13 +162,17 @@ public function addPaymentMean(string $typeCode, ?string $ibanId = null,?string throw new \Exception("$typeCode is not a valide Unit of Unit Of Measurement"); } - $this->xmlGenerator->addPaymentMean($typeCode, $ibanId , $accountName, $bicId); + $this->xmlGenerator->addPaymentMean($typeCode, $ibanId, $accountName, $bicId); + } + public function setPaymentTerms(\DateTime $dueDate, ?string $description = null) + { + $this->xmlGenerator->setPaymentTerms($dueDate, $description); } public function addNote(string $content, ?string $subjectCode = null, ?string $contentCode = null) { - $this->xmlGenerator->addNote( $content, $subjectCode, $contentCode); + $this->xmlGenerator->addNote($content, $subjectCode, $contentCode); } /** @@ -189,22 +187,23 @@ public function validate(string $xml, bool $schematron = false): mixed return $this->xmlGenerator->validate($xml, $schematron); } - /** * Generates a pdf string to be saved * * @param string $pdf - * @param boolean $addFacturxLogo + * @param bool $addFacturxLogo + * @param array $fpdiParams * @return string */ - public function getPdf($pdf , $addFacturxLogo = false ) + public function getPdf($pdf, $addFacturxLogo = false, $fpdiParams = []) { - if ( in_array( $this->profile, [static::ZUGFERD_BASIC, static::ZUGFERD_CONFORT, static::ZUGFERD_EXTENDED]) ){ + if (in_array($this->profile, [static::ZUGFERD_BASIC, static::ZUGFERD_CONFORT, static::ZUGFERD_EXTENDED])) { // Ensure false, there is no logo for those profiles $addFacturxLogo = false; } $factureX = new PdfWriter(); - $factureX->setPdfMetaData( $this->invoiceInformations); + $factureX->setPdfMetaData($this->invoiceInformations); + return $factureX->generateFacturxFromFiles( $pdf, $this->getXml(), @@ -212,11 +211,14 @@ public function getPdf($pdf , $addFacturxLogo = false ) true, '', [], - $addFacturxLogo ); + $addFacturxLogo, + 'Data', + $fpdiParams + ); } - public function addEmbeddedAttachment( ?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description ){ - $this->xmlGenerator->addEmbeddedAttachment( $id,$scheme, $filename, $contents, $mimeCode, $description ); + public function addEmbeddedAttachment(?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description) + { + $this->xmlGenerator->addEmbeddedAttachment($id, $scheme, $filename, $contents, $mimeCode, $description); } - } diff --git a/src/Types.php b/src/Types.php index d35160b..964a662 100644 --- a/src/Types.php +++ b/src/Types.php @@ -16,7 +16,7 @@ trait EnumToArray public static function valueArray(): array { foreach (self::cases() as $enum) { - $values[$enum->value] = str_replace( '_' , ' ', $enum->name); + $values[$enum->value] = str_replace('_', ' ', $enum->name); } return $values; @@ -27,17 +27,18 @@ public static function valueArray(): array * Helper Types class to access type enum values. * Can be used to generate select boxes easily. */ -class Types { - - public static function getInternationalCodes(){ - return InternationalCodeDesignator::valueArray(); - } +class Types +{ + public static function getInternationalCodes() + { + return InternationalCodeDesignator::valueArray(); + } } enum PaymentMeansCode: string { use EnumToArray; - + case INSTRUMENT_NOT_DEFINED = "1"; case AUTOMATED_CLEARING_HOUSE_CREDIT = "2"; case AUTOMATED_CLEARING_HOUSE_DEBIT = "3"; @@ -2435,7 +2436,7 @@ enum CurrencyCode: string case SOMONI = "TJS"; case TURKMENISTAN_NEW_MANAT = "TMT"; case TUNISIAN_DINAR = "TND"; - case PA_ANGA = "TOP"; + case PA_ANGA = "TOP"; case TURKISH_LIRA = "TRY"; case TRINIDAD_AND_TOBAGO_DOLLAR = "TTD"; case NEW_TAIWAN_DOLLAR = "TWD"; @@ -2479,7 +2480,7 @@ enum CurrencyCode: string enum InvoiceTypeCode: string { use EnumToArray; - + // https://service.unece.org/trade/untdid/d16b/tred/tred1001.htm case DEBIT_NOTE_RELATED_TO_GOODS_OR_SERVICES = '80'; case CREDIT_NOTE_RELATED_TO_GOODS_OR_SERVICES = '81'; diff --git a/src/Ubl.php b/src/Ubl.php index 31d7df8..38a4dde 100644 --- a/src/Ubl.php +++ b/src/Ubl.php @@ -12,11 +12,17 @@ use Einvoicing\Party; use Einvoicing\Payments\Payment; use Einvoicing\Payments\Transfer; -use Einvoicing\Presets\{Peppol, Nlcius, CiusRo, CiusIt, CiusEsFace, CiusAtGov, CiusAtNat}; +use Einvoicing\Presets\CiusAtGov; +use Einvoicing\Presets\CiusAtNat; +use Einvoicing\Presets\CiusEsFace; +use Einvoicing\Presets\CiusIt; +use Einvoicing\Presets\CiusRo; +use Einvoicing\Presets\Nlcius; +use Einvoicing\Presets\Peppol; use Einvoicing\Writers\UblWriter; -class Ubl extends XmlGenerator { - +class Ubl extends XmlGenerator +{ public const CIUS_AT_NAT = CiusAtNat::class; public const CIUS_AT_GOV = CiusAtGov::class; public const CIUS_ES_FACE = CiusEsFace::class; @@ -31,58 +37,61 @@ class Ubl extends XmlGenerator { public function validate(string $xml, $schematron) { - if ($schematron){ - return $this->euValidation($xml ,'ubl'); + if ($schematron) { + return $this->euValidation($xml, 'ubl'); } else { try { $this->invoice->validate(); + return null; - } catch(ValidationException $e) { + } catch (ValidationException $e) { return [$e->getBusinessRuleId() => $e->getMessage()]; } } } - + /** * Validation against API * Use public api https://www.itb.ec.europa.eu/invoice/api/validation , this function is copied from josemmo/einvoicing test file * (schematron files use xslt2, incompatible with milo/schematron for now) * @param string $contents * @param string $type - * @return boolean + * @return bool */ - protected function euValidation(string $contents, string $type) { - + protected function euValidation(string $contents, string $type) + { + $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => 'https://www.itb.ec.europa.eu/vitb/rest/invoice/api/validate', CURLOPT_RETURNTRANSFER => true, - CURLOPT_POSTFIELDS => json_encode( [ + CURLOPT_POSTFIELDS => json_encode([ 'contentToValidate' => base64_encode($contents), 'embeddingMethod' => 'BASE64', - 'validationType' => 'ubl' + 'validationType' => 'ubl', ]) , CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', - 'Accept: application/json' + 'Accept: application/json', ], - CURLOPT_POST => 1 + CURLOPT_POST => 1, ]); $res = curl_exec($ch); curl_close($ch); unset($ch); $report = json_decode($res, true); - if ($report['result'] === 'SUCCESS'){ + if ($report['result'] === 'SUCCESS') { return []; } else { - $errors = isset($report['reports']['error']) ? $report['reports']['error'] : $report['reports']['warning']; - return array_map( function($e){ return $e['description']." - ".$e['location'] ;} , $errors); + $errors = isset($report['reports']['error']) ? $report['reports']['error'] : $report['reports']['warning']; + + return array_map(function ($e) { + return $e['description']." - ".$e['location'] ; + }, $errors); } - } - public function initDocument($invoiceId, DateTime $issueDateTime, $invoiceType, ?DateTime $deliveryDate = null) { $this->invoice = new Invoice($this->profile); @@ -101,6 +110,15 @@ public function initDocument($invoiceId, DateTime $issueDateTime, $invoiceType, $this->invoice->setDelivery($this->delivery); } + public function setPaymentTerms(DateTime $dueDate, ?string $description = null) + { + $this->invoice->setDueDate($dueDate); + if ($description) { + $payment = $this->invoice->getPayment(); + $payment->setTerms($description) ; + } + } + public function setSeller(string $id, InternationalCodeDesignator $idType, string $name, $tradingName = null) { $this->seller = new Party(); @@ -191,24 +209,25 @@ public function addPaymentMean(PaymentMeansCode $typeCode, ?string $ibanId = nul $payment->addTransfer($transfer); } - public function addEmbeddedAttachment( ?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description ){ + public function addEmbeddedAttachment(?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description) + { // not implemented $embeddedAttachment = new Attachment(); - if($id){ - $embeddedAttachment->setId( new Identifier($id, $scheme) ); + if ($id) { + $embeddedAttachment->setId(new Identifier($id, $scheme)); } - if($filename){ - $embeddedAttachment->setFilename($filename ); + if ($filename) { + $embeddedAttachment->setFilename($filename); } - if($contents){ - $embeddedAttachment->setContents($contents ); + if ($contents) { + $embeddedAttachment->setContents($contents); } - if($mimeCode){ - $embeddedAttachment->setMimeCode($mimeCode ); + if ($mimeCode) { + $embeddedAttachment->setMimeCode($mimeCode); } - if($description){ - $embeddedAttachment->setDescription($description ); + if ($description) { + $embeddedAttachment->setDescription($description); } $this->invoice->addAttachment($embeddedAttachment); } -} \ No newline at end of file +} diff --git a/src/XmlGenerator.php b/src/XmlGenerator.php index 7725f23..b46c754 100644 --- a/src/XmlGenerator.php +++ b/src/XmlGenerator.php @@ -2,42 +2,58 @@ namespace DigitalInvoice; - require_once __DIR__ . '/Types.php'; -interface XmlGeneratorInterface { - public function validate(string $xml , $schematron); - public function initDocument( $invoiceId , \DateTime $issueDateTime, $invoiceType, ?\DateTime $deliveryDate=null ); +interface XmlGeneratorInterface +{ + public function validate(string $xml, $schematron); + + public function initDocument($invoiceId, \DateTime $issueDateTime, $invoiceType, ?\DateTime $deliveryDate = null); + public function setSeller(string $id, InternationalCodeDesignator $idType, string $name, $tradingName = null); - public function setSellerContact( ?string $personName = null, ?string $telephone = null, ?string $email = null, ?string $departmentName = null); + + public function setSellerContact(?string $personName = null, ?string $telephone = null, ?string $email = null, ?string $departmentName = null); + public function setSellerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null); + public function setSellerTaxRegistration(string $id, string $schemeID); + public function setBuyer(string $buyerReference, string $name, string $id = null); + public function createAddress(string $postCode, string $city, string $countryCode, string $lineOne, ?string $lineTwo = null, ?string $lineThree = null); + public function getXml(); + public function setBuyerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null); - public function addItem(string $name, float $price, float $taxRatePercent, float $quantity, UnitOfMeasurement $unit, ?string $globalID = null, ?string $globalIDCode =null ): float; + + public function addItem(string $name, float $price, float $taxRatePercent, float $quantity, UnitOfMeasurement $unit, ?string $globalID = null, ?string $globalIDCode = null): float; + public function addNote(string $content, ?string $subjectCode = null, ?string $contentCode = null); - public function addPaymentMean(PaymentMeansCode $typeCode , ?string $ibanId = null,?string $accountName = null, ?string $bicId = null); - public function addEmbeddedAttachment( ?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description ); + + public function addPaymentMean(PaymentMeansCode $typeCode, ?string $ibanId = null, ?string $accountName = null, ?string $bicId = null); + + public function addEmbeddedAttachment(?string $id, ?string $scheme, ?string $filename, ?string $contents, ?string $mimeCode, ?string $description); + + public function setPaymentTerms(\DateTime $dueDate, ?string $description = null); } -abstract class XmlGenerator implements XmlGeneratorInterface { +abstract class XmlGenerator implements XmlGeneratorInterface +{ protected $profile; protected float $totalBasis; protected float $tax; - - + + public $invoice; public $currency; public mixed $seller; public mixed $buyer; public const LEVELS = []; - // Some common constants for + // Some common constants for public const DATE_102 = 'Ymd'; public const DATE_610 = 'Ym'; public const DATE_616 = 'YW'; diff --git a/src/Zugferd.php b/src/Zugferd.php index f983ff2..1852a55 100644 --- a/src/Zugferd.php +++ b/src/Zugferd.php @@ -2,16 +2,15 @@ namespace DigitalInvoice; - use Easybill\ZUGFeRD\Builder; use Easybill\ZUGFeRD\Model\Address; //use Easybill\ZUGFeRD\Model\AllowanceCharge; use Easybill\ZUGFeRD\Model\Date; use Easybill\ZUGFeRD\Model\Document; use Easybill\ZUGFeRD\Model\Note; -use Easybill\ZUGFeRD\Model\Trade\Amount; +use Easybill\ZUGFeRD\Model\Schema; use Easybill\ZUGFeRD\Model\Trade\Agreement; -use Easybill\ZUGFeRD\Model\Trade\BillingPeriod; +use Easybill\ZUGFeRD\Model\Trade\Amount; use Easybill\ZUGFeRD\Model\Trade\CreditorFinancialAccount; use Easybill\ZUGFeRD\Model\Trade\CreditorFinancialInstitution; use Easybill\ZUGFeRD\Model\Trade\Delivery; @@ -27,27 +26,20 @@ use Easybill\ZUGFeRD\Model\Trade\MonetarySummation; use Easybill\ZUGFeRD\Model\Trade\PaymentMeans; use Easybill\ZUGFeRD\Model\Trade\PaymentTerms; -use Easybill\ZUGFeRD\Model\Trade\ReferencedDocument; use Easybill\ZUGFeRD\Model\Trade\Settlement; use Easybill\ZUGFeRD\Model\Trade\Tax\TaxRegistration; use Easybill\ZUGFeRD\Model\Trade\Tax\TradeTax; use Easybill\ZUGFeRD\Model\Trade\Trade; -use Easybill\ZUGFeRD\Model\Trade\TradeParty; -use Easybill\ZUGFeRD\Model\Schema; -use Easybill\ZUGFeRD\Model\Trade\TradeCountry; -use Easybill\ZUGFeRD\Model\Trade\SpecifiedLogisticsServiceCharge; use Easybill\ZUGFeRD\Model\Trade\TradeContact; +use Easybill\ZUGFeRD\Model\Trade\TradeParty; use Easybill\ZUGFeRD\Model\Trade\UniversalCommunication; use Easybill\ZUGFeRD\SchemaValidator; - - - - -class Zugferd extends XmlGenerator { +class Zugferd extends XmlGenerator +{ public const ZUGFERD_CONFORT = Document::TYPE_COMFORT; - public const ZUGFERD_BASIC = Document::TYPE_BASIC; - public const ZUGFERD_EXTENDED = Document::TYPE_EXTENDED; + public const ZUGFERD_BASIC = Document::TYPE_BASIC; + public const ZUGFERD_EXTENDED = Document::TYPE_EXTENDED; public const LEVEL_MINIMUM = 0; @@ -58,8 +50,8 @@ class Zugferd extends XmlGenerator { public Delivery $delivery ; public Settlement $settlement ; - - public function validate(string $xml , $schematron) { + public function validate(string $xml, $schematron) + { try { libxml_use_internal_errors(true); libxml_clear_errors(); @@ -67,24 +59,27 @@ public function validate(string $xml , $schematron) { if ($isValid) { return null; } + return implode("\n", array_column(libxml_get_errors(), 'message')); } finally { libxml_use_internal_errors(false); libxml_clear_errors(); } } - public function initDocument( $invoiceId , \DateTime $issueDateTime, $invoiceType, ?\DateTime $deliveryDate=null ){ + + public function initDocument($invoiceId, \DateTime $issueDateTime, $invoiceType, ?\DateTime $deliveryDate = null) + { $this->invoice = new Document($this->profile); $this->header = $this->invoice->getHeader(); - + $this->header->setId($invoiceId) ->setDate(new Date($issueDateTime, 102)) ; $this->trade = $this->invoice->getTrade(); $this->agreement = $this->trade->getAgreement(); - - if ($deliveryDate == null ) { + + if ($deliveryDate == null) { $deliveryDate = clone $issueDateTime; } else { $this->hasDelivery = false; @@ -92,17 +87,30 @@ public function initDocument( $invoiceId , \DateTime $issueDateTime, $invoiceTy $this->delivery = new Delivery($deliveryDate->format(self::DATE_102), 102); $this->settlement = new Settlement('', $this->currency->value); $this->trade - ->setSettlement( $this->settlement) - ->setDelivery($this->delivery ); - - + ->setSettlement($this->settlement) + ->setDelivery($this->delivery); } - protected function getAmount(float $amount) : Amount { - return new Amount(self::decimalFormat($amount) , $this->currency->value ); + + public function setPaymentTerms(\DateTime $dueDate, ?string $description = null) + { + $this->settlement->setPaymentTerms(new PaymentTerms((string) $description, new Date($dueDate, 102))); } + + protected function getAmount(float $amount): Amount + { + return new Amount(self::decimalFormat($amount), $this->currency->value); + } + + protected $calculated = false; + protected function calculateTotals() { - if(count($this->taxLines)) { + // we don't have any mean to reset Settlement Tax Traded + // so we bail if already calculated + if ($this->calculated) { + return; + } + if (count($this->taxLines)) { $totalBasis = 0; $tax = 0; foreach ($this->taxLines as $rate => $items) { @@ -111,16 +119,15 @@ protected function calculateTotals() $tradeTax->setCode(TaxTypeCodeContent::VAT->value); $tradeTax->setCategory(VatCategory::STANDARD->value); $totalBasis += $sum; - $tradeTax->setBasisAmount( $this->getAmount($sum)); + $tradeTax->setBasisAmount($this->getAmount($sum)); $tradeTax->setPercent(self::decimalFormat($rate)) ; $tax += $calculated = $sum * $rate / 100; - $tradeTax->setCalculatedAmount( $this->getAmount($calculated) ); + $tradeTax->setCalculatedAmount($this->getAmount($calculated)); $this->settlement->addTradeTax($tradeTax); } } else { - - if(!isset($this->totalBasis)) { + if (! isset($this->totalBasis)) { throw new \Exception('You should call setPrice to set taxBasisTotal and taxTotal'); } @@ -130,114 +137,123 @@ protected function calculateTotals() $grand = $totalBasis + $tax ; - $summation = new MonetarySummation($totalBasis, 0.00, 0.00, $totalBasis, $tax , $grand, $this->currency->value); - $summation->setDuePayableAmount( $this->getAmount( $grand)); + $summation = new MonetarySummation($totalBasis, 0.00, 0.00, $totalBasis, $tax, $grand, $this->currency->value); + $summation->setDuePayableAmount($this->getAmount($grand)); $this->settlement->setMonetarySummation($summation); - - + $this->calculated = true; } - public function setSeller(string $id, InternationalCodeDesignator $idType, - string $name, - $tradingName = null) - { + public function setSeller( + string $id, + InternationalCodeDesignator $idType, + string $name, + $tradingName = null + ) { $this->seller = new TradeParty( $name, new Address(), // to be filled later - [ ], // Tax registration to be filled later + [ ], // Tax registration to be filled later ); $this->seller->setId($id); - $this->seller->setGlobalId(new Schema($idType->value,$id)); + $this->seller->setGlobalId(new Schema($idType->value, $id)); $this->agreement->setSeller($this->seller); - } - public function setSellerContact( ?string $personName = null, ?string $telephone = null, ?string $email = null, ?string $departmentName = null) + public function setSellerContact(?string $personName = null, ?string $telephone = null, ?string $email = null, ?string $departmentName = null) { $this->seller->definedTradeContact = new TradeContact( $personName, $departmentName, - $telephone ? new UniversalCommunication( $telephone ) : null, + $telephone ? new UniversalCommunication($telephone) : null, null, // no fax - $email ? new UniversalCommunication(null, $email) : null + $email ? new UniversalCommunication(null, $email) : null ); - } public function setSellerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null) { - $this->seller->setAddress( $this->createAddress( $postCode, $city, $countryCode, $lineOne, $lineTwo , $lineThree ) ); + $this->seller->setAddress($this->createAddress($postCode, $city, $countryCode, $lineOne, $lineTwo, $lineThree)); } - public function setSellerTaxRegistration(string $id, string $schemeID) { + public function setSellerTaxRegistration(string $id, string $schemeID) + { // todo - $this->seller->addTaxRegistration(new TaxRegistration($schemeID,$id) ); + $this->seller->addTaxRegistration(new TaxRegistration($schemeID, $id)); } - public function setBuyer(string $buyerReference, string $name, string $id = null){ + public function setBuyer(string $buyerReference, string $name, string $id = null) + { $this->buyer = new TradeParty( $name, new Address(), // to be filled later ); - if($id) { + if ($id) { $this->buyer->setId($id); } $this->agreement ->setBuyer($this->buyer) ->setBuyerReference($buyerReference); - if ($this->hasDelivery){ + if ($this->hasDelivery) { $this->delivery->setShipToTradeParty($this->buyer); } } - public function getXml(){ + + public function getXml() + { $this->calculateTotals(); $builder = Builder::create(); - return $builder->getXML($this->invoice); + return $builder->getXML($this->invoice); } - public function createAddress(string $postCode, string $city, string $countryCode, string $lineOne, ?string $lineTwo = null, ?string $lineThree = null){ + + public function createAddress(string $postCode, string $city, string $countryCode, string $lineOne, ?string $lineTwo = null, ?string $lineThree = null) + { $address = new Address(); // to be filled later - + $address->setPostcode($postCode) ; $address->setLineOne($lineOne) ; $address->setCity($city); - if ($lineThree && $lineTwo){ + if ($lineThree && $lineTwo) { $lineTwo .= " ". $lineThree; } - $address->setLineTwo( $lineTwo ) ; - $address->setCountryCode( $countryCode); + $address->setLineTwo($lineTwo) ; + $address->setCountryCode($countryCode); + return $address; } - public function setBuyerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null){ - $this->buyer->setAddress( $this->createAddress( $postCode, $city, $countryCode, $lineOne, $lineTwo , $lineThree ) ); + + public function setBuyerAddress(string $lineOne, string $postCode, string $city, string $countryCode, ?string $lineTwo = null, ?string $lineThree = null) + { + $this->buyer->setAddress($this->createAddress($postCode, $city, $countryCode, $lineOne, $lineTwo, $lineThree)); } - public function addItem(string $name, float $price, float $taxRatePercent, float $quantity , UnitOfMeasurement $unit , ?string $globalID = null, string $globalIDCode = null): float + + public function addItem(string $name, float $price, float $taxRatePercent, float $quantity, UnitOfMeasurement $unit, ?string $globalID = null, string $globalIDCode = null): float { $lineNumber = count($this->items) + 1; $tradeAgreement = new SpecifiedTradeAgreement(); - $grossPrice = new Price( $price, $this->currency->value, false); + $grossPrice = new Price($price, $this->currency->value, false); $grossPrice->setQuantity(new Quantity($unit->value, 1)); $tradeAgreement->setGrossPrice($grossPrice); - - $grossNetPrice = new Price($price,$this->currency->value, false); + + $grossNetPrice = new Price($price, $this->currency->value, false); $grossNetPrice->setQuantity(new Quantity($unit->value, 1)); $tradeAgreement->setNetPrice($grossNetPrice); $lineItemSettlement = new SpecifiedTradeSettlement(); - + $lineItemTradeTax = new TradeTax(); $lineItemTradeTax->setCode(TaxTypeCodeContent::VAT->value); - $lineItemTradeTax->setCategory( VatCategory::STANDARD->value); + $lineItemTradeTax->setCategory(VatCategory::STANDARD->value); $lineItemTradeTax->setPercent($taxRatePercent); $totalPrice = $price * $quantity; - + $lineItemSettlement ->setTradeTax($lineItemTradeTax) ->setMonetarySummation(new SpecifiedTradeMonetarySummation($totalPrice)); - + $product = new Product($globalID, $name); $lineItem = new LineItem(); @@ -251,23 +267,24 @@ public function addItem(string $name, float $price, float $taxRatePercent, float return $totalPrice; } + public function addNote(string $content, ?string $subjectCode = null, ?string $contentCode = null) { - $this->header->addNote( new Note($content, $subjectCode)); + $this->header->addNote(new Note($content, $subjectCode)); } - public function addPaymentMean(PaymentMeansCode $typeCode , ?string $ibanId = null,?string $accountName = null, ?string $bicId = null){ + public function addPaymentMean(PaymentMeansCode $typeCode, ?string $ibanId = null, ?string $accountName = null, ?string $bicId = null) + { - $this->settlement = new Settlement( '' , $this->currency); // should we send a payment reference? - $mean = new PaymentMeans(); - $mean->setCode( $typeCode->value ) ; - - // $mean->information = 'get info from type code??'; - $mean->setPayeeAccount(new CreditorFinancialAccount($ibanId, $accountName, null)) ; - if ($bicId){ - $mean->setPayeeInstitution( new CreditorFinancialInstitution($bicId, null,null)) ; - } - $this->settlement->setPaymentMeans( $mean); + $this->settlement = new Settlement('', $this->currency); // should we send a payment reference? + $mean = new PaymentMeans(); + $mean->setCode($typeCode->value) ; + // $mean->information = 'get info from type code??'; + $mean->setPayeeAccount(new CreditorFinancialAccount($ibanId, $accountName, null)) ; + if ($bicId) { + $mean->setPayeeInstitution(new CreditorFinancialInstitution($bicId, null, null)) ; + } + $this->settlement->setPaymentMeans($mean); } } diff --git a/test/InvoiceTest.php b/test/InvoiceTest.php index a45f498..82e70e2 100644 --- a/test/InvoiceTest.php +++ b/test/InvoiceTest.php @@ -74,16 +74,16 @@ public function profilesProvider() [Ubl::CIUS_IT, false, true], [Ubl::CIUS_ES_FACE, false, true], [Ubl::CIUS_AT_GOV, false, true], - [Ubl::CIUS_AT_NAT, false, true] + [Ubl::CIUS_AT_NAT, false, true], ]; } /** * @dataProvider profilesProvider */ - public function testInvoiceXml($profile, $isPdf, $embedPdf = false ): void + public function testInvoiceXml($profile, $isPdf, $embedPdf = false): void { - $invoice = new Invoice('123', new \Datetime('2023-11-07'), null , CurrencyCode::EURO , $profile); + $invoice = new Invoice('123', new \Datetime('2023-11-07'), null, CurrencyCode::EURO, $profile); $invoice->setSeller( '12344', @@ -117,16 +117,23 @@ public function testInvoiceXml($profile, $isPdf, $embedPdf = false ): void 'FR' ); - $invoice->setPrice(100, 20); - // Item 1 - $invoice->addItem('service a la demande', 750, 10, 0, 'DAY', 'xxxx') ; + if (in_array($profile, [FacturX::MINIMUM ,FacturX::BASIC_WL ])) { + $invoice->setPrice(750, 20); + } else { + // Item 1 + $invoice->addItem('service a la demande', 750, 20, 1, 'DAY', 'xxxx') ; + } + // add payment - $invoice->addPaymentMean('58','MA2120300000000202051' , 'Youniwemi'); - + $invoice->addPaymentMean('58', 'MA2120300000000202051', 'Youniwemi'); + + // set payment terms + $invoice->setPaymentTerms(new \Datetime('2023-12-07'), 'After A Month'); + // Embedding pdf - if ($embedPdf){ + if ($embedPdf) { $pdfFile = file_get_contents(__DIR__.'/examples/basic.pdf'); $invoice->addEmbeddedAttachment('123', null, 'basic', $pdfFile, 'application/pdf', 'The pdf invoice'); } @@ -139,13 +146,17 @@ public function testInvoiceXml($profile, $isPdf, $embedPdf = false ): void $result = $invoice->validate($xml); self::assertNull($result, $result ? $result."\nIN\n".$xml : ''); - if ($isPdf){ + if ($isPdf) { // This will for a more thorough validation $pdfFile = file_get_contents(__DIR__.'/examples/basic.pdf'); - $result = $invoice->getPdf($pdfFile, true); - + $addLogo = in_array($profile, [FacturX::MINIMUM ,FacturX::BASIC_WL, FacturX::BASIC, FacturX::EN16931, FacturX::EXTENDED]); + $result = $invoice->getPdf($pdfFile, $addLogo); + $profile = explode(":", $profile); + $short = array_pop($profile); + file_put_contents(__DIR__.'/examples/basic-'.$short.'.pdf', $result); // Check xml again $facturX = new PdfWriter(); + try { $xml = $facturX->getFacturxXmlFromPdf($result); $this->assertTrue(true); @@ -156,6 +167,6 @@ public function testInvoiceXml($profile, $isPdf, $embedPdf = false ): void // A complete validation using schematron $result = $invoice->validate($xml, true); - $this->assertEmpty($result, $result ? print_r($result,true) ."\n".$xml : ''); + $this->assertEmpty($result, $result ? print_r($result, true) ."\n".$xml : ''); } } diff --git a/test/TypesTest.php b/test/TypesTest.php index b46c15f..c9f3d9e 100644 --- a/test/TypesTest.php +++ b/test/TypesTest.php @@ -3,17 +3,13 @@ namespace DigitalInvoice\Tests; use DigitalInvoice\Types; - - use PHPUnit\Framework\TestCase; class TypesTest extends TestCase { - public function testInternationnalCodes() { $codes = Types::getInternationalCodes(); $this->assertEquals('SIRET CODE', $codes['0009']); } - }