mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Merge pull request #8368 from LarsK1/v5-develop
Support for XRechnung / ZUGFeRD / e-Factur
This commit is contained in:
commit
d742e7b42f
3
.gitignore
vendored
3
.gitignore
vendored
@ -37,4 +37,5 @@ public/test.pdf
|
|||||||
public/storage/test.pdf
|
public/storage/test.pdf
|
||||||
/Modules
|
/Modules
|
||||||
_ide_helper_models.php
|
_ide_helper_models.php
|
||||||
_ide_helper.php
|
_ide_helper.php
|
||||||
|
/composer.phar
|
||||||
|
@ -175,6 +175,9 @@ class SendRemindersCron extends Command
|
|||||||
$invoice->calc()->getInvoice()->save();
|
$invoice->calc()->getInvoice()->save();
|
||||||
$invoice->fresh();
|
$invoice->fresh();
|
||||||
$invoice->service()->deletePdf()->save();
|
$invoice->service()->deletePdf()->save();
|
||||||
|
if ($invoice->company->enable_e_invoice){
|
||||||
|
$invoice->service()->deleteEInvoice()->save();
|
||||||
|
}
|
||||||
|
|
||||||
/* Refresh the client here to ensure the balance is fresh */
|
/* Refresh the client here to ensure the balance is fresh */
|
||||||
$client = $invoice->client;
|
$client = $invoice->client;
|
||||||
|
@ -295,6 +295,11 @@ class InvoiceSum
|
|||||||
return $this->total;
|
return $this->total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTotalSurcharges()
|
||||||
|
{
|
||||||
|
return $this->total_custom_values;
|
||||||
|
}
|
||||||
|
|
||||||
public function setTaxMap()
|
public function setTaxMap()
|
||||||
{
|
{
|
||||||
if ($this->invoice->is_amount_discount == true) {
|
if ($this->invoice->is_amount_discount == true) {
|
||||||
|
@ -173,6 +173,11 @@ class InvoiceSumInclusive
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTotalSurcharges()
|
||||||
|
{
|
||||||
|
return $this->total_custom_values;
|
||||||
|
}
|
||||||
|
|
||||||
public function getRecurringInvoice()
|
public function getRecurringInvoice()
|
||||||
{
|
{
|
||||||
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
|
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
|
||||||
|
@ -535,7 +535,7 @@ class InvoiceController extends BaseController
|
|||||||
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) {
|
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) {
|
||||||
return response(['message' => 'Please verify your account to send emails.'], 400);
|
return response(['message' => 'Please verify your account to send emails.'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
|
$invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
|
||||||
|
|
||||||
if (! $invoices) {
|
if (! $invoices) {
|
||||||
@ -678,7 +678,7 @@ class InvoiceController extends BaseController
|
|||||||
case 'clone_to_invoice':
|
case 'clone_to_invoice':
|
||||||
$invoice = CloneInvoiceFactory::create($invoice, auth()->user()->id);
|
$invoice = CloneInvoiceFactory::create($invoice, auth()->user()->id);
|
||||||
return $this->itemResponse($invoice);
|
return $this->itemResponse($invoice);
|
||||||
|
|
||||||
case 'clone_to_quote':
|
case 'clone_to_quote':
|
||||||
$quote = CloneInvoiceToQuoteFactory::create($invoice, auth()->user()->id);
|
$quote = CloneInvoiceToQuoteFactory::create($invoice, auth()->user()->id);
|
||||||
|
|
||||||
@ -860,6 +860,73 @@ class InvoiceController extends BaseController
|
|||||||
}, basename($file), $headers);
|
}, basename($file), $headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/invoice/{invitation_key}/download_xinvoice",
|
||||||
|
* operationId="downloadXInvoice",
|
||||||
|
* tags={"invoices"},
|
||||||
|
* summary="Download a specific x-invoice by invitation key",
|
||||||
|
* description="Downloads a specific x-invoice",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="invitation_key",
|
||||||
|
* in="path",
|
||||||
|
* description="The Invoice Invitation Key",
|
||||||
|
* example="D2J234DFA",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(
|
||||||
|
* type="string",
|
||||||
|
* format="string",
|
||||||
|
* ),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the x-invoice pdf",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
*
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
* @param $invitation_key
|
||||||
|
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||||
|
*/
|
||||||
|
public function downloadEInvoice($invitation_key)
|
||||||
|
{
|
||||||
|
$invitation = $this->invoice_repo->getInvitationByKey($invitation_key);
|
||||||
|
|
||||||
|
if (! $invitation) {
|
||||||
|
return response()->json(['message' => 'no record found'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contact = $invitation->contact;
|
||||||
|
$invoice = $invitation->invoice;
|
||||||
|
|
||||||
|
$file = $invoice->service()->getEInvoice($contact);
|
||||||
|
|
||||||
|
$headers = ['Content-Type' => 'application/xml'];
|
||||||
|
|
||||||
|
if (request()->input('inline') == 'true') {
|
||||||
|
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->streamDownload(function () use ($file) {
|
||||||
|
echo Storage::get($file);
|
||||||
|
}, basename($file), $headers);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Get(
|
* @OA\Get(
|
||||||
* path="/api/v1/invoices/{id}/delivery_note",
|
* path="/api/v1/invoices/{id}/delivery_note",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace App\Jobs\Entity;
|
namespace App\Jobs\Entity;
|
||||||
|
|
||||||
use App\Exceptions\FilePermissionsFailure;
|
use App\Exceptions\FilePermissionsFailure;
|
||||||
|
use App\Jobs\Invoice\CreateXInvoice;
|
||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
use App\Models\Credit;
|
use App\Models\Credit;
|
||||||
use App\Models\CreditInvitation;
|
use App\Models\CreditInvitation;
|
||||||
@ -211,7 +212,9 @@ class CreateEntityPdf implements ShouldQueue
|
|||||||
throw new FilePermissionsFailure($e->getMessage());
|
throw new FilePermissionsFailure($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($this->entity_string == "invoice" && $this->company->enable_e_invoice){
|
||||||
|
(new CreateXInvoice($this->entity, true))->handle();
|
||||||
|
}
|
||||||
$this->invitation = null;
|
$this->invitation = null;
|
||||||
$this->entity = null;
|
$this->entity = null;
|
||||||
$this->company = null;
|
$this->company = null;
|
||||||
@ -219,7 +222,8 @@ class CreateEntityPdf implements ShouldQueue
|
|||||||
$this->contact = null;
|
$this->contact = null;
|
||||||
$maker = null;
|
$maker = null;
|
||||||
$state = null;
|
$state = null;
|
||||||
|
|
||||||
|
|
||||||
return $file_path;
|
return $file_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
240
app/Jobs/Invoice/CreateXInvoice.php
Normal file
240
app/Jobs/Invoice/CreateXInvoice.php
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Invoice;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Product;
|
||||||
|
use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories;
|
||||||
|
use horstoeko\zugferd\ZugferdDocumentBuilder;
|
||||||
|
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
|
||||||
|
use horstoeko\zugferd\ZugferdProfiles;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
|
||||||
|
class CreateXInvoice implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
private Invoice $invoice;
|
||||||
|
private bool $alterpdf;
|
||||||
|
private string $custompdfpath;
|
||||||
|
|
||||||
|
public function __construct(Invoice $invoice, bool $alterPDF, string $custompdfpath = "")
|
||||||
|
{
|
||||||
|
$this->invoice = $invoice;
|
||||||
|
$this->alterpdf = $alterPDF;
|
||||||
|
$this->custompdfpath = $custompdfpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function handle(): string
|
||||||
|
{
|
||||||
|
$invoice = $this->invoice;
|
||||||
|
$company = $invoice->company;
|
||||||
|
$client = $invoice->client;
|
||||||
|
$profile = "";
|
||||||
|
switch ($company->e_invoice_type) {
|
||||||
|
case "EN16931":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_EN16931;
|
||||||
|
break;
|
||||||
|
case "XInvoice_2_2":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_XRECHNUNG_2_2;
|
||||||
|
break;
|
||||||
|
case "XInvoice_2_1":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_XRECHNUNG_2_1;
|
||||||
|
break;
|
||||||
|
case "XInvoice_2_0":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_XRECHNUNG_2;
|
||||||
|
break;
|
||||||
|
case "XInvoice_1_0":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_XRECHNUNG;
|
||||||
|
break;
|
||||||
|
case "XInvoice-Extended":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_EXTENDED;
|
||||||
|
break;
|
||||||
|
case "XInvoice-BasicWL":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_BASICWL;
|
||||||
|
break;
|
||||||
|
case "XInvoice-Basic":
|
||||||
|
$profile = ZugferdProfiles::PROFILE_BASIC;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$xrechnung = ZugferdDocumentBuilder::CreateNew($profile);
|
||||||
|
|
||||||
|
$xrechnung
|
||||||
|
->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode())
|
||||||
|
->setDocumentSupplyChainEvent(date_create($invoice->date))
|
||||||
|
->setDocumentSeller($company->getSetting('name'))
|
||||||
|
->setDocumentSellerAddress($company->getSetting("address1"), $company->getSetting("address2"), "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2, $company->getSetting("state"))
|
||||||
|
->setDocumentSellerContact($invoice->user->first_name." ".$invoice->user->last_name, "", $invoice->user->phone, "", $invoice->user->email)
|
||||||
|
->setDocumentBuyer($client->name, $client->number)
|
||||||
|
->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2)
|
||||||
|
->setDocumentBuyerReference($client->routing_id)
|
||||||
|
->setDocumentBuyerContact($client->primary_contact()->first()->first_name . " " . $client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email)
|
||||||
|
->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state)
|
||||||
|
->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($invoice->date)->diff(date_create($invoice->due_date))->format("%d"), 'paydate' => $invoice->due_date]));
|
||||||
|
if (!empty($invoice->public_notes)) {
|
||||||
|
$xrechnung->addDocumentNote($invoice->public_notes);
|
||||||
|
}
|
||||||
|
if (!empty($invoice->po_number)) {
|
||||||
|
$xrechnung->setDocumentBuyerOrderReferencedDocument($invoice->po_number);
|
||||||
|
}
|
||||||
|
if (empty($client->routing_id)){
|
||||||
|
$xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference"));
|
||||||
|
}
|
||||||
|
$xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment"));
|
||||||
|
|
||||||
|
if (str_contains($company->getSetting('vat_number'), "/")) {
|
||||||
|
$xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number'));
|
||||||
|
} else {
|
||||||
|
$xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoicingdata = $invoice->calc();
|
||||||
|
$globaltax = null;
|
||||||
|
|
||||||
|
//Create line items and calculate taxes
|
||||||
|
foreach ($invoice->line_items as $index => $item) {
|
||||||
|
$xrechnung->addNewPosition($index)
|
||||||
|
->setDocumentPositionProductDetails($item->notes)
|
||||||
|
->setDocumentPositionGrossPrice($item->gross_line_total)
|
||||||
|
->setDocumentPositionNetPrice($item->line_total);
|
||||||
|
if (isset($item->task_id)) {
|
||||||
|
$xrechnung->setDocumentPositionQuantity($item->quantity, "HUR");
|
||||||
|
} else {
|
||||||
|
$xrechnung->setDocumentPositionQuantity($item->quantity, "H87");
|
||||||
|
}
|
||||||
|
$linenetamount = $item->line_total;
|
||||||
|
if ($item->discount > 0){
|
||||||
|
if ($invoice->is_amount_discount){
|
||||||
|
$linenetamount -= $item->discount;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$linenetamount -= $linenetamount * ($item->discount / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$xrechnung->setDocumentPositionLineSummation($linenetamount);
|
||||||
|
// According to european law, each line item can only have one tax rate
|
||||||
|
if (!(empty($item->tax_name1) && empty($item->tax_name2) && empty($item->tax_name3))){
|
||||||
|
if (!empty($item->tax_name1)) {
|
||||||
|
$xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate1);
|
||||||
|
} elseif (!empty($item->tax_name2)) {
|
||||||
|
$xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate2);
|
||||||
|
} elseif (!empty($item->tax_name3)) {
|
||||||
|
$xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate3);
|
||||||
|
} else {
|
||||||
|
nlog("Can't add correct tax position");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!empty($invoice->tax_name1)) {
|
||||||
|
$globaltax = 0;
|
||||||
|
$xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name1, $invoice), 'VAT', $invoice->tax_rate1);
|
||||||
|
} elseif (!empty($invoice->tax_name2)) {
|
||||||
|
$globaltax = 1;
|
||||||
|
$xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name2, $invoice), 'VAT', $invoice->tax_rate2);
|
||||||
|
} elseif (!empty($invoice->tax_name3)) {
|
||||||
|
$globaltax = 2;
|
||||||
|
$xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name3, $invoice), 'VAT', $item->tax_rate3);
|
||||||
|
} else {
|
||||||
|
nlog("Can't add correct tax position");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($invoice->isPartial()) {
|
||||||
|
$xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, $invoice->partial);
|
||||||
|
} else {
|
||||||
|
$xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($invoicingdata->getTaxMap() as $item) {
|
||||||
|
$tax = explode(" ", $item["name"]);
|
||||||
|
$xrechnung->addDocumentTax($this->getTaxType("", $invoice), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]);
|
||||||
|
// TODO: Add correct tax type within getTaxType
|
||||||
|
}
|
||||||
|
if (!empty($globaltax)){
|
||||||
|
$tax = explode(" ", $invoicingdata->getTotalTaxMap()[$globaltax]["name"]);
|
||||||
|
$xrechnung->addDocumentTax($this->getTaxType("", $invoice), "VAT", $invoicingdata->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicingdata->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]);
|
||||||
|
// TODO: Add correct tax type within getTaxType
|
||||||
|
}
|
||||||
|
|
||||||
|
$disk = config('filesystems.default');
|
||||||
|
if (!Storage::exists($client->e_invoice_filepath($invoice->invitations->first()))) {
|
||||||
|
Storage::makeDirectory($client->e_invoice_filepath($invoice->invitations->first()));
|
||||||
|
}
|
||||||
|
$xrechnung->writeFile(Storage::disk($disk)->path($client->e_invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml")));
|
||||||
|
// The validity can be checked using https://portal3.gefeg.com/invoice/validation
|
||||||
|
|
||||||
|
if ($this->alterpdf) {
|
||||||
|
if ($this->custompdfpath != "") {
|
||||||
|
$pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $this->custompdfpath);
|
||||||
|
$pdfBuilder->generateDocument();
|
||||||
|
$pdfBuilder->saveDocument($this->custompdfpath);
|
||||||
|
} else {
|
||||||
|
$filepath_pdf = $client->invoice_filepath($invoice->invitations->first()) . $invoice->getFileName();
|
||||||
|
$file = Storage::disk($disk)->exists($filepath_pdf);
|
||||||
|
if ($file) {
|
||||||
|
$pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, Storage::disk($disk)->path($filepath_pdf));
|
||||||
|
$pdfBuilder->generateDocument();
|
||||||
|
$pdfBuilder->saveDocument(Storage::disk($disk)->path($filepath_pdf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $client->e_invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTaxType($name, Invoice $invoice): string
|
||||||
|
{
|
||||||
|
$taxtype = null;
|
||||||
|
switch ($name) {
|
||||||
|
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:
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::STANDARD_RATE;
|
||||||
|
break;
|
||||||
|
case Product::PRODUCT_TYPE_EXEMPT:
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX;
|
||||||
|
break;
|
||||||
|
case Product::PRODUCT_TYPE_ZERO_RATED:
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::ZERO_RATED_GOODS;
|
||||||
|
break;
|
||||||
|
case Product::PRODUCT_TYPE_REVERSE_TAX:
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$eu_states = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "EL", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", "CH"];
|
||||||
|
if (empty($taxtype)){
|
||||||
|
if (in_array($invoice->company->country()->iso_3166_2, $eu_states) && in_array($invoice->client->country->iso_3166_2, $eu_states)){
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES;
|
||||||
|
}
|
||||||
|
elseif (!in_array($invoice->client->country->iso_3166_2, $eu_states)){
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX;
|
||||||
|
}
|
||||||
|
elseif ($invoice->client->country->iso_3166_2 == "ES-CN"){
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX;
|
||||||
|
}
|
||||||
|
elseif (in_array($invoice->client->country->iso_3166_2, ["ES-CE", "ES-ML"])){
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nlog("Unkown tax case for xinvoice");
|
||||||
|
$taxtype = ZugferdDutyTaxFeeCategories::STANDARD_RATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $taxtype;
|
||||||
|
}
|
||||||
|
}
|
@ -78,13 +78,19 @@ class ZipInvoices implements ShouldQueue
|
|||||||
|
|
||||||
$this->invoices->each(function ($invoice) {
|
$this->invoices->each(function ($invoice) {
|
||||||
(new CreateEntityPdf($invoice->invitations()->first()))->handle();
|
(new CreateEntityPdf($invoice->invitations()->first()))->handle();
|
||||||
|
if ($this->company->use_xinvoice){
|
||||||
|
(new CreateXInvoice($invoice, false))->handle();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($this->invoices as $invoice) {
|
foreach ($this->invoices as $invoice) {
|
||||||
$file = $invoice->service()->getInvoicePdf();
|
$file = $invoice->service()->getInvoicePdf();
|
||||||
|
$xinvoice = $invoice->service()->getXInvoice();
|
||||||
$zip_file_name = basename($file);
|
$zip_file_name = basename($file);
|
||||||
$zipFile->addFromString($zip_file_name, Storage::get($file));
|
$xinvoice_zip_file_name = basename($xinvoice);
|
||||||
|
$zipFile->addFromString($zip_file_name, Storage::get($file))
|
||||||
|
->addDir($xinvoice_zip_file_name, Storage::get($xinvoice));
|
||||||
|
|
||||||
//$download_file = file_get_contents($invoice->pdf_file_path($invitation, 'url', true));
|
//$download_file = file_get_contents($invoice->pdf_file_path($invitation, 'url', true));
|
||||||
//$zipFile->addFromString(basename($invoice->pdf_file_path($invitation)), $download_file);
|
//$zipFile->addFromString(basename($invoice->pdf_file_path($invitation)), $download_file);
|
||||||
|
@ -18,6 +18,7 @@ use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
|
|||||||
use App\Utils\HtmlEngine;
|
use App\Utils\HtmlEngine;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
use Illuminate\Mail\Mailable;
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class TemplateEmail extends Mailable
|
class TemplateEmail extends Mailable
|
||||||
{
|
{
|
||||||
@ -152,6 +153,11 @@ class TemplateEmail extends Mailable
|
|||||||
$this->attachData($ubl_string, $this->invitation->invoice->getFileName('xml'));
|
$this->attachData($ubl_string, $this->invitation->invoice->getFileName('xml'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($this->invitation && $this->invitation->invoice && $company->use_xinvoice && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
|
||||||
|
$this->invitation->invoice->service()->getXInvoice($this->invitation->contact);
|
||||||
|
$disk = config('filesystems.default');
|
||||||
|
$this->attach(Storage::disk($disk)->path($this->invitation->invoice->client->xinvoice_filepath($this->invitation->invoice->invitations->first()) . $this->invitation->invoice->getFileName("xml")));
|
||||||
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ use Laracasts\Presenter\PresentableTrait;
|
|||||||
* @property string|null $client_hash
|
* @property string|null $client_hash
|
||||||
* @property string|null $logo
|
* @property string|null $logo
|
||||||
* @property string|null $phone
|
* @property string|null $phone
|
||||||
|
* @property string|null routing_id
|
||||||
* @property string $balance
|
* @property string $balance
|
||||||
* @property string $paid_to_date
|
* @property string $paid_to_date
|
||||||
* @property string $credit_balance
|
* @property string $credit_balance
|
||||||
@ -400,6 +401,7 @@ class Client extends BaseModel implements HasLocalePreference
|
|||||||
'public_notes',
|
'public_notes',
|
||||||
'phone',
|
'phone',
|
||||||
'number',
|
'number',
|
||||||
|
'routing_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $with = [
|
protected $with = [
|
||||||
@ -449,6 +451,7 @@ class Client extends BaseModel implements HasLocalePreference
|
|||||||
'id_number',
|
'id_number',
|
||||||
'public_notes',
|
'public_notes',
|
||||||
'phone',
|
'phone',
|
||||||
|
'routing_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
// public function scopeExclude($query)
|
// public function scopeExclude($query)
|
||||||
@ -910,6 +913,12 @@ class Client extends BaseModel implements HasLocalePreference
|
|||||||
|
|
||||||
return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/invoices/';
|
return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/invoices/';
|
||||||
}
|
}
|
||||||
|
public function e_invoice_filepath($invitation)
|
||||||
|
{
|
||||||
|
$contact_key = $invitation->contact->contact_key;
|
||||||
|
|
||||||
|
return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/e_invoice/';
|
||||||
|
}
|
||||||
|
|
||||||
public function quote_filepath($invitation)
|
public function quote_filepath($invitation)
|
||||||
{
|
{
|
||||||
|
@ -97,6 +97,8 @@ use Laracasts\Presenter\PresentableTrait;
|
|||||||
* @property int $stock_notification
|
* @property int $stock_notification
|
||||||
* @property string|null $matomo_url
|
* @property string|null $matomo_url
|
||||||
* @property int|null $matomo_id
|
* @property int|null $matomo_id
|
||||||
|
* @property bool $enable_e_invoice
|
||||||
|
* @property string $e_invoice_type
|
||||||
* @property int $enabled_expense_tax_rates
|
* @property int $enabled_expense_tax_rates
|
||||||
* @property int $invoice_task_project
|
* @property int $invoice_task_project
|
||||||
* @property int $report_include_deleted
|
* @property int $report_include_deleted
|
||||||
@ -837,6 +839,8 @@ class Company extends BaseModel
|
|||||||
'google_analytics_key',
|
'google_analytics_key',
|
||||||
'matomo_url',
|
'matomo_url',
|
||||||
'matomo_id',
|
'matomo_id',
|
||||||
|
'enable_e_invoice',
|
||||||
|
'e_invoice_type',
|
||||||
'client_can_register',
|
'client_can_register',
|
||||||
'enable_shop_api',
|
'enable_shop_api',
|
||||||
'invoice_task_timelog',
|
'invoice_task_timelog',
|
||||||
@ -914,7 +918,7 @@ class Company extends BaseModel
|
|||||||
|
|
||||||
public function refreshTaxData()
|
public function refreshTaxData()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function documents()
|
public function documents()
|
||||||
|
@ -123,6 +123,8 @@ class Product extends BaseModel
|
|||||||
public const PRODUCT_TYPE_EXEMPT = 5;
|
public const PRODUCT_TYPE_EXEMPT = 5;
|
||||||
public const PRODUCT_TYPE_REDUCED_TAX = 6;
|
public const PRODUCT_TYPE_REDUCED_TAX = 6;
|
||||||
public const PRODUCT_TYPE_OVERRIDE_TAX = 7;
|
public const PRODUCT_TYPE_OVERRIDE_TAX = 7;
|
||||||
|
public const PRODUCT_TYPE_ZERO_RATED = 8;
|
||||||
|
public const PRODUCT_TYPE_REVERSE_TAX = 9;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'custom_value1',
|
'custom_value1',
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
namespace App\Services\Email;
|
namespace App\Services\Email;
|
||||||
|
|
||||||
|
use App\Jobs\Invoice\CreateXInvoice;
|
||||||
|
use App\Services\Invoice\GetInvoiceXInvoice;
|
||||||
use App\DataMapper\EmailTemplateDefaults;
|
use App\DataMapper\EmailTemplateDefaults;
|
||||||
use App\Jobs\Entity\CreateRawPdf;
|
use App\Jobs\Entity\CreateRawPdf;
|
||||||
use App\Jobs\Invoice\CreateUbl;
|
use App\Jobs\Invoice\CreateUbl;
|
||||||
@ -55,7 +57,7 @@ class EmailDefaults
|
|||||||
public function __construct(protected Email $email)
|
public function __construct(protected Email $email)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for generating
|
* Entry point for generating
|
||||||
* the defaults for the email object
|
* the defaults for the email object
|
||||||
@ -78,7 +80,6 @@ class EmailDefaults
|
|||||||
->setAttachments()
|
->setAttachments()
|
||||||
->setVariables()
|
->setVariables()
|
||||||
->setHeaders();
|
->setHeaders();
|
||||||
|
|
||||||
return $this->email->email_object;
|
return $this->email->email_object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +184,6 @@ class EmailDefaults
|
|||||||
// Default template to be used
|
// Default template to be used
|
||||||
$this->email->email_object->body = EmailTemplateDefaults::getDefaultTemplate($this->email->email_object->email_template_body, $this->locale);
|
$this->email->email_object->body = EmailTemplateDefaults::getDefaultTemplate($this->email->email_object->email_template_body, $this->locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ class EmailDefaults
|
|||||||
public function setVariables(): self
|
public function setVariables(): self
|
||||||
{
|
{
|
||||||
$this->email->email_object->body = strtr($this->email->email_object->body, $this->email->email_object->variables);
|
$this->email->email_object->body = strtr($this->email->email_object->body, $this->email->email_object->variables);
|
||||||
|
|
||||||
$this->email->email_object->subject = strtr($this->email->email_object->subject, $this->email->email_object->variables);
|
$this->email->email_object->subject = strtr($this->email->email_object->subject, $this->email->email_object->variables);
|
||||||
|
|
||||||
if ($this->template != 'custom') {
|
if ($this->template != 'custom') {
|
||||||
@ -253,7 +253,7 @@ class EmailDefaults
|
|||||||
foreach ($bccs as $bcc) {
|
foreach ($bccs as $bcc) {
|
||||||
$bcc_array[] = new Address($bcc);
|
$bcc_array[] = new Address($bcc);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->email->email_object->bcc = array_merge($this->email->email_object->bcc, $bcc_array);
|
$this->email->email_object->bcc = array_merge($this->email->email_object->bcc, $bcc_array);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -267,7 +267,7 @@ class EmailDefaults
|
|||||||
return $this;
|
return $this;
|
||||||
// return $this->email->email_object->cc;
|
// return $this->email->email_object->cc;
|
||||||
// return [
|
// return [
|
||||||
|
|
||||||
// ];
|
// ];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,7 +298,16 @@ class EmailDefaults
|
|||||||
$this->email->email_object->entity instanceof Quote ||
|
$this->email->email_object->entity instanceof Quote ||
|
||||||
$this->email->email_object->entity instanceof Credit)) {
|
$this->email->email_object->entity instanceof Credit)) {
|
||||||
$pdf = ((new CreateRawPdf($this->email->email_object->invitation, $this->email->company->db))->handle());
|
$pdf = ((new CreateRawPdf($this->email->email_object->invitation, $this->email->company->db))->handle());
|
||||||
|
if ($this->email->email_object->company->enable_e_invoice && $this->email->email_object->entity instanceof Invoice) {
|
||||||
|
$tempfile = tmpfile();
|
||||||
|
file_put_contents(stream_get_meta_data($tempfile)['uri'], $pdf);
|
||||||
|
$xinvoice_path = (new CreateXInvoice($this->email->email_object->entity, true, stream_get_meta_data($tempfile)['uri']))->handle();
|
||||||
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode(file_get_contents(stream_get_meta_data($tempfile)['uri'])), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]);
|
||||||
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode(file_get_contents($xinvoice_path)), 'name' => explode(".", $this->email->email_object->entity->getFileName('xml'))[0]."-xinvoice.xml"]]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]);
|
||||||
|
}
|
||||||
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]);
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,6 +319,11 @@ class EmailDefaults
|
|||||||
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($ubl_string), 'name' => $this->email->email_object->entity->getFileName('xml')]]);
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($ubl_string), 'name' => $this->email->email_object->entity->getFileName('xml')]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** E-Invoice xml file */
|
||||||
|
if ($this->email->email_object->company->enable_e_invoice && $this->email->email_object->entity instanceof Invoice) {
|
||||||
|
$xinvoice_path = (new GetInvoiceXInvoice($this->email->email_object->entity))->run();
|
||||||
|
$this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode(file_get_contents($xinvoice_path)), 'name' => explode(".", $this->email->email_object->entity->getFileName('xml'))[0]."-e_invoice.xml"]]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->email->email_object->settings->document_email_attachment || !$this->email->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
|
if (!$this->email->email_object->settings->document_email_attachment || !$this->email->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
|
||||||
return $this;
|
return $this;
|
||||||
@ -332,7 +346,7 @@ class EmailDefaults
|
|||||||
if ($this->email->email_object->entity instanceof Invoice) {
|
if ($this->email->email_object->entity instanceof Invoice) {
|
||||||
$expense_ids = [];
|
$expense_ids = [];
|
||||||
$task_ids = [];
|
$task_ids = [];
|
||||||
|
|
||||||
foreach ($this->email->email_object->entity->line_items as $item) {
|
foreach ($this->email->email_object->entity->line_items as $item) {
|
||||||
if (property_exists($item, 'expense_id')) {
|
if (property_exists($item, 'expense_id')) {
|
||||||
$expense_ids[] = $item->expense_id;
|
$expense_ids[] = $item->expense_id;
|
||||||
|
54
app/Services/Invoice/GetInvoiceXInvoice.php
Normal file
54
app/Services/Invoice/GetInvoiceXInvoice.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services\Invoice;
|
||||||
|
|
||||||
|
use App\Jobs\Invoice\CreateXInvoice;
|
||||||
|
use App\Models\ClientContact;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Services\AbstractService;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class GetInvoiceXInvoice extends AbstractService
|
||||||
|
{
|
||||||
|
public function __construct(Invoice $invoice, ClientContact $contact = null)
|
||||||
|
{
|
||||||
|
$this->invoice = $invoice;
|
||||||
|
|
||||||
|
$this->contact = $contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
if (! $this->contact) {
|
||||||
|
$this->contact = $this->invoice->client->primary_contact()->first() ?: $this->invoice->client->contacts()->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
$invitation = $this->invoice->invitations->where('client_contact_id', $this->contact->id)->first();
|
||||||
|
|
||||||
|
if (! $invitation) {
|
||||||
|
$invitation = $this->invoice->invitations->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_path = $this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()). $this->invoice->getFileName("xml");
|
||||||
|
|
||||||
|
// $disk = 'public';
|
||||||
|
$disk = config('filesystems.default');
|
||||||
|
|
||||||
|
$file = Storage::disk($disk)->exists($file_path);
|
||||||
|
|
||||||
|
if (! $file) {
|
||||||
|
$file_path = (new CreateXInvoice($this->invoice, false))->handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file_path;
|
||||||
|
}
|
||||||
|
}
|
@ -14,10 +14,12 @@ namespace App\Services\Invoice;
|
|||||||
use App\Events\Invoice\InvoiceWasArchived;
|
use App\Events\Invoice\InvoiceWasArchived;
|
||||||
use App\Jobs\Entity\CreateEntityPdf;
|
use App\Jobs\Entity\CreateEntityPdf;
|
||||||
use App\Jobs\Inventory\AdjustProductInventory;
|
use App\Jobs\Inventory\AdjustProductInventory;
|
||||||
|
use App\Jobs\Invoice\CreateXInvoice;
|
||||||
use App\Libraries\Currency\Conversion\CurrencyApi;
|
use App\Libraries\Currency\Conversion\CurrencyApi;
|
||||||
use App\Models\CompanyGateway;
|
use App\Models\CompanyGateway;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use App\Models\InvoiceInvitation;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
@ -184,6 +186,11 @@ class InvoiceService
|
|||||||
return (new GenerateDeliveryNote($invoice, $contact))->run();
|
return (new GenerateDeliveryNote($invoice, $contact))->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEInvoice($contact = null)
|
||||||
|
{
|
||||||
|
return (new GetInvoiceXInvoice($this->invoice, $contact))->run();
|
||||||
|
}
|
||||||
|
|
||||||
public function sendEmail($contact = null)
|
public function sendEmail($contact = null)
|
||||||
{
|
{
|
||||||
$send_email = new SendEmail($this->invoice, null, $contact);
|
$send_email = new SendEmail($this->invoice, null, $contact);
|
||||||
@ -293,7 +300,7 @@ class InvoiceService
|
|||||||
} elseif ($this->invoice->balance < 0 || $this->invoice->balance > 0) {
|
} elseif ($this->invoice->balance < 0 || $this->invoice->balance > 0) {
|
||||||
$this->invoice->status_id = Invoice::STATUS_SENT;
|
$this->invoice->status_id = Invoice::STATUS_SENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,6 +358,27 @@ class InvoiceService
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteEInvoice()
|
||||||
|
{
|
||||||
|
$this->invoice->load('invitations');
|
||||||
|
|
||||||
|
$this->invoice->invitations->each(function ($invitation) {
|
||||||
|
try {
|
||||||
|
if (Storage::disk(config('filesystems.default'))->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) {
|
||||||
|
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) {
|
||||||
|
Storage::disk('public')->delete($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"));
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
nlog($e->getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function removeUnpaidGatewayFees()
|
public function removeUnpaidGatewayFees()
|
||||||
{
|
{
|
||||||
$balance = $this->invoice->balance;
|
$balance = $this->invoice->balance;
|
||||||
@ -421,6 +449,10 @@ class InvoiceService
|
|||||||
if ($force) {
|
if ($force) {
|
||||||
$this->invoice->invitations->each(function ($invitation) {
|
$this->invoice->invitations->each(function ($invitation) {
|
||||||
(new CreateEntityPdf($invitation))->handle();
|
(new CreateEntityPdf($invitation))->handle();
|
||||||
|
if ($invitation instanceof InvoiceInvitation)
|
||||||
|
{
|
||||||
|
(new CreateXInvoice($invitation->invoice, true))->handle();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -428,6 +460,10 @@ class InvoiceService
|
|||||||
|
|
||||||
$this->invoice->invitations->each(function ($invitation) {
|
$this->invoice->invitations->each(function ($invitation) {
|
||||||
CreateEntityPdf::dispatch($invitation);
|
CreateEntityPdf::dispatch($invitation);
|
||||||
|
if ($invitation instanceof InvoiceInvitation)
|
||||||
|
{
|
||||||
|
CreateXInvoice::dispatch($invitation->invoice, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
nlog('failed creating invoices in Touch PDF');
|
nlog('failed creating invoices in Touch PDF');
|
||||||
|
@ -91,6 +91,7 @@
|
|||||||
"turbo124/predis": "1.1.11",
|
"turbo124/predis": "1.1.11",
|
||||||
"twilio/sdk": "^6.40",
|
"twilio/sdk": "^6.40",
|
||||||
"webpatser/laravel-countries": "dev-master#75992ad",
|
"webpatser/laravel-countries": "dev-master#75992ad",
|
||||||
|
"horstoeko/zugferd":"^1",
|
||||||
"wepay/php-sdk": "^0.3"
|
"wepay/php-sdk": "^0.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
|
||||||
|
Schema::table('clients', function (Blueprint $table) {
|
||||||
|
$table->string('routing_id')->default(null)->nullable();
|
||||||
|
});
|
||||||
|
Schema::table('companies', function (Blueprint $table) {
|
||||||
|
$table->boolean('enable_e_invoice')->default(false);
|
||||||
|
$table->string('e_invoice_type')->default("EN16931");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
@ -4979,6 +4979,9 @@ $LANG = array(
|
|||||||
'white_label_body' => 'Thank you for purchasing a white label license. <br><br> Your license key is: <br><br> :license_key',
|
'white_label_body' => 'Thank you for purchasing a white label license. <br><br> Your license key is: <br><br> :license_key',
|
||||||
'payment_type_Klarna' => 'Klarna',
|
'payment_type_Klarna' => 'Klarna',
|
||||||
'payment_type_Interac E Transfer' => 'Interac E Transfer',
|
'payment_type_Interac E Transfer' => 'Interac E Transfer',
|
||||||
|
'xinvoice_payable' => 'Payable within :payeddue days net until :paydate',
|
||||||
|
'xinvoice_no_buyers_reference' => "No buyer's reference given",
|
||||||
|
'xinvoice_online_payment' => 'The invoice needs to be payed online via the provided link',
|
||||||
'pre_payment' => 'Pre Payment',
|
'pre_payment' => 'Pre Payment',
|
||||||
'number_of_payments' => 'Number of payments',
|
'number_of_payments' => 'Number of payments',
|
||||||
'number_of_payments_helper' => 'The number of times this payment will be made',
|
'number_of_payments_helper' => 'The number of times this payment will be made',
|
||||||
|
@ -205,6 +205,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
|
|||||||
Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action');
|
Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action');
|
||||||
Route::put('invoices/{invoice}/upload', [InvoiceController::class, 'upload'])->name('invoices.upload');
|
Route::put('invoices/{invoice}/upload', [InvoiceController::class, 'upload'])->name('invoices.upload');
|
||||||
Route::get('invoice/{invitation_key}/download', [InvoiceController::class, 'downloadPdf'])->name('invoices.downloadPdf');
|
Route::get('invoice/{invitation_key}/download', [InvoiceController::class, 'downloadPdf'])->name('invoices.downloadPdf');
|
||||||
|
Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoices.downloadEInvoice');
|
||||||
Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk');
|
Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk');
|
||||||
Route::post('invoices/update_reminders', [InvoiceController::class, 'update_reminders'])->name('invoices.update_reminders');
|
Route::post('invoices/update_reminders', [InvoiceController::class, 'update_reminders'])->name('invoices.update_reminders');
|
||||||
|
|
||||||
@ -316,7 +317,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
|
|||||||
|
|
||||||
Route::post('verify', [TwilioController::class, 'generate'])->name('verify.generate')->middleware('throttle:100,1');
|
Route::post('verify', [TwilioController::class, 'generate'])->name('verify.generate')->middleware('throttle:100,1');
|
||||||
Route::post('verify/confirm', [TwilioController::class, 'confirm'])->name('verify.confirm');
|
Route::post('verify/confirm', [TwilioController::class, 'confirm'])->name('verify.confirm');
|
||||||
|
|
||||||
Route::resource('vendors', VendorController::class); // name = (vendors. index / create / show / update / destroy / edit
|
Route::resource('vendors', VendorController::class); // name = (vendors. index / create / show / update / destroy / edit
|
||||||
Route::post('vendors/bulk', [VendorController::class, 'bulk'])->name('vendors.bulk');
|
Route::post('vendors/bulk', [VendorController::class, 'bulk'])->name('vendors.bulk');
|
||||||
Route::put('vendors/{vendor}/upload', [VendorController::class, 'upload']);
|
Route::put('vendors/{vendor}/upload', [VendorController::class, 'upload']);
|
||||||
|
@ -102,7 +102,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie
|
|||||||
Route::resource('documents', App\Http\Controllers\ClientPortal\DocumentController::class)->only(['index', 'show']);
|
Route::resource('documents', App\Http\Controllers\ClientPortal\DocumentController::class)->only(['index', 'show']);
|
||||||
|
|
||||||
Route::get('subscriptions/{recurring_invoice}/plan_switch/{target}', [App\Http\Controllers\ClientPortal\SubscriptionPlanSwitchController::class, 'index'])->name('subscription.plan_switch');
|
Route::get('subscriptions/{recurring_invoice}/plan_switch/{target}', [App\Http\Controllers\ClientPortal\SubscriptionPlanSwitchController::class, 'index'])->name('subscription.plan_switch');
|
||||||
|
|
||||||
Route::get('subscriptions/{recurring_invoice}', [SubscriptionController::class, 'show'])->middleware('portal_enabled')->name('subscriptions.show');
|
Route::get('subscriptions/{recurring_invoice}', [SubscriptionController::class, 'show'])->middleware('portal_enabled')->name('subscriptions.show');
|
||||||
Route::get('subscriptions', [SubscriptionController::class, 'index'])->middleware('portal_enabled')->name('subscriptions.index');
|
Route::get('subscriptions', [SubscriptionController::class, 'index'])->middleware('portal_enabled')->name('subscriptions.index');
|
||||||
|
|
||||||
@ -130,6 +130,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
|
|||||||
Route::get('credit/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'creditRouter']);
|
Route::get('credit/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'creditRouter']);
|
||||||
Route::get('recurring_invoice/{invitation_key}/download_pdf', [RecurringInvoiceController::class, 'downloadPdf'])->name('recurring_invoice.download_invitation_key');
|
Route::get('recurring_invoice/{invitation_key}/download_pdf', [RecurringInvoiceController::class, 'downloadPdf'])->name('recurring_invoice.download_invitation_key');
|
||||||
Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key');
|
Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key');
|
||||||
|
Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoice.download_e_invoice');
|
||||||
Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key');
|
Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key');
|
||||||
Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key');
|
Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key');
|
||||||
Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload']);
|
Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload']);
|
||||||
|
68
tests/Unit/EInvoiceTest.php
Normal file
68
tests/Unit/EInvoiceTest.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Jobs\Entity\CreateEntityPdf;
|
||||||
|
use App\Jobs\Invoice\CreateXInvoice;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use horstoeko\zugferd\ZugferdDocumentReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @covers App\Jobs\Invoice\CreateXInvoice
|
||||||
|
*/
|
||||||
|
class EInvoiceTest extends TestCase
|
||||||
|
{
|
||||||
|
use MockAccountData;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
protected function setUp() :void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->withoutMiddleware(
|
||||||
|
ThrottleRequests::class
|
||||||
|
);
|
||||||
|
$this->makeTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEInvoiceGenerates()
|
||||||
|
{
|
||||||
|
$xinvoice = (new CreateXInvoice($this->invoice, false))->handle();
|
||||||
|
$this->assertNotNull($xinvoice);
|
||||||
|
$this->assertFileExists($xinvoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testValidityofXMLFile()
|
||||||
|
{
|
||||||
|
$xinvoice = (new CreateXInvoice($this->invoice, false))->handle();
|
||||||
|
$document = ZugferdDocumentReader::readAndGuessFromFile($xinvoice);
|
||||||
|
$document ->getDocumentInformation($documentno);
|
||||||
|
$this->assertEquals($this->invoice->number, $documentno);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function checkEmbededPDFFile()
|
||||||
|
{
|
||||||
|
$pdf = (new CreateEntityPdf($this->invoice->invitations()->first()));
|
||||||
|
(new CreateXInvoice($this->invoice, true, $pdf))->handle();
|
||||||
|
$document = ZugferdDocumentReader::readAndGuessFromFile($pdf);
|
||||||
|
$document ->getDocumentInformation($documentno);
|
||||||
|
$this->assertEquals($this->invoice->number, $documentno);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user