company = $this->document->company; $this->client = $this->document->client; $profile = $this->client->getSetting('e_invoice_type'); $profile = match ($profile) { "XInvoice_3_0" => ZugferdProfiles::PROFILE_XRECHNUNG_3, "XInvoice_2_3" => ZugferdProfiles::PROFILE_XRECHNUNG_2_3, "XInvoice_2_2" => ZugferdProfiles::PROFILE_XRECHNUNG_2_2, "XInvoice_2_1" => ZugferdProfiles::PROFILE_XRECHNUNG_2_1, "XInvoice_2_0" => ZugferdProfiles::PROFILE_XRECHNUNG_2, "XInvoice_1_0" => ZugferdProfiles::PROFILE_XRECHNUNG, "XInvoice-Extended" => ZugferdProfiles::PROFILE_EXTENDED, "XInvoice-BasicWL" => ZugferdProfiles::PROFILE_BASICWL, "XInvoice-Basic" => ZugferdProfiles::PROFILE_BASIC, default => ZugferdProfiles::PROFILE_EN16931, }; $this->xdocument = ZugferdDocumentBuilder::CreateNew($profile); $this->bootFlags() ->setBaseDocument() ->setDocumentInformation() ->setPoNumber() ->setRoutingNumber() ->setDeliveryAddress() ->setDocumentTaxes() // 1. First set taxes ->setPaymentMeans() // 2. Then payment means ->setPaymentTerms() // 3. Then payment terms ->setLineItems() // 4. Then line items ->setCustomSurcharges() // 4a. Surcharges ->setDocumentSummation(); // 5. Finally document summation return $this; } private function setCustomSurcharges(): self { $item = $this->calc->getTaxMap()->first() ?: ['tax_rate' => 0, 'tax_id' => null]; $tax_code = $item['tax_id'] ? $this->getTaxType($item["tax_id"] ?? '2') : $this->tax_code; if ($this->document->custom_surcharge1 > 0) { $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge1 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge1; $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge')); } if ($this->document->custom_surcharge2 > 0) { $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge2 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge2; $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge')); } if ($this->document->custom_surcharge3 > 0) { $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge3 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge3; $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge')); } if ($this->document->custom_surcharge4 > 0) { $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge4 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge4; $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"],null,null,null,null,null,null, ctrans('texts.surcharge')); } return $this; } /** * setDocumentTaxes * * VATEX-EU-143 - Article 143 - Exemptions on importation * VATEX-EU-146 - Article 146 - Exemptions on exportation * VATEX-EU-148 - Article 148 - Exemptions for international transport * VATEX-EU-151 - Article 151 - Exemptions for certain transactions * VATEX-EU-169 - Article 169 - Right of deduction * VATEX-EU-AE - Reverse charge - VAT to be paid by the recipient * VATEX-EU-D - Triangulation rule - Intra-EU supply * VATEX-EU-F - Free export item, tax not charged * VATEX-EU-G - Export outside the EU * VATEX-EU-IC - Intra-Community supply * VATEX-EU-O - Outside scope of tax * VATEX-EU-IC-SC - Intra-Community supply of services to customer in another member state * VATEX-EU-AE-SC - Services to customer outside the EU * VATEX-EU-NOT-TAX - Not subject to VAT * * @return self */ private function setDocumentTaxes(): self { if ($this->document->total_taxes == 0) { $base_amount = 0; $tax_amount = 0; $tax_rate = 0; if (in_array($this->tax_code,[ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE, ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX])) { //reverse charge $base_amount = $this->document->amount; } $this->xdocument->addDocumentTax( $this->tax_code, "VAT", $base_amount, $tax_amount, $tax_rate, null, $this->exemption_reason_code ); if ($this->calc->getTotalDiscount() > 0) { $this->xdocument->addDocumentAllowanceCharge( $this->calc->getTotalDiscount(), false, $this->tax_code, "VAT", 0, null,null,null,null,null,null, ctrans('texts.discount') ); } return $this; } $tax_map = $this->calc->getTaxMap(); $net_subtotal = $tax_map->sum('base_amount'); $total_tax = $this->calc->getTotalTaxes(); $taxable_amount = $this->document->amount - $total_tax; //taxable amount and net subtotal should be the same $adjustment = round($taxable_amount - $net_subtotal, 2); // Process each tax rate group foreach ($tax_map as $item) { $tax_type = $this->getTaxType($item["tax_id"]); // Add tax information $this->xdocument->addDocumentTax( $tax_type, "VAT", $item["base_amount"] + $adjustment, // Taxable amount after discount $item["total"], $item["tax_rate"], $tax_type == ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES ? ctrans('texts.intracommunity_tax_info') : '' ); if ($this->calc->getTotalDiscount() > 0) { $ratio = $item["base_amount"] / $net_subtotal; $this->xdocument->addDocumentAllowanceCharge( round($this->calc->getTotalDiscount() * $ratio, 2), false, $this->getTaxType($item["tax_id"] ?? '2'), "VAT", $item["tax_rate"], null,null,null,null,null,null,ctrans('texts.discount') ); } $adjustment = 0; } return $this; } private function setPaymentTerms(): self { $this->xdocument->addDocumentPaymentTerm( ctrans("texts.xinvoice_payable", [ 'payeddue' => date_create($this->document->date ?? now()->format('Y-m-d')) ->diff(date_create($this->document->due_date ?? now()->format('Y-m-d'))) ->format("%d"), 'paydate' => $this->document->due_date ]) ); return $this; } public function getDocument() { return $this->xdocument; } public function getXml(): string { return $this->xdocument->getContent(); } private function bootFlags(): self { $this->calc = $this->document->calc(); $br = new \App\DataMapper\Tax\BaseRule(); $eu_states = $br->eu_country_codes; $item = $this->document->line_items[0] ?? null; if (is_null($item)) { return $this; } if (!in_array($this->document->client->country->iso_3166_2, $eu_states)) { $this->tax_code = ZugferdDutyTaxFeeCategories::FREE_EXPORT_ITEM_TAX_NOT_CHARGED; $exemption_reason_code = "VATEX-EU-G"; } elseif ($this->client->is_tax_exempt || $item->tax_id == '5' || $item->tax_id == '8') { $this->tax_code = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX; // $this->exemption_reason_code = "VATEX-EU-NOT-TAX"; $this->exemption_reason_code = "VATEX-EU-O"; // nlog("exemption_reason_code: {$this->exemption_reason_code}"); } elseif ($item->tax_id == '9') { //reverse charge $this->tax_code = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE; $this->exemption_reason_code = "VATEX-EU-AE"; } elseif ($item->tax_id == '10') { //intra-community $this->tax_code = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; $this->exemption_reason_code = "VATEX-EU-IC"; } else { $this->tax_code = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX; $this->exemption_reason_code = "VATEX-EU-O"; } return $this; } private function setDocumentSummation(): self { $document_discount = $this->calc->getTotalDiscount(); $total_tax = round($this->calc->getTotalTaxes(), 2); $taxable_amount = $this->document->amount - $total_tax; $base_taxable_amount = $this->calc->getTaxMap()->sum('base_amount'); $subtotal = $this->document->uses_inclusive_taxes ? ($this->calc->getTotal() - $total_tax - $this->calc->getTotalNetSurcharges() + $this->calc->getTotalDiscount()) : ($this->calc->getSubTotal()); // nlog([ // $this->document->amount, // Total amount with VAT // $this->document->balance, // Amount due // $subtotal, // Sum before tax // $this->calc->getTotalSurcharges(), // Total charges // $document_discount, // Total allowances // $taxable_amount, // Tax basis total (net) // $total_tax, // Total tax amount // 0, // // round($this->document->amount - ($base_taxable_amount+$total_tax),2), // Total prepaid amount // $this->document->amount - $this->document->balance, // ]); $this->xdocument->setDocumentSummation( $this->document->amount, // Total amount with VAT $this->document->balance, // Amount due $subtotal, // Sum before tax $this->document->uses_inclusive_taxes ? $this->calc->getTotalNetSurcharges() : $this->calc->getTotalSurcharges(), // Total charges $document_discount, // Total allowances $taxable_amount, // Tax basis total (net) round($total_tax, 2), // Total tax amount 0, // round($this->document->amount - ($base_taxable_amount+$total_tax),2), // Total rounding amount $this->document->amount - $this->document->balance // Amount already paid ); return $this; } private function setLineItems(): self { foreach ($this->document->line_items as $index => $item) { /** @var InvoiceItem $item **/ $position_id = (string) ($index + 1); // 1. Start new position and set basic details $this->xdocument->addNewPosition($position_id) ->setDocumentPositionProductDetails( strlen($item->product_key ?? '') >= 1 ? $item->product_key : "no product name defined", $item->notes ) ->setDocumentPositionQuantity( $item->quantity, $item->type_id == 2 ? "HUR" : "H87" ) ->setDocumentPositionNetPrice( $this->document->uses_inclusive_taxes ? $item->net_cost : $item->cost ); // 2. ALWAYS add tax information (even if zero) if (strlen($item->tax_name1) > 1) { $this->xdocument->addDocumentPositionTax( $this->getTaxType($item->tax_id ?? '2'), 'VAT', $item->tax_rate1 ); } else { // Add zero tax if no tax is specified $this->xdocument->addDocumentPositionTax( ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX, 'VAT', 0 ); } $line_discount = 0; // 3. Add allowances/charges (discounts) if any if ($item->discount > 0) { $line_discount = $this->calculateTotalItemDiscountAmount($item); $this->xdocument->addDocumentPositionGrossPriceAllowanceCharge( abs($line_discount), false ); } // 4. Finally add monetary summation $this->xdocument->setDocumentPositionLineSummation($this->document->uses_inclusive_taxes ? ($item->line_total - $item->tax_amount) : $item->line_total); } return $this; } private function calculateTotalItemDiscountAmount($item): float { if ($item->is_amount_discount) { return $item->discount; } return ($item->cost * $item->quantity) * ($item->discount / 100); } private function setCompanyTaxRegistration(): array { if (str_contains($this->company->getSetting('vat_number'), "/")) { return ["FC", $this->company->getSetting('vat_number')]; } return ["VA", $this->company->getSetting('vat_number')]; } private function setPaymentMeans(): self { /**Check if the e_invoice object is populated */ if (isset($this->company->e_invoice->Invoice->PaymentMeans) && ($pm = $this->company->e_invoice->Invoice->PaymentMeans[0] ?? false)) { switch ($pm->PaymentMeansCode->value ?? false) { case '30': case '58': $iban = $pm->PayeeFinancialAccount->ID->value; $name = $pm->PayeeFinancialAccount->Name ?? ''; $bic = $pm->PayeeFinancialAccount->FinancialInstitutionBranch->FinancialInstitution->ID->value ?? ''; $typecode = $pm->PaymentMeansCode->value; $this->xdocument->addDocumentPaymentMean(typeCode: $typecode, payeeIban: $iban, payeeAccountName: $name, payeeBic: $bic); return $this; default: # code... break; } } //Otherwise default to the "old style" $custom_value1 = $this->company->settings->custom_value1; //BR-DE-23 - If „Payment means type code“ (BT-81) contains a code for credit transfer (30, 58), „CREDIT TRANSFER“ (BG-17) shall be provided. //Payment Means - Switcher if (isset($custom_value1) && !empty($custom_value1) && ($custom_value1 == '30' || $custom_value1 == '58')) { $this->xdocument->addDocumentPaymentMean(typeCode: $this->company->settings->custom_value1, payeeIban: $this->company->settings->custom_value2, payeeAccountName: $this->company->settings->custom_value4, payeeBic: $this->company->settings->custom_value3); } else { $this->xdocument->addDocumentPaymentMean('68', ctrans("texts.xinvoice_online_payment")); } return $this; } private function setDeliveryAddress(): self { if (isset($this->client->shipping_address1) && $this->client->shipping_country) { $this->xdocument->setDocumentShipToAddress( $this->client->shipping_address1, $this->client->shipping_address2, "", $this->client->shipping_postal_code, $this->client->shipping_city, $this->client->shipping_country->iso_3166_2, $this->client->shipping_state ); } return $this; } private function setDocumentInformation(): self { $this->xdocument->setDocumentInformation( $this->getDocumentNumber(), $this->getDocumentType(), $this->getDocumentDate(), $this->getDocumentCurrency() ); return $this; } private function setBaseDocument(): self { $user_or_company_phone = strlen($this->company->present()->phone()) > 3 ? $this->company->present()->phone() : $this->document->user->present()->phone(); $company_tax_registration = $this->setCompanyTaxRegistration(); $this->xdocument ->setDocumentSupplyChainEvent($this->getDocumentDate()) ->setDocumentSeller($this->company->getSetting('name')) ->setDocumentSellerAddress($this->company->getSetting("address1"), $this->company->getSetting("address2"), "", $this->company->getSetting("postal_code"), $this->company->getSetting("city"), $this->company->country()->iso_3166_2, $this->company->getSetting("state")) ->setDocumentSellerContact($this->document->user->present()->getFullName(), "", $user_or_company_phone, "", $this->document->user->email) ->setDocumentSellerCommunication("EM", $this->document->user->email) ->addDocumentSellerTaxRegistration($company_tax_registration[0], $company_tax_registration[1]) ->setDocumentBuyer($this->client->present()->name(), $this->client->number) ->setDocumentBuyerAddress($this->client->address1, "", "", $this->client->postal_code, $this->client->city, $this->client->country->iso_3166_2, $this->client->state) ->setDocumentBuyerContact($this->client->present()->primary_contact_name(), "", $this->client->present()->phone(), "", $this->client->present()->email()) ->setDocumentBuyerCommunication("EM", $this->client->present()->email()) ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->document->date ?? now()->format('Y-m-d'))->diff(date_create($this->document->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->document->due_date])); if (strlen($this->client->vat_number ?? '') > 1) { $this->xdocument->addDocumentBuyerTaxRegistration($this->getDocumentLevelTaxRegistration(), $this->client->vat_number); } return $this; } private function setRoutingNumber(): self { if (empty($this->client->routing_id)) { $this->xdocument->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); } else { $this->xdocument->setDocumentBuyerReference($this->client->routing_id) ->setDocumentBuyerCommunication("0204", $this->client->routing_id); } return $this; } private function setPoNumber(): self { if (isset($this->document->po_number) && strlen($this->document->po_number) > 1) { $this->xdocument->setDocumentBuyerOrderReferencedDocument($this->document->po_number); } return $this; } //////////////////Getters////////////////// private function getDocumentNumber(): string { return empty($this->document->number) ? "DRAFT" : $this->document->number; } private function getDocumentType(): string { return match (get_class($this->document)) { Quote::class => ZugferdDocumentType::CONTRACT_PRICE_QUOTE, Invoice::class => ZugferdDocumentType::COMMERCIAL_INVOICE, Credit::class => ZugferdDocumentType::CREDIT_NOTE, default => ZugferdDocumentType::COMMERCIAL_INVOICE, }; } private function getDocumentDate(): DateTime { return date_create($this->document->date ?? now()->format('Y-m-d')); } private function getDocumentCurrency(): string { return $this->client->getCurrencyCode(); } private function getDocumentLevelTaxRegistration(): string { return strlen($this->client->vat_number ?? '') > 1 ? "VA" : "FC"; } private function getTaxType(string $tax_id): string { switch ($tax_id) { case Product::PRODUCT_TYPE_SERVICE: case Product::PRODUCT_TYPE_DIGITAL: case Product::PRODUCT_TYPE_PHYSICAL: case Product::PRODUCT_TYPE_SHIPPING: case Product::PRODUCT_TYPE_REDUCED_TAX: $tax_type = ZugferdDutyTaxFeeCategories::STANDARD_RATE; break; case Product::PRODUCT_TYPE_EXEMPT: $tax_type = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX; break; case Product::PRODUCT_TYPE_ZERO_RATED: $tax_type = ZugferdDutyTaxFeeCategories::ZERO_RATED_GOODS; break; case Product::PRODUCT_TYPE_REVERSE_TAX: $tax_type = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE; break; default: $tax_type = null; break; } if ($this->client->is_tax_exempt) { $tax_type = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX; } $br = new \App\DataMapper\Tax\BaseRule(); $eu_states = $br->eu_country_codes; if (empty($tax_type)) { if ((in_array($this->company->country()->iso_3166_2, $eu_states) && in_array($this->client->country->iso_3166_2, $eu_states)) && $this->company->country()->iso_3166_2 != $this->client->country->iso_3166_2) { $tax_type = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; } elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) { $tax_type = ZugferdDutyTaxFeeCategories::FREE_EXPORT_ITEM_TAX_NOT_CHARGED; } elseif ($this->document->client->country->iso_3166_2 == "ES-CN") { $tax_type = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; } elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { $tax_type = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA; } else { // nlog("Unkown tax case for xinvoice"); $tax_type = ZugferdDutyTaxFeeCategories::STANDARD_RATE; } } return $tax_type; } }