diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index 3ab2cc90da02..0553b5dd3aec 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -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"; + } + + } } diff --git a/app/Http/Requests/Expense/EDocumentRequest.php b/app/Http/Requests/Expense/EDocumentRequest.php new file mode 100644 index 000000000000..5428eda9822d --- /dev/null +++ b/app/Http/Requests/Expense/EDocumentRequest.php @@ -0,0 +1,43 @@ +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); + + } + +} diff --git a/app/Jobs/EDocument/ImportEDocument.php b/app/Jobs/EDocument/ImportEDocument.php new file mode 100644 index 000000000000..13fb22de94f8 --- /dev/null +++ b/app/Jobs/EDocument/ImportEDocument.php @@ -0,0 +1,64 @@ +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"); + } + + } +} diff --git a/app/Services/EDocument/Imports/ZugferdEDocument.php b/app/Services/EDocument/Imports/ZugferdEDocument.php new file mode 100644 index 000000000000..443855e6b676 --- /dev/null +++ b/app/Services/EDocument/Imports/ZugferdEDocument.php @@ -0,0 +1,125 @@ +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->setPdfFontDefault("arial"); + $visualizer->setPdfPaperSize('A4-P'); + + $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 = false; + $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->getDocumentSellerTaxRegistration($taxtype); + $taxid = null; + if (array_key_exists("VA", $taxtype)) { + $taxid = $taxtype["VA"]; + } + // TODO find vendor + $vendor = Vendor::whereHas('contacts', function ($q) use($contact_email, $taxid) { + $q->where('email',$contact_email)->where('vat_number', $taxid); + })->first(); + + + if ($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->email = $contact_email; + + $vendor->save(); + $expense->vendor_id = $vendor->id; + // Vendor not found + // Handle accordingly + } + $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; + } +} diff --git a/composer.json b/composer.json index e9839b003ec1..11aa0a0f9058 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/lang/en/texts.php b/lang/en/texts.php index 758575e06cdf..43674305c77a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -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; \ No newline at end of file +return $lang; diff --git a/resources/views/edocument/xinvoice.php b/resources/views/edocument/xinvoice.php new file mode 100644 index 000000000000..338d552172d7 --- /dev/null +++ b/resources/views/edocument/xinvoice.php @@ -0,0 +1,406 @@ + + + + + +getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod); +$document->getDocumentBuyer($buyername, $buyerids, $buyerdescription); +$document->getDocumentBuyerAddress($buyeraddressline1, $buyeraddressline2, $buyeraddressline3, $buyerpostcode, $buyercity, $buyercounty, $buyersubdivision); +?> +

+
+
+
+
+
+

+

+ Invoice +

+

+ Invoice Date format("d.m.Y"); ?> +

+

+ Sehr geehrter Kunde, +

+

+ wir erlauben uns Ihnen folgende Position in Rechnung zu stellen. +

+ + + + + + + + + + + + + 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); + ?> + firstDocumentPositionNote()) { ?> + + + + + nextDocumentPositionNote()); ?> + + + + + + + firstDocumentPositionTax()) { ?> + getDocumentPositionTax($categoryCode, $typeCode, $rateApplicablePercent, $calculatedAmount, $exemptionReason, $exemptionReasonCode); ?> + + + + + + firstDocumentPositionGrossPriceAllowanceCharge()) { ?> + + getDocumentPositionGrossPrice($grossAmount, $grossBasisQuantity, $grossBasisQuantityUnitCode); ?> + getDocumentPositionGrossPriceAllowanceCharge($actualAmount, $isCharge, $calculationPercent, $basisAmount, $reason, $taxTypeCode, $taxCategoryCode, $rateApplicablePercent, $sequence, $basisQuantity, $basisQuantityUnitCode, $reasonCode); ?> + + + + + + + nextDocumentPositionGrossPriceAllowanceCharge()); ?> + + + nextDocumentPosition()); ?> + + + + + firstDocumentAllowanceCharge()) { ?> + + + + + + + + + + getDocumentAllowanceCharge($actualAmount, $isCharge, $taxCategoryCode, $taxTypeCode, $rateApplicablePercent, $sequence, $calculationPercent, $basisAmount, $basisQuantity, $basisQuantityUnitCode, $reasonCode, $reason); ?> + + + + + + + + nextDocumentAllowanceCharge()); ?> + + + + + getDocumentSummation($grandTotalAmount, $duePayableAmount, $lineTotalAmount, $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, $roundingAmount, $totalPrepaidAmount); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + firstDocumentTax()) { ?> + + + + + + + + + + + getDocumentTax($categoryCode, $typeCode, $basisAmount, $calculatedAmount, $rateApplicablePercent, $exemptionReason, $exemptionReasonCode, $lineTotalBasisAmount, $allowanceChargeBasisAmount, $taxPointDate, $dueDateTypeCode); ?> + + + + + + + + + nextDocumentTax()); ?> + + + + + + + + + + + firstDocumentPaymentTerms()) { ?> + + + + getDocumentPaymentTerm($description, $dueDate, $directDebitMandateID); ?> + + + + nextDocumentPaymentTerms()); ?> + + + getDocumentNotes($documentNotes); ?> + + + + +
Pos.BeschreibungStk.PreisMengeMwSt %
  + getDocumentPositionNote($posnoteContent, $posnoteContentCode, $posnoteSubjectCode); ?> + + +
% 
   ()
 
 Allowance/Charge
 
 
 Summe
 Nettobetrag
 Summe Aufschläge
 Summe Rabatte
 MwSt.
 Bruttosumme
 Bereits gezahlt
 Zu Zahlen
 
 VAT Breakdown
 %
 Summe
+ +
Hinweise:
+ + + diff --git a/routes/api.php b/routes/api.php index 9befaf9a07f8..f62d48b488c3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -184,10 +184,7 @@ 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 +227,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 +412,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');