several reworks + wip mindee

This commit is contained in:
paulwer 2024-06-22 18:52:25 +02:00
parent fc7d84dc24
commit 2a7eb83965
7 changed files with 221 additions and 35 deletions

View File

@ -497,7 +497,7 @@ class ExpenseController extends BaseController
$expenses = Expense::withTrashed()->find($request->ids);
if($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) {
if ($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) {
$this->expense_repo->categorize($expenses, $request->category_id);
$expenses = collect([]);
}
@ -573,7 +573,7 @@ class ExpenseController extends BaseController
*/
public function upload(UploadExpenseRequest $request, Expense $expense)
{
if (! $this->checkFeature(Account::FEATURE_DOCUMENTS)) {
if (!$this->checkFeature(Account::FEATURE_DOCUMENTS)) {
return $this->featureFailure();
}
@ -587,9 +587,8 @@ class ExpenseController extends BaseController
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 (new ImportEDocument($request->file("documents")[0]->get(), $request->file("documents")[0]->getClientOriginalName(), $request->file("documents")[0]->getMimeType()))->handle();
} else {
return "No file found";
}

View File

@ -13,6 +13,7 @@ namespace App\Jobs\EDocument;
use App\Models\Expense;
use App\Services\EDocument\Imports\ParseEDocument;
use App\Utils\TempFile;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
@ -29,7 +30,7 @@ class ImportEDocument implements ShouldQueue
public $deleteWhenMissingModels = true;
public function __construct(private readonly string $file_content, private string $file_name)
public function __construct(private readonly string $file_content, private string $file_name, private string $file_mime_type)
{
}
@ -42,6 +43,10 @@ class ImportEDocument implements ShouldQueue
*/
public function handle(): Expense
{
return (new ParseEDocument($this->file_content, $this->file_name))->run();
$file = TempFile::UploadedFileFromRaw($this->file_content, $this->file_name, $this->file_mime_type);
return (new ParseEDocument($file))->run();
}
}

View File

@ -0,0 +1,148 @@
<?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 App\Utils\Traits\SavesDocuments;
use Exception;
use Mindee\Client;
use Mindee\Product\Invoice\InvoiceV4;
use Illuminate\Http\UploadedFile;
class MindeeEDocument extends AbstractService
{
use SavesDocuments;
public ZugferdDocumentReader|string $document;
/**
* @throws Exception
*/
public function __construct(public UploadedFile $file)
{
# 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();
$api_key = config('services.mindee.api_key');
if (!$api_key)
throw new Exception('Mindee API key not configured');
// check global contingent
// TODO: add contingent for each company
$mindeeClient = new Client($api_key);
// Load a file from disk
$inputSource = $mindeeClient->sourceFromFile($this->file);
// Parse the file
$apiResponse = $mindeeClient->parse(InvoiceV4::class, $inputSource);
$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();
$documents = [];
array_push($documents, $this->file);
if ($this->file->getExtension() == "xml")
array_push($documents, TempFile::UploadedFileFromRaw($visualizer->renderPdf(), $documentno . "_visualiser.pdf", "application/pdf"));
$this->saveDocuments($documents, $expense);
$expense->saveQuietly();
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

@ -14,6 +14,7 @@ namespace App\Services\EDocument\Imports;
use App\Models\Expense;
use App\Services\AbstractService;
use Exception;
use Illuminate\Http\UploadedFile;
class ParseEDocument extends AbstractService
{
@ -21,32 +22,50 @@ class ParseEDocument extends AbstractService
/**
* @throws Exception
*/
public function __construct(private string $file_content, private string $file_name)
public function __construct(private UploadedFile $file)
{
}
/**
* Execute the service.
* the service will parse the file with all available libraries of the system and will return an expense, when possible
*
* @return Expense
* @throws \Exception
*/
public function run(): 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");
$expense = null;
// try to parse via Zugferd lib
$zugferd_exception = null;
try {
$expense = (new ZugferdEDocument($this->file))->run();
} catch (Exception $e) {
$zugferd_exception = $e;
}
} else {
throw new Exception("File type not supported");
// try to parse via mindee lib
$mindee_exception = null;
try {
$expense = (new MindeeEDocument($this->file))->run();
} catch (Exception $e) {
// ignore not available exceptions
$mindee_exception = $e;
}
// return expense, when available and supress any errors occured before
if ($expense)
return $expense;
// log exceptions and throw error
if ($zugferd_exception)
nlog("Zugferd Exception: " . $zugferd_exception->getMessage());
if ($mindee_exception)
nlog("Mindee Exception: " . $zugferd_exception->getMessage());
throw new Exception("File type not supported or issue while parsing");
}
}

View File

@ -25,6 +25,7 @@ use Exception;
use horstoeko\zugferd\ZugferdDocumentReader;
use horstoeko\zugferdvisualizer\renderer\ZugferdVisualizerLaravelRenderer;
use horstoeko\zugferdvisualizer\ZugferdVisualizer;
use Illuminate\Http\UploadedFile;
class ZugferdEDocument extends AbstractService
{
@ -34,7 +35,7 @@ class ZugferdEDocument extends AbstractService
/**
* @throws Exception
*/
public function __construct(public string $tempdocument, public string $documentname)
public function __construct(public UploadedFile $file)
{
# 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
}
@ -45,7 +46,7 @@ class ZugferdEDocument extends AbstractService
public function run(): Expense
{
$user = auth()->user();
$this->document = ZugferdDocumentReader::readAndGuessFromContent($this->tempdocument);
$this->document = ZugferdDocumentReader::readAndGuessFromContent($this->file->get());
$this->document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);
$this->document->getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount);
@ -68,10 +69,12 @@ class ZugferdEDocument extends AbstractService
$expense->currency_id = Currency::whereCode($invoiceCurrency)->first()->id;
$expense->save();
$origin_file = TempFile::UploadedFileFromRaw($this->tempdocument, $this->documentname, "application/xml");
$uploaded_file = TempFile::UploadedFileFromRaw($visualizer->renderPdf(), $documentno . "_visualiser.pdf", "application/pdf");
$this->saveDocuments([$origin_file, $uploaded_file], $expense);
$expense->save();
$documents = [];
array_push($documents, $this->file);
if ($this->file->getExtension() == "xml")
array_push($documents, TempFile::UploadedFileFromRaw($visualizer->renderPdf(), $documentno . "_visualiser.pdf", "application/pdf"));
$this->saveDocuments($documents, $expense);
$expense->saveQuietly();
if ($taxCurrency && $taxCurrency != $invoiceCurrency) {
$expense->private_notes = ctrans("texts.tax_currency_mismatch");

View File

@ -181,13 +181,12 @@ class InboundMailEngine
// check if document can be parsed to an expense
try {
$expense = (new ParseEDocument($document->get(), $document->getFilename()))->run();
$expense = (new ParseEDocument($document))->run();
// check if expense was already matched within this job and skip if true
if (array_search($expense->id, $parsed_expense_ids)) {
$this->saveDocument($document, $expense);
if (array_search($expense->id, $parsed_expense_ids))
continue;
}
array_push($parsed_expenses, $expense->id);
} catch (\Exception $err) {
@ -201,6 +200,8 @@ class InboundMailEngine
}
}
$is_imported_by_parser = array_search($expense->id, $parsed_expense_ids);
// populate missing data with data from email
if (!$expense)
$expense = ExpenseFactory::create($company->id, $company->owner()->id);
@ -217,18 +218,22 @@ class InboundMailEngine
if (!$expense->vendor_id && $expense_vendor)
$expense->vendor_id = $expense_vendor->id;
// handle documents
// save document only, when not imported by parser
$documents = [];
if ($is_imported_by_parser)
array_push($documents, $document);
// handle email document
// email document
if ($email->body_document !== null)
array_push($documents, $email->body_document);
$expense->saveQuietly();
$this->saveDocuments($documents, $expense);
if ($is_imported_by_parser)
$expense->saveQuietly();
else
$expense->save();
}
}

View File

@ -51,6 +51,13 @@ return [
'redirect' => env('MICROSOFT_REDIRECT_URI'),
],
'mindee' => [
'api_key' => env('MINDEE_API_KEY'),
'global_contingent_month' => env('MINDEE_GLOBAL_CONTINGENT_MONTH', 1000),
'company_contingent_month' => env('MINDEE_COMPANY_CONTINGENT_MONTH', 500),
'company_contingent_month_enterprise' => env('MINDEE_COMPANY_CONTINGENT_MONTH', 500),
],
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'),