Merge pull request #9635 from LarsK1/v5-develop

Feature: E-Invoice Import
This commit is contained in:
David Bomba 2024-06-22 13:54:06 +10:00 committed by GitHub
commit 0ed2520c92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1248 additions and 67 deletions

View File

@ -19,10 +19,12 @@ use App\Http\Requests\Expense\BulkExpenseRequest;
use App\Http\Requests\Expense\CreateExpenseRequest;
use App\Http\Requests\Expense\DestroyExpenseRequest;
use App\Http\Requests\Expense\EditExpenseRequest;
use App\Http\Requests\Expense\EDocumentRequest;
use App\Http\Requests\Expense\ShowExpenseRequest;
use App\Http\Requests\Expense\StoreExpenseRequest;
use App\Http\Requests\Expense\UpdateExpenseRequest;
use App\Http\Requests\Expense\UploadExpenseRequest;
use App\Jobs\EDocument\ImportEDocument;
use App\Models\Account;
use App\Models\Expense;
use App\Repositories\ExpenseRepository;
@ -581,4 +583,15 @@ class ExpenseController extends BaseController
return $this->itemResponse($expense->fresh());
}
public function edocument(EDocumentRequest $request): string
{
if ($request->hasFile("documents")) {
return (new ImportEDocument($request->file("documents")[0]->get(), $request->file("documents")[0]->getClientOriginalName()))->handle();
}
else {
return "No file found";
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests\Expense;
use App\Http\Requests\Request;
use App\Models\User;
class EDocumentRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
/** @var User $user */
$user = auth()->user();
return $user->isAdmin();
}
public function rules()
{
$rules = [];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) {
$rules['documents'] = $this->fileValidation();
}
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
$this->replace($input);
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\EDocument;
use App\Models\Expense;
use App\Services\EDocument\Imports\ZugferdEDocument;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ImportEDocument implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $deleteWhenMissingModels = true;
private string $file_name;
private readonly string $file_content;
public function __construct(string $file_content, string $file_name)
{
$this->file_content = $file_content;
$this->file_name = $file_name;
}
/**
* Execute the job.
*
* @return Expense
* @throws \Exception
*/
public function handle(): Expense
{
if (str_contains($this->file_name, ".xml")){
switch (true) {
case stristr($this->file_content, "urn:cen.eu:en16931:2017"):
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"):
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.1"):
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.0"):
return (new ZugferdEDocument($this->file_content, $this->file_name))->run();
default:
throw new Exception("E-Invoice standard not supported");
}
}
else {
throw new Exception("File type not supported");
}
}
}

View File

@ -0,0 +1,129 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Imports;
use App\Factory\ExpenseFactory;
use App\Factory\VendorFactory;
use App\Jobs\Util\UploadFile;
use App\Models\Country;
use App\Models\Currency;
use App\Models\Expense;
use App\Models\Vendor;
use App\Services\AbstractService;
use App\Utils\TempFile;
use Exception;
use horstoeko\zugferd\ZugferdDocumentReader;
use horstoeko\zugferdvisualizer\renderer\ZugferdVisualizerLaravelRenderer;
use horstoeko\zugferdvisualizer\ZugferdVisualizer;
class ZugferdEDocument extends AbstractService {
public ZugferdDocumentReader|string $document;
/**
* @throws Exception
*/
public function __construct(public string $tempdocument, public string $documentname)
{
# curl -X POST http://localhost:8000/api/v1/edocument/upload -H "Content-Type: multipart/form-data" -H "X-API-TOKEN: 7tdDdkz987H3AYIWhNGXy8jTjJIoDhkAclCDLE26cTCj1KYX7EBHC66VEitJwWhn" -H "X-Requested-With: XMLHttpRequest" -F _method=PUT -F documents[]=@einvoice.xml
}
/**
* @throws Exception
*/
public function run(): Expense
{
$user = auth()->user();
$this->document = ZugferdDocumentReader::readAndGuessFromContent($this->tempdocument);
$this->document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);
$this->document->getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount);
$expense = Expense::where('amount', $grandTotalAmount)->where("transaction_reference", $documentno)->whereDate("date", $documentdate)->first();
if (empty($expense)) {
// The document does not exist as an expense
// Handle accordingly
$visualizer = new ZugferdVisualizer($this->document);
$visualizer->setDefaultTemplate();
$visualizer->setRenderer(app(ZugferdVisualizerLaravelRenderer::class));
$visualizer->setPdfFontDefault("arial");
$visualizer->setPdfPaperSize('A4-P');
$visualizer->setTemplate('edocument.xinvoice');
$expense = ExpenseFactory::create($user->company()->id, $user->id);
$expense->date = $documentdate;
$expense->user_id = $user->id;
$expense->company_id = $user->company->id;
$expense->public_notes = $documentno;
$expense->currency_id = Currency::whereCode($invoiceCurrency)->first()->id;
$expense->save();
$origin_file = TempFile::UploadedFileFromRaw($this->tempdocument, $this->documentname, "application/xml");
(new UploadFile($origin_file, UploadFile::DOCUMENT, $user, $expense->company, $expense, null, false))->handle();
$uploaded_file = TempFile::UploadedFileFromRaw($visualizer->renderPdf(), $documentno."_visualiser.pdf", "application/pdf");
(new UploadFile($uploaded_file, UploadFile::DOCUMENT, $user, $expense->company, $expense, null, false))->handle();
$expense->save();
if ($taxCurrency && $taxCurrency != $invoiceCurrency) {
$expense->private_notes = ctrans("texts.tax_currency_mismatch");
}
$expense->uses_inclusive_taxes = True;
$expense->amount = $grandTotalAmount;
$counter = 1;
if ($this->document->firstDocumentTax()) {
do {
$this->document->getDocumentTax($categoryCode, $typeCode, $basisAmount, $calculatedAmount, $rateApplicablePercent, $exemptionReason, $exemptionReasonCode, $lineTotalBasisAmount, $allowanceChargeBasisAmount, $taxPointDate, $dueDateTypeCode);
$expense->{"tax_amount$counter"} = $calculatedAmount;
$expense->{"tax_rate$counter"} = $rateApplicablePercent;
$counter++;
} while ($this->document->nextDocumentTax());
}
$this->document->getDocumentSeller($name, $buyer_id, $buyer_description);
$this->document->getDocumentSellerContact($person_name, $person_department, $contact_phone, $contact_fax, $contact_email);
$this->document->getDocumentSellerAddress($address_1, $address_2, $address_3, $postcode, $city, $country, $subdivision);
$this->document->getDocumentSellerTaxRegistration($taxtype);
$taxid = null;
if (array_key_exists("VA", $taxtype)) {
$taxid = $taxtype["VA"];
}
$vendor = Vendor::where('vat_number', $taxid)->first();
if (!empty($vendor)) {
// Vendor found
$expense->vendor_id = $vendor->id;
} else {
$vendor = VendorFactory::create($user->company()->id, $user->id);
$vendor->name = $name;
if ($taxid != null) {
$vendor->vat_number = $taxid;
}
$vendor->currency_id = Currency::whereCode($invoiceCurrency)->first()->id;
$vendor->phone = $contact_phone;
$vendor->address1 = $address_1;
$vendor->address2 = $address_2;
$vendor->city = $city;
$vendor->postal_code = $postcode;
$vendor->country_id = Country::where('iso_3166_2', $country)->first()->id;
$vendor->save();
$expense->vendor_id = $vendor->id;
}
$expense->transaction_reference = $documentno;
}
else {
// The document exists as an expense
// Handle accordingly
nlog("Document already exists");
$expense->private_notes = $expense->private_notes . ctrans("texts.edocument_import_already_exists", ["date" => time()]);
}
$expense->save();
return $expense;
}
}

View File

@ -56,6 +56,7 @@
"hedii/laravel-gelf-logger": "^8",
"horstoeko/orderx": "dev-master",
"horstoeko/zugferd": "^1",
"horstoeko/zugferdvisualizer":"^1",
"hyvor/php-json-exporter": "^0.0.3",
"imdhemy/laravel-purchases": "^1.7",
"intervention/image": "^2.5",

639
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ca9bed451100b5dc088fe6bcfb9babea",
"content-hash": "9b3b2f15703a291c1ebbedf56aaa4c69",
"packages": [
{
"name": "adrienrn/php-mimetyper",
@ -1933,6 +1933,68 @@
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "dompdf/dompdf",
"version": "v2.0.8",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "c20247574601700e1f7c8dab39310fca1964dc52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52",
"reference": "c20247574601700e1f7c8dab39310fca1964dc52",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
"phenx/php-svg-lib": ">=0.5.2 <1.0.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v2.0.8"
},
"time": "2024-04-29T13:06:17+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.3.3",
@ -3800,6 +3862,66 @@
},
"time": "2024-06-15T05:49:47+00:00"
},
{
"name": "horstoeko/zugferdvisualizer",
"version": "v1.0.8",
"source": {
"type": "git",
"url": "https://github.com/horstoeko/zugferdvisualizer.git",
"reference": "3a53ebf570284bf5fa3e525240761c03d3936076"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/horstoeko/zugferdvisualizer/zipball/3a53ebf570284bf5fa3e525240761c03d3936076",
"reference": "3a53ebf570284bf5fa3e525240761c03d3936076",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^2.0",
"ext-dom": "*",
"ext-mbstring": "*",
"horstoeko/zugferd": "^1",
"league/commonmark": "^1|^2",
"mpdf/mpdf": "^8",
"php": "^7.3|^7.4|^8"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"pdepend/pdepend": "^2",
"phploc/phploc": "^7",
"phpmd/phpmd": "^2",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^9",
"sebastian/phpcpd": "^6",
"squizlabs/php_codesniffer": "^3"
},
"type": "package",
"autoload": {
"psr-4": {
"horstoeko\\zugferdvisualizer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Erling",
"email": "daniel@erling.com.de",
"role": "lead"
}
],
"description": "A library",
"homepage": "https://github.com/horstoeko/zugferdvisualizer",
"support": {
"issues": "https://github.com/horstoeko/zugferdvisualizer/issues",
"source": "https://github.com/horstoeko/zugferdvisualizer/tree/v1.0.8"
},
"time": "2024-06-17T15:39:04+00:00"
},
{
"name": "http-interop/http-factory-guzzle",
"version": "1.2.0",
@ -6440,6 +6562,73 @@
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "masterminds/html5",
"version": "2.9.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.9.0"
},
"time": "2024-03-31T07:05:07+00:00"
},
{
"name": "microsoft/microsoft-graph",
"version": "1.110.0",
@ -6773,6 +6962,179 @@
],
"time": "2024-04-12T21:02:21+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.2.4",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
"reference": "9e3ff91606fed11cd58a130eabaaf60e56fdda88"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/9e3ff91606fed11cd58a130eabaaf60e56fdda88",
"reference": "9e3ff91606fed11cd58a130eabaaf60e56fdda88",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Mpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Matěj Humpál",
"role": "Developer, maintainer"
},
{
"name": "Ian Back",
"role": "Developer (retired)"
}
],
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
"homepage": "https://mpdf.github.io",
"keywords": [
"pdf",
"php",
"utf-8"
],
"support": {
"docs": "http://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
"funding": [
{
"url": "https://www.paypal.me/mpdf",
"type": "custom"
}
],
"time": "2024-06-14T16:06:41+00:00"
},
{
"name": "mpdf/psr-http-message-shim",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-http-message-shim.git",
"reference": "3206e6b80b6d2479e148ee497e9f2bebadc919db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/3206e6b80b6d2479e148ee497e9f2bebadc919db",
"reference": "3206e6b80b6d2479e148ee497e9f2bebadc919db",
"shasum": ""
},
"require": {
"psr/http-message": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrHttpMessageShim\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
},
{
"name": "Nigel Cunningham",
"email": "nigel.cunningham@technocrat.com.au"
}
],
"description": "Shim to allow support of different psr/message versions.",
"support": {
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
"source": "https://github.com/mpdf/psr-http-message-shim/tree/1.0.0"
},
"time": "2023-09-01T05:59:47+00:00"
},
{
"name": "mpdf/psr-log-aware-trait",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
"shasum": ""
},
"require": {
"psr/log": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrLogAwareTrait\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
}
],
"description": "Trait to allow support of different psr/log versions.",
"support": {
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
},
"time": "2023-05-03T06:19:36+00:00"
},
{
"name": "mtdowling/jmespath.php",
"version": "2.7.0",
@ -6839,6 +7201,66 @@
},
"time": "2023-08-25T10:54:48+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2024-06-12T14:39:25+00:00"
},
{
"name": "nelexa/zip",
"version": "4.0.2",
@ -7890,6 +8312,96 @@
},
"time": "2024-04-22T22:05:04+00:00"
},
{
"name": "phenx/php-font-lib",
"version": "0.5.6",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "a1681e9793040740a405ac5b189275059e2a9863"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863",
"reference": "a1681e9793040740a405ac5b189275059e2a9863",
"shasum": ""
},
"require": {
"ext-mbstring": "*"
},
"require-dev": {
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Fabien Ménager",
"email": "fabien.menager@gmail.com"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/PhenX/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/0.5.6"
},
"time": "2024-01-29T14:45:26+00:00"
},
{
"name": "phenx/php-svg-lib",
"version": "0.5.4",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691",
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "Fabien Ménager",
"email": "fabien.menager@gmail.com"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/PhenX/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4"
},
"time": "2024-04-08T12:52:34+00:00"
},
{
"name": "php-http/client-common",
"version": "2.7.1",
@ -9992,6 +10504,71 @@
},
"time": "2024-03-25T10:48:46+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "v8.5.1",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "4a3d572b0f8b28bb6fd016ae8bbfc445facef152"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/4a3d572b0f8b28bb6fd016ae8bbfc445facef152",
"reference": "4a3d572b0f8b28bb6fd016ae8bbfc445facef152",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=5.6.20"
},
"require-dev": {
"phpunit/phpunit": "^5.7.27"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.5.1"
},
"time": "2024-02-15T16:41:13+00:00"
},
{
"name": "sabre/uri",
"version": "3.0.1",
@ -16386,66 +16963,6 @@
},
"time": "2024-05-16T03:13:13+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2024-06-12T14:39:25+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v7.10.0",

View File

@ -2934,6 +2934,13 @@ $lang = array(
'mime_types' => 'Mime types',
'mime_types_placeholder' => '.pdf , .docx, .jpg',
'mime_types_help' => 'Comma separated list of allowed mime types, leave blank for all',
'ticket_number_start_help' => 'Ticket number must be greater than the current ticket number',
'new_ticket_template_id' => 'New ticket',
'new_ticket_autoresponder_help' => 'Selecting a template will send an auto response to a client/contact when a new ticket is created',
'update_ticket_template_id' => 'Updated ticket',
'update_ticket_autoresponder_help' => 'Selecting a template will send an auto response to a client/contact when a ticket is updated',
'close_ticket_template_id' => 'Closed ticket',
'close_ticket_autoresponder_help' => 'Selecting a template will send an auto response to a client/contact when a ticket is closed',
'default_priority' => 'Default priority',
'alert_new_comment_id' => 'New comment',
'alert_comment_ticket_help' => 'Selecting a template will send a notification (to agent) when a comment is made.',
@ -5303,6 +5310,9 @@ $lang = array(
'currency_bhutan_ngultrum' => 'Bhutan Ngultrum',
'end_of_month' => 'End Of Month',
'merge_e_invoice_to_pdf' => 'Merge E-Invoice and PDF',
'end_of_month' => 'End Of Month',
'tax_currency_mismatch' => 'Tax currency is different from invoice currency',
'edocument_import_already_exists' => '\nThe invoice has already been imported on :date'
);
return $lang;
return $lang;

View File

@ -0,0 +1,406 @@
<html>
<head>
<style>
@page {
size: 21cm 29cm;
margin-left: 2.5cm;
}
body {
font-size: 9pt;
}
h1 {
font-size: 19px;
}
table {
margin: 0;
padding: 0;
table-layout: fixed;
}
tr {
margin: 0;
padding: 0;
}
th, td {
vertical-align: top;
}
th {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
font-size: 8pt;
}
td {
font-size: 8pt;
}
table.postable {
width: 100%;
min-width: 100%;
max-width: 100%;
margin-top: 5px;
}
table.postable th {
padding-bottom: 10px;
}
table.postable td.posno,
table.postable th.posno {
width: 10%;
min-width: 10%;
max-width: 10%;
text-align: left;
}
table.postable td.posdesc,
table.postable th.posdesc {
width: 25%;
min-width: 25%;
max-width: 25%;
text-align: left;
}
table.postable td.posqty,
table.postable th.posqty {
width: 20%;
min-width: 20%;
max-width: 20%;
text-align: right;
}
table.postable td.posunitprice,
table.postable th.posunitprice {
width: 20%;
min-width: 20%;
max-width: 20%;
text-align: right;
}
table.postable td.poslineamount,
table.postable th.poslineamount {
width: 20%;
min-width: 20%;
max-width: 20%;
text-align: right;
}
table.postable td.poslinevat,
table.postable th.poslinevat {
width: 5%;
min-width: 5%;
max-width: 5%;
text-align: right;
}
table.postable th.posno {
border-bottom: 1px solid #dcdcdc;
}
table.postable th.posdesc {
border-bottom: 1px solid #dcdcdc;
}
table.postable th.posqty {
border-bottom: 1px solid #dcdcdc;
}
table.postable th.posunitprice {
border-bottom: 1px solid #dcdcdc;
}
table.postable th.poslineamount {
border-bottom: 1px solid #dcdcdc;
}
table.postable th.poslinevat {
border-bottom: 1px solid #dcdcdc;
}
table.postable td.totalname {
width: 20%;
min-width: 20%;
max-width: 20%;
text-align: left;
border-bottom: 1px solid #dcdcdc;
}
table.postable td.totalvalue {
width: 20%;
min-width: 20%;
max-width: 20%;
text-align: right;
border-bottom: 1px solid #dcdcdc;
}
.space {
padding-top: 10px;
}
.space2 {
padding-top: 20px;
}
.space3 {
padding-top: 30px;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.red {
color: #ff0000;
}
.green {
color: #00fff0
}
.mt-15 {
margin-top: 15px;
}
.mt-20 {
margin-top: 20px;
}
.mt-25 {
margin-top: 25px;
}
.mt-30 {
margin-top: 30px;
}
.pt-15 {
padding-top: 15px;
}
.pt-20 {
padding-top: 20px;
}
.pt-25 {
padding-top: 25px;
}
.pt-30 {
padding-top: 30px;
}
.fs-10 {
font-size: 10pt;
}
.fs-11 {
font-size: 11pt;
}
.fs-12 {
font-size: 12pt;
}
.fs-13 {
font-size: 13pt;
}
.fs-14 {
font-size: 14pt;
}
.pb-0 {
padding-bottom: 0px;
}
</style>
</head>
<body>
<?php
$document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);
$document->getDocumentBuyer($buyername, $buyerids, $buyerdescription);
$document->getDocumentBuyerAddress($buyeraddressline1, $buyeraddressline2, $buyeraddressline3, $buyerpostcode, $buyercity, $buyercounty, $buyersubdivision);
?>
<p>
<?php echo $buyername; ?><br>
<?php if ($buyeraddressline1) { ?><?php echo $buyeraddressline1; ?><br><?php } ?>
<?php if ($buyeraddressline2) { ?><?php echo $buyeraddressline2; ?><br><?php } ?>
<?php if ($buyeraddressline3) { ?><?php echo $buyeraddressline3; ?><br><?php } ?>
<?php echo $buyercounty . " " . $buyerpostcode . " " . $buyercity; ?><br>
</p>
<h1 style="margin: 0; padding: 0; margin-top: 50px">
Invoice <?php echo $documentno; ?>
</h1>
<p style="margin: 0; padding: 0">
Invoice Date <?php echo $documentdate->format("d.m.Y"); ?>
</p>
<p style="margin-top: 50px" class="bold">
Sehr geehrter Kunde,
</p>
<p>
wir erlauben uns Ihnen folgende Position in Rechnung zu stellen.
</p>
<table class="postable">
<thead>
<tr>
<th class="posno">Pos.</th>
<th class="posdesc">Beschreibung</th>
<th class="posqty">Stk.</th>
<th class="posunitprice">Preis</th>
<th class="poslineamount">Menge</th>
<th class="poslinevat">MwSt %</th>
</tr>
</thead>
<tbody>
<?php
if ($document->firstDocumentPosition()) {
$isfirstposition = true;
do {
$document->getDocumentPositionGenerals($lineid, $linestatuscode, $linestatusreasoncode);
$document->getDocumentPositionProductDetails($prodname, $proddesc, $prodsellerid, $prodbuyerid, $prodglobalidtype, $prodglobalid);
$document->getDocumentPositionGrossPrice($grosspriceamount, $grosspricebasisquantity, $grosspricebasisquantityunitcode);
$document->getDocumentPositionNetPrice($netpriceamount, $netpricebasisquantity, $netpricebasisquantityunitcode);
$document->getDocumentPositionLineSummation($lineTotalAmount, $totalAllowanceChargeAmount);
$document->getDocumentPositionQuantity($billedquantity, $billedquantityunitcode, $chargeFreeQuantity, $chargeFreeQuantityunitcode, $packageQuantity, $packageQuantityunitcode);
?>
<?php if ($document->firstDocumentPositionNote()) { ?>
<tr>
<td class="<?php echo $isfirstposition ? ' space' : '' ?>">&nbsp;</td>
<td colspan="5" class="<?php echo $isfirstposition ? ' space' : '' ?>">
<?php $document->getDocumentPositionNote($posnoteContent, $posnoteContentCode, $posnoteSubjectCode); ?>
<?php echo $posnoteContent; ?>
<?php $isfirstposition = false; ?>
</td>
</tr>
<?php } while ($document->nextDocumentPositionNote()); ?>
<tr>
<td class="posno<?php echo $isfirstposition ? ' space' : '' ?>"><?php echo $lineid; ?></td>
<td class="posdesc<?php echo $isfirstposition ? ' space' : '' ?>"><?php echo $prodname; ?></td>
<td class="posqty<?php echo $isfirstposition ? ' space' : '' ?>"><?php echo $billedquantity; ?> <?php echo $billedquantityunitcode ?></td>
<td class="posunitprice<?php echo $isfirstposition ? ' space' : '' ?>"><?php echo number_format($netpriceamount, 2); ?> <?php echo $invoiceCurrency; ?></td>
<td class="poslineamount<?php echo $isfirstposition ? ' space' : '' ?>"><?php echo number_format($lineTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
<?php if ($document->firstDocumentPositionTax()) { ?>
<?php $document->getDocumentPositionTax($categoryCode, $typeCode, $rateApplicablePercent, $calculatedAmount, $exemptionReason, $exemptionReasonCode); ?>
<td class="poslinevat<?php echo $isfirstposition ? ' space' : '' ?>"><?php echo number_format($rateApplicablePercent, 2); ?> %</td>
<?php } else { ?>
<td class="poslinevat<?php echo $isfirstposition ? ' space' : '' ?>">&nbsp;</td>
<?php } ?>
</tr>
<?php if ($document->firstDocumentPositionGrossPriceAllowanceCharge()) { ?>
<?php do { ?>
<?php $document->getDocumentPositionGrossPrice($grossAmount, $grossBasisQuantity, $grossBasisQuantityUnitCode); ?>
<?php $document->getDocumentPositionGrossPriceAllowanceCharge($actualAmount, $isCharge, $calculationPercent, $basisAmount, $reason, $taxTypeCode, $taxCategoryCode, $rateApplicablePercent, $sequence, $basisQuantity, $basisQuantityUnitCode, $reasonCode); ?>
<tr>
<td class="posno">&nbsp;</td>
<td class="posdesc bold italic"><?php echo ($isCharge ? "Charge" : "Allowance") ?></td>
<td class="posqty">&nbsp;</td>
<td class="posunitprice italic"><?php echo number_format($actualAmount, 2); ?> (<?php echo number_format($grossAmount, 2); ?>) <?php echo $invoiceCurrency; ?></td>
</tr>
<?php } while ($document->nextDocumentPositionGrossPriceAllowanceCharge()); ?>
<?php } ?>
<?php $isfirstposition = false; ?>
<?php } while ($document->nextDocumentPosition()); ?>
<?php } ?>
<!--
Allowance/Charge
-->
<?php if ($document->firstDocumentAllowanceCharge()) { ?>
<tr>
<td colspan="6">&nbsp;</td>
</tr>
<tr>
<td colspan="3">&nbsp;</td>
<td colspan="3" class="bold fs-11 space">Allowance/Charge</td>
</tr>
<?php $isFirstDocumentAllowanceCharge = true; ?>
<?php do { ?>
<?php $document->getDocumentAllowanceCharge($actualAmount, $isCharge, $taxCategoryCode, $taxTypeCode, $rateApplicablePercent, $sequence, $calculationPercent, $basisAmount, $basisQuantity, $basisQuantityUnitCode, $reasonCode, $reason); ?>
<tr>
<td class="<?php echo $isFirstDocumentAllowanceCharge ? 'space' : ''; ?>" colspan="3">&nbsp;</td>
<td class="<?php echo $isFirstDocumentAllowanceCharge ? 'space' : ''; ?> totalname"><?php echo $reason ? $reason : ($isCharge ? "Charge" : "Allowance"); ?></td>
<td class="<?php echo $isFirstDocumentAllowanceCharge ? 'space' : ''; ?> totalvalue"><?php echo number_format($basisAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
<td class="<?php echo $isFirstDocumentAllowanceCharge ? 'space' : ''; ?> totalvalue bold"><?php echo number_format($actualAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<?php $isFirstDocumentAllowanceCharge = false; ?>
<?php } while ($document->nextDocumentAllowanceCharge()); ?>
<?php } ?>
<!--
Summmation
-->
<?php $document->getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount); ?>
<tr>
<td colspan="6">&nbsp;</td>
</tr>
<tr>
<td colspan="3">&nbsp;</td>
<td colspan="3" class="bold fs-11 space">Summe</td>
</tr>
<tr>
<td class="space" colspan="3">&nbsp;</td>
<td class="space totalname" colspan="2">Nettobetrag</td>
<td class="space totalvalue"><?php echo number_format($lineTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<?php if($chargeTotalAmount != 0) { ?>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname" colspan="2">Summe Aufschläge</td>
<td class="totalvalue"><?php echo number_format($chargeTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<?php } ?>
<?php if($allowanceTotalAmount != 0) { ?>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname" colspan="2">Summe Rabatte</td>
<td class="totalvalue"><?php echo number_format($allowanceTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<?php } ?>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname" colspan="2">MwSt.</td>
<td class="totalvalue"><?php echo number_format($taxTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname bold" colspan="2">Bruttosumme</td>
<td class="totalvalue bold"><?php echo number_format($grandTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname bold" colspan="2">Bereits gezahlt</td>
<td class="totalvalue bold"><?php echo number_format($totalPrepaidAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname bold" colspan="2">Zu Zahlen</td>
<td class="totalvalue bold"><?php echo number_format($duePayableAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<!--
VAT Summation
-->
<?php if($document->firstDocumentTax()) { ?>
<tr>
<td colspan="6">&nbsp;</td>
</tr>
<tr>
<td colspan="3">&nbsp;</td>
<td colspan="3" class="bold fs-11">VAT Breakdown</td>
</tr>
<?php $isfirsttax = true ?>
<?php $sumbasisamount = 0.0 ?>
<?php do { ?>
<?php $document->getDocumentTax($categoryCode, $typeCode, $basisAmount, $calculatedAmount, $rateApplicablePercent, $exemptionReason, $exemptionReasonCode, $lineTotalBasisAmount, $allowanceChargeBasisAmount, $taxPointDate, $dueDateTypeCode); ?>
<tr>
<td class="<?php echo $isfirsttax ? 'space' : '' ?>" colspan="3">&nbsp;</td>
<td class="totalname<?php echo $isfirsttax ? ' space' : '' ?>"><?php echo number_format($rateApplicablePercent, 2); ?>%</td>
<td class="totalvalue<?php echo $isfirsttax ? ' space' : '' ?>"><?php echo number_format($basisAmount,2) ?> <?php echo $invoiceCurrency; ?></td>
<td class="totalvalue bold<?php echo $isfirsttax ? ' space' : '' ?>"><?php echo number_format($calculatedAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<?php $sumbasisamount = $sumbasisamount + $basisAmount ?>
<?php $isfirsttax = false ?>
<?php } while ($document->nextDocumentTax()); ?>
<tr>
<td class="" colspan="3">&nbsp;</td>
<td class="totalname">Summe</td>
<td class="totalvalue"><?php echo number_format($sumbasisamount, 2); ?> <?php echo $invoiceCurrency; ?></td>
<td class="totalvalue bold"><?php echo number_format($taxTotalAmount, 2); ?> <?php echo $invoiceCurrency; ?></td>
</tr>
<?php } ?>
<!--
Paymentterms
-->
<?php if ($document->firstDocumentPaymentTerms()) { ?>
<?php $isfirstpaymentterm = true ?>
<?php do { ?>
<tr>
<?php $document->getDocumentPaymentTerm($description, $dueDate, $directDebitMandateID); ?>
<td colspan="6" class="<?php echo $isfirstpaymentterm ? 'space3' : '' ?>">
<?php echo $description; ?>
</td>
</tr>
<?php $isfirstpaymentterm = false ?>
<?php } while ($document->nextDocumentPaymentTerms()); ?>
<?php } ?>
<tr><td colspan="6" class=""><bold>Hinweise:</bold></td></tr>
<?php $document->getDocumentNotes($documentNotes); ?>
<?php foreach ($documentNotes as $documentNote) { ?>
<tr><td colspan="6" class=""><?php echo trim(nl2br($documentNote['content'])); ?></td></tr>
<?php } ?>
</tbody>
</table>
</body>
</html>

View File

@ -184,10 +184,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('client_statement', [ClientStatementController::class, 'statement'])->name('client.statement');
Route::post('companies/purge/{company}', [MigrationController::class, 'purgeCompany'])->middleware('password_protected');
Route::post('companies/current', [CompanyController::class, 'current'])->name('companies.current');
Route::post('companies/purge/{company}', [MigrationController::class, 'purgeCompany'])->middleware('password_protected');
Route::post('companies/purge_save_settings/{company}', [MigrationController::class, 'purgeCompanySaveSettings'])->middleware('password_protected');
Route::resource('companies', CompanyController::class); // name = (companies. index / create / show / update / destroy / edit
@ -230,8 +228,8 @@ Route::post('companies/purge/{company}', [MigrationController::class, 'purgeComp
Route::resource('expenses', ExpenseController::class); // name = (expenses. index / create / show / update / destroy / edit
Route::put('expenses/{expense}/upload', [ExpenseController::class, 'upload']);
Route::post('expenses/bulk', [ExpenseController::class, 'bulk'])->name('expenses.bulk');
Route::post('export', [ExportController::class, 'index'])->name('export.index');
Route::put('edocument/upload', [ExpenseController::class, "edocument"])->name("expenses.edocument");
Route::resource('expense_categories', ExpenseCategoryController::class); // name = (expense_categories. index / create / show / update / destroy / edit
Route::post('expense_categories/bulk', [ExpenseCategoryController::class, 'bulk'])->name('expense_categories.bulk');
@ -415,7 +413,7 @@ Route::post('companies/purge/{company}', [MigrationController::class, 'purgeComp
Route::get('subscriptions/steps', [SubscriptionStepsController::class, 'index']);
Route::post('subscriptions/steps/check', [SubscriptionStepsController::class, 'check']);
Route::resource('subscriptions', SubscriptionController::class);
Route::post('subscriptions/bulk', [SubscriptionController::class, 'bulk'])->name('subscriptions.bulk');