mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-08 14:54:31 -04:00
several reworks + wip mindee
This commit is contained in:
parent
fc7d84dc24
commit
2a7eb83965
@ -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";
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
148
app/Services/EDocument/Imports/MindeeEDocument.php
Normal file
148
app/Services/EDocument/Imports/MindeeEDocument.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
} else {
|
||||
throw new Exception("File type 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;
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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 = [];
|
||||
array_push($documents, $document);
|
||||
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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user