1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-13 22:54:25 +01:00

Merge branch 'v5-develop' of https://github.com/invoiceninja/invoiceninja into feature-inbound-email-expenses

This commit is contained in:
paulwer 2024-03-24 12:54:55 +01:00
commit ad964ca61a
80 changed files with 1936 additions and 608 deletions

View File

@ -479,6 +479,8 @@ class CompanySettings extends BaseSettings
public $e_invoice_type = 'EN16931';
public $e_quote_type = 'OrderX_Comfort';
public $default_expense_payment_type_id = '0';
public $enable_e_invoice = false;
@ -502,6 +504,7 @@ class CompanySettings extends BaseSettings
public $enable_rappen_rounding = false;
public static $casts = [
'e_quote_type' => 'string',
'enable_rappen_rounding' => 'bool',
'use_unapplied_payment' => 'string',
'show_pdfhtml_on_mobile' => 'bool',

View File

@ -427,8 +427,11 @@ class BaseExport
protected array $task_report_keys = [
'start_date' => 'task.start_date',
'start_time' => 'task.start_time',
'end_date' => 'task.end_date',
'end_time' => 'task.end_time',
'duration' => 'task.duration',
'duration_words' => 'task.duration_words',
'rate' => 'task.rate',
'number' => 'task.number',
'description' => 'task.description',

View File

@ -11,18 +11,19 @@
namespace App\Export\CSV;
use App\Export\Decorators\Decorator;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\DateFormat;
use App\Models\Task;
use App\Models\Timezone;
use App\Transformers\TaskTransformer;
use App\Utils\Ninja;
use Illuminate\Database\Eloquent\Builder;
use League\Csv\Writer;
use App\Models\Company;
use App\Models\Timezone;
use App\Libraries\MultiDB;
use App\Models\DateFormat;
use Carbon\CarbonInterval;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use League\Csv\Writer;
use App\Export\Decorators\Decorator;
use App\Transformers\TaskTransformer;
use Illuminate\Database\Eloquent\Builder;
class TaskExport extends BaseExport
{
@ -177,19 +178,26 @@ class TaskExport extends BaseExport
foreach ($logs as $key => $item) {
if (in_array('task.start_date', $this->input['report_keys']) || in_array('start_date', $this->input['report_keys'])) {
$entity['task.start_date'] = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name)->format($date_format_default);
$carbon_object = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name);
$entity['task.start_date'] = $carbon_object->format($date_format_default);
$entity['task.start_time'] = $carbon_object->format('H:i:s');
}
if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] > 0) {
$entity['task.end_date'] = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name)->format($date_format_default);
$carbon_object = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name);
$entity['task.end_date'] = $carbon_object->format($date_format_default);
$entity['task.end_time'] = $carbon_object->format('H:i:s');
}
if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] == 0) {
$entity['task.end_date'] = ctrans('texts.is_running');
$entity['task.end_time'] = ctrans('texts.is_running');
}
if (in_array('task.duration', $this->input['report_keys']) || in_array('duration', $this->input['report_keys'])) {
$entity['task.duration'] = $task->calcDuration();
$seconds = $task->calcDuration();
$entity['task.duration'] = $seconds;
$entity['task.duration_words'] = CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans();
}
$entity = $this->decorateAdvancedFields($task, $entity);
@ -197,8 +205,12 @@ class TaskExport extends BaseExport
$this->storage_array[] = $entity;
$entity['task.start_date'] = '';
$entity['task.start_time'] = '';
$entity['task.end_date'] = '';
$entity['task.end_time'] = '';
$entity['task.duration'] = '';
$entity['task.duration_words'] = '';
}
}

View File

@ -23,7 +23,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
$invoice = $entity;
} elseif($entity->invoice) {
$invoice = $entity->invoice;
} elseif($entity->invoices()->exists()) {
} elseif(method_exists($entity, 'invoices') && $entity->invoices()->exists()) {
$invoice = $entity->invoices()->first();
}

View File

@ -200,14 +200,16 @@ class InvoiceFilters extends QueryFilters
*/
public function payable(string $client_id = ''): Builder
{
if (strlen($client_id) == 0) {
return $this->builder;
}
return $this->builder->whereIn('status_id', [Invoice::STATUS_DRAFT, Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->where('is_deleted', 0)
->where('client_id', $this->decodePrimaryKey($client_id));
return $this->builder
->where('client_id', $this->decodePrimaryKey($client_id))
->whereIn('status_id', [Invoice::STATUS_DRAFT, Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('is_deleted', 0)
->where('balance', '>', 0);
}

View File

@ -246,8 +246,6 @@ class InvoiceSum
if ($this->invoice->status_id != Invoice::STATUS_DRAFT) {
if ($this->invoice->amount != $this->invoice->balance) {
// $paid_to_date = $this->invoice->amount - $this->invoice->balance;
$this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision) - $this->invoice->paid_to_date; //21-02-2024 cannot use the calculated $paid_to_date here as it could send the balance backward.
} else {
$this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision);
@ -256,8 +254,10 @@ class InvoiceSum
/* Set new calculated total */
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
if($this->rappen_rounding)
if($this->rappen_rounding){
$this->invoice->amount = $this->roundRappen($this->invoice->amount);
$this->invoice->balance = $this->roundRappen($this->invoice->balance);
}
$this->invoice->total_taxes = $this->getTotalTaxes();

View File

@ -272,11 +272,11 @@ class InvoiceSumInclusive
}
/* Set new calculated total */
/** @todo - rappen rounding here */
$this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
if($this->rappen_rounding) {
$this->invoice->amount = $this->roundRappen($this->invoice->amount);
$this->invoice->balance = $this->roundRappen($this->invoice->balance);
}
$this->invoice->total_taxes = $this->getTotalTaxes();

View File

@ -43,7 +43,7 @@ class ContactLoginController extends Controller
if ($request->session()->has('company_key')) {
MultiDB::findAndSetDbByCompanyKey($request->session()->get('company_key'));
$company = Company::where('company_key', $request->input('company_key'))->first();
$company = Company::where('company_key', $request->session()->get('company_key'))->first();
} elseif ($request->has('company_key')) {
MultiDB::findAndSetDbByCompanyKey($request->input('company_key'));
$company = Company::where('company_key', $request->input('company_key'))->first();

View File

@ -390,13 +390,20 @@ class LoginController extends BaseController
$truth->setUser($user);
$truth->setCompany($set_company);
$user->account->companies->each(function ($company) use ($user) {
if ($company->tokens()->where('is_system', true)->count() == 0) {
(new CreateCompanyToken($company, $user, request()->server('HTTP_USER_AGENT')))->handle();
//21-03-2024
$cu->each(function ($cu){
if(CompanyToken::where('company_id', $cu->company_id)->where('user_id', $cu->user_id)->where('is_system', true)->doesntExist()){
(new CreateCompanyToken($cu->company, $cu->user, request()->server('HTTP_USER_AGENT')))->handle();
}
});
$truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->first());
// $user->account->companies->each(function ($company) use ($user) {
// if ($company->tokens()->where('user_id',$user->id)->where('is_system', true)->count() == 0) {
// (new CreateCompanyToken($company, $user, request()->server('HTTP_USER_AGENT')))->handle();
// }
// });
$truth->setCompanyToken(CompanyToken::where('user_id', $user->id)->where('company_id', $set_company->id)->where('is_system', true)->first());
return CompanyUser::query()->where('user_id', $user->id);
}

View File

@ -90,14 +90,13 @@ class YodleeController extends BaseController
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->from_date = now()->subYear();
$bank_integration->integration_type = BankIntegration::INTEGRATION_TYPE_YODLEE;
$bank_integration->auto_sync = true;
$bank_integration->save();
}
}
$company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->each(function ($bank_integration) use ($company) { // TODO: filter to yodlee only
ProcessBankTransactionsYodlee::dispatch($company->account->id, $bank_integration);
});

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -12,17 +13,20 @@
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use App\Models\Invoice;
class DashboardController extends Controller
{
/**
* @return Factory|View
*/
public function index()
public function index(): \Illuminate\View\View
{
return redirect()->route('client.invoices.index');
//return $this->render('dashboard.index');
$total_invoices = Invoice::withTrashed()
->where('client_id', auth()->guard('contact')->user()->client_id)
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID])
->sum('amount');
return $this->render('dashboard.index', [
'total_invoices' => $total_invoices,
]);
}
}

View File

@ -727,6 +727,74 @@ class CreditController extends BaseController
}, $credit->numberFormatter() . '.pdf', $headers);
}
/**
* @OA\Get(
* path="/api/v1/credit/{invitation_key}/download_e_credit",
* operationId="downloadXcredit",
* tags={"credit"},
* summary="Download a specific x-credit by invitation key",
* description="Downloads a specific x-credit",
* @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 credit Invitation Key",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the x-credit 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 downloadECredit($invitation_key)
{
$invitation = $this->credit_repository->getInvitationByKey($invitation_key);
if (! $invitation) {
return response()->json(['message' => 'no record found'], 400);
}
$contact = $invitation->contact;
$credit = $invitation->credit;
$file = $credit->service()->getEInvoice($contact);
$file_name = $credit->getFileName("xml");
$headers = ['Content-Type' => 'application/xml'];
if (request()->input('inline') == 'true') {
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
}
return response()->streamDownload(function () use ($file) {
echo $file;
}, $file_name, $headers);
}
/**
* Update the specified resource in storage.

View File

@ -851,4 +851,71 @@ class PurchaseOrderController extends BaseController
echo $file;
}, $purchase_order->numberFormatter().".pdf", $headers);
}
/**
* @OA\Get(
* path="/api/v1/credit/{invitation_key}/download_e_purchase_order",
* operationId="downloadEPurchaseOrder",
* tags={"purchase_orders"},
* summary="Download a specific E-Purchase-Order by invitation key",
* description="Downloads a specific E-Purchase-Order",
* @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 E-Purchase-Order Invitation Key",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the E-Purchase-Order 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 downloadEPurchaseOrder($invitation_key)
{
$invitation = $this->purchase_order_repository->getInvitationByKey($invitation_key);
if (! $invitation) {
return response()->json(['message' => 'no record found'], 400);
}
$contact = $invitation->contact;
$purchase_order = $invitation->purchase_order;
$file = $purchase_order->service()->getEPurchaseOrder($contact);
$file_name = $purchase_order->getFileName("xml");
$headers = ['Content-Type' => 'application/xml'];
if (request()->input('inline') == 'true') {
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
}
return response()->streamDownload(function () use ($file) {
echo $file;
}, $file_name, $headers);
}
}

View File

@ -860,6 +860,75 @@ class QuoteController extends BaseController
}
/**
* @OA\Get(
* path="/api/v1/invoice/{invitation_key}/download_e_quote",
* operationId="downloadXQuote",
* tags={"quotes"},
* summary="Download a specific x-quote by invitation key",
* description="Downloads a specific x-quote",
* @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 Quote Invitation Key",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the x-quote 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 downloadEQuote($invitation_key)
{
$invitation = $this->quote_repo->getInvitationByKey($invitation_key);
if (! $invitation) {
return response()->json(['message' => 'no record found'], 400);
}
$contact = $invitation->contact;
$quote = $invitation->quote;
$file = $quote->service()->getEInvoice($contact);
$file_name = $quote->getFileName("xml");
$headers = ['Content-Type' => 'application/xml'];
if (request()->input('inline') == 'true') {
$headers = array_merge($headers, ['Content-Disposition' => 'inline']);
}
return response()->streamDownload(function () use ($file) {
echo $file;
}, $file_name, $headers);
}
/**
* Update the specified resource in storage.
*

View File

@ -260,8 +260,8 @@ class UserController extends BaseController
/** @var \App\Models\User $logged_in_user */
$logged_in_user = auth()->user();
$company_user = CompanyUser::whereUserId($user->id)
->whereCompanyId($logged_in_user->companyId())
$company_user = CompanyUser::where('user_id',$user->id)
->where('company_id', $logged_in_user->companyId())
->withTrashed()
->first();
@ -269,14 +269,10 @@ class UserController extends BaseController
return response()->json(['message', 'Cannot detach owner.'], 401);
}
$token = $company_user->token->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->first();
if ($token) {
$token->delete();
}
$company_user->tokens()->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->forceDelete();
if ($company_user) {
$company_user->delete();
$company_user->forceDelete();
}
return response()->json(['message' => ctrans('texts.user_detached')], 200);

View File

@ -57,8 +57,6 @@ class RefundPaymentRequest extends Request
if (isset($input['credits'])) {
unset($input['credits']);
// foreach($input['credits'] as $key => $credit)
// $input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($credit['credit_id']);
}
$this->replace($input);

View File

@ -16,7 +16,6 @@ use App\Http\ValidationRules\Credit\CreditsSumRule;
use App\Http\ValidationRules\Credit\ValidCreditsRules;
use App\Http\ValidationRules\Payment\ValidInvoicesRules;
use App\Http\ValidationRules\PaymentAmountsBalanceRule;
use App\Http\ValidationRules\ValidCreditsPresentRule;
use App\Http\ValidationRules\ValidPayableInvoicesRule;
use App\Models\Payment;
use App\Utils\Traits\MakesHash;
@ -39,6 +38,41 @@ class StorePaymentRequest extends Request
return $user->can('create', Payment::class);
}
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$rules = [
'client_id' => ['bail','required',Rule::exists('clients','id')->where('company_id',$user->company()->id)->where('is_deleted', 0)],
'amount' => ['bail', 'numeric', new PaymentAmountsBalanceRule()],
'invoices.*.amount' => ['bail','required'],
'invoices.*.invoice_id' => ['bail','required','distinct', new ValidInvoicesRules($this->all()),Rule::exists('invoices','id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'credits.*.credit_id' => ['bail','required','distinct', new ValidCreditsRules($this->all()),Rule::exists('credits','id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'credits.*.amount' => ['bail','required', new CreditsSumRule($this->all())],
'invoices' => ['bail','sometimes', 'nullable', 'array', new ValidPayableInvoicesRule()],
'number' => ['bail', 'nullable', Rule::unique('payments')->where('company_id', $user->company()->id)],
'idempotency_key' => ['nullable', 'bail', 'string','max:64', Rule::unique('payments')->where('company_id', $user->company()->id)],
];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) {
$rules['documents'] = $this->fileValidation();
}else {
$rules['documents'] = 'bail|sometimes|array';
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->fileValidation();
} elseif ($this->file('file')) {
$rules['file'] = $this->fileValidation();
}
return $rules;
}
public function prepareForValidation()
{
@ -78,7 +112,6 @@ class StorePaymentRequest extends Request
foreach ($input['credits'] as $key => $value) {
if (array_key_exists('credit_id', $input['credits'][$key])) {
$input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($value['credit_id']);
$credits_total += $value['amount'];
}
}
@ -103,39 +136,5 @@ class StorePaymentRequest extends Request
$this->replace($input);
}
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$rules = [
'amount' => ['numeric', 'bail', new PaymentAmountsBalanceRule(), new ValidCreditsPresentRule($this->all())],
'client_id' => 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0',
'invoices.*.invoice_id' => 'bail|required|distinct|exists:invoices,id',
'invoices.*.amount' => 'bail|required',
'invoices.*.invoice_id' => new ValidInvoicesRules($this->all()),
'credits.*.credit_id' => 'bail|required|exists:credits,id',
'credits.*.credit_id' => new ValidCreditsRules($this->all()),
'credits.*.amount' => ['bail','required', new CreditsSumRule($this->all())],
'invoices' => new ValidPayableInvoicesRule(),
'number' => ['nullable', 'bail', Rule::unique('payments')->where('company_id', $user->company()->id)],
'idempotency_key' => ['nullable', 'bail', 'string','max:64', Rule::unique('payments')->where('company_id', $user->company()->id)],
];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) {
$rules['documents'] = $this->fileValidation();
}else {
$rules['documents'] = 'bail|sometimes|array';
}
if ($this->file('file') && is_array($this->file('file'))) {
$rules['file.*'] = $this->fileValidation();
} elseif ($this->file('file')) {
$rules['file'] = $this->fileValidation();
}
return $rules;
}
}

View File

@ -13,7 +13,6 @@ namespace App\Http\Requests\Payment;
use App\Http\Requests\Request;
use App\Http\ValidationRules\PaymentAppliedValidAmount;
use App\Http\ValidationRules\ValidCreditsPresentRule;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -41,16 +40,17 @@ class UpdatePaymentRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
$rules = [
'invoices' => ['array', new PaymentAppliedValidAmount($this->all()), new ValidCreditsPresentRule($this->all())],
'invoices.*.invoice_id' => 'distinct',
'client_id' => ['sometimes', 'bail', Rule::in([$this->payment->client_id])],
'number' => ['sometimes', 'bail', Rule::unique('payments')->where('company_id', $user->company()->id)->ignore($this->payment->id)],
'invoices' => ['sometimes', 'bail', 'nullable', 'array', new PaymentAppliedValidAmount($this->all())],
'invoices.*.invoice_id' => ['sometimes','distinct',Rule::exists('invoices','id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'invoices.*.amount' => ['sometimes','numeric','min:0'],
'credits.*.credit_id' => ['sometimes','bail','distinct',Rule::exists('credits','id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'credits.*.amount' => ['required', 'bail'],
];
if ($this->number) {
$rules['number'] = Rule::unique('payments')->where('company_id', $user->company()->id)->ignore($this->payment->id);
}
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) {
@ -74,10 +74,6 @@ class UpdatePaymentRequest extends Request
$input = $this->decodePrimaryKeys($input);
if (isset($input['client_id'])) {
unset($input['client_id']);
}
if (isset($input['amount'])) {
unset($input['amount']);
}
@ -85,7 +81,6 @@ class UpdatePaymentRequest extends Request
if (isset($input['invoices']) && is_array($input['invoices']) !== false) {
foreach ($input['invoices'] as $key => $value) {
if(isset($input['invoices'][$key]['invoice_id'])) {
// if (array_key_exists('invoice_id', $input['invoices'][$key])) {
$input['invoices'][$key]['invoice_id'] = $this->decodePrimaryKey($value['invoice_id']);
}
}
@ -93,7 +88,6 @@ class UpdatePaymentRequest extends Request
if (isset($input['credits']) && is_array($input['credits']) !== false) {
foreach ($input['credits'] as $key => $value) {
// if (array_key_exists('credits', $input['credits'][$key])) {
if (isset($input['credits'][$key]['credit_id'])) {
$input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($value['credit_id']);
}

View File

@ -57,9 +57,6 @@ class ValidRefundableRequest implements Rule
if ($payment->invoices()->exists()) {
$this->checkInvoice($payment->invoices, $request_invoices);
// foreach ($payment->invoices as $paymentable_invoice) {
// $this->checkInvoice($paymentable_invoice, $request_invoices);
// }
}
foreach ($request_invoices as $request_invoice) {

View File

@ -61,7 +61,10 @@ class PaymentAppliedValidAmount implements Rule
$payment_amounts = 0;
$invoice_amounts = 0;
$payment_amounts = $payment->amount - $payment->refunded - $payment->applied;
// $payment_amounts = $payment->amount - $payment->refunded - $payment->applied;
//20-03-2024 - applied amounts are never tainted by refunded amount.
$payment_amounts = $payment->amount - $payment->applied;
if (request()->has('credits')
&& is_array(request()->input('credits'))
@ -84,10 +87,6 @@ class PaymentAppliedValidAmount implements Rule
$inv = $inv_collection->firstWhere('id', $invoice['invoice_id']);
nlog($inv->status_id);
nlog($inv->amount);
nlog($invoice['amount']);
if($inv->status_id == Invoice::STATUS_DRAFT && $inv->amount >= $invoice['amount']) {
} elseif ($inv->balance < $invoice['amount']) {

View File

@ -17,6 +17,7 @@ use Illuminate\Contracts\Validation\Rule;
/**
* Class ValidCreditsPresentRule.
* @deprecated 20-03-2024
*/
class ValidCreditsPresentRule implements Rule
{
@ -49,11 +50,8 @@ class ValidCreditsPresentRule implements Rule
private function validCreditsPresent(): bool
{
//todo need to ensure the clients credits are here not random ones!
if (array_key_exists('credits', $this->input) && is_array($this->input['credits']) && count($this->input['credits']) > 0) {
$credit_collection = Credit::query()->whereIn('id', array_column($this->input['credits'], 'credit_id'))->count();
return $credit_collection == count($this->input['credits']);
}

View File

@ -101,9 +101,7 @@ class PortalComposer
$enabled_modules = auth()->guard('contact')->user()->company->enabled_modules;
$data = [];
// TODO: Enable dashboard once it's completed.
// $this->settings->enable_client_portal_dashboard
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
$data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
if (self::MODULE_INVOICES & $enabled_modules) {
$data[] = ['title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text'];

View File

@ -63,11 +63,14 @@ class UpdateCalculatedFields
Project::query()->with('tasks')->whereHas('tasks', function ($query) {
$query->where('updated_at', '>', now()->subHours(2));
})
->cursor()
->each(function ($project) {
$project->current_hours = $this->calculateDuration($project);
$project->save();
});
->cursor()
->each(function ($project) {
$project->current_hours = $this->calculateDuration($project);
$project->save();
});
//Clean password resets table
\DB::connection($db)->table('password_resets')->where('created_at', '<', now()->subHour())->delete();
}
}

View File

@ -0,0 +1,139 @@
<?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\Jobs\EDocument;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\App;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use horstoeko\zugferd\ZugferdDocumentBuilder;
use App\Services\EDocument\Standards\OrderXDocument;
use App\Services\EDocument\Standards\FacturaEInvoice;
use App\Services\EDocument\Standards\ZugferdEDokument;
class CreateEDocument implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $deleteWhenMissingModels = true;
public function __construct(private object $document, private bool $returnObject = false)
{
}
/**
* Execute the job.
*
* @return string|ZugferdDocumentBuilder
*/
public function handle(): string|ZugferdDocumentBuilder
{
/* Forget the singleton*/
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
$settings_entity = ($this->document instanceof PurchaseOrder) ? $this->document->vendor : $this->document->client;
App::setLocale($settings_entity->locale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->document->client->getMergedSettings()));
$e_document_type = strlen($settings_entity->getSetting('e_invoice_type')) > 2 ? $settings_entity->getSetting('e_invoice_type') : "XInvoice_3_0";
$e_quote_type = strlen($settings_entity->getSetting('e_quote_type')) > 2 ? $settings_entity->getSetting('e_quote_type') : "OrderX_Extended";
if ($this->document instanceof Invoice){
switch ($e_document_type) {
case "EN16931":
case "XInvoice_3_0":
case "XInvoice_2_3":
case "XInvoice_2_2":
case "XInvoice_2_1":
case "XInvoice_2_0":
case "XInvoice_1_0":
case "XInvoice-Extended":
case "XInvoice-BasicWL":
case "XInvoice-Basic":
$zugferd = (new ZugferdEDokument($this->document))->run();
return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml();
case "Facturae_3.2":
case "Facturae_3.2.1":
case "Facturae_3.2.2":
return (new FacturaEInvoice($this->document, str_replace("Facturae_", "", $e_document_type)))->run();
default:
$zugferd = (new ZugferdEDokument($this->document))->run();
return $this->returnObject ? $zugferd : $zugferd->getXml();
}
}
elseif ($this->document instanceof Quote){
switch ($e_quote_type){
case "OrderX_Basic":
case "OrderX_Comfort":
case "OrderX_Extended":
$orderx = (new OrderXDocument($this->document))->run();
return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml();
default:
$orderx = (new OrderXDocument($this->document))->run();
return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml();
}
}
elseif ($this->document instanceof PurchaseOrder){
switch ($e_quote_type){
case "OrderX_Basic":
case "OrderX_Comfort":
case "OrderX_Extended":
$orderx = (new OrderXDocument($this->document))->run();
return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml();
default:
$orderx = (new OrderXDocument($this->document))->run();
return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml();
}
}
elseif ($this->document instanceof Credit) {
switch ($e_document_type) {
case "EN16931":
case "XInvoice_3_0":
case "XInvoice_2_3":
case "XInvoice_2_2":
case "XInvoice_2_1":
case "XInvoice_2_0":
case "XInvoice_1_0":
case "XInvoice-Extended":
case "XInvoice-BasicWL":
case "XInvoice-Basic":
$zugferd = (new ZugferdEDokument($this->document))->run();
return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml();
default:
$zugferd = (new ZugferdEDokument($this->document))->run();
return $this->returnObject ? $zugferd : $zugferd->getXml();
}
}
else{
return "";
}
}
}

View File

@ -1,86 +0,0 @@
<?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\Jobs\Invoice;
use App\Models\Invoice;
use App\Services\Invoice\EInvoice\FacturaEInvoice;
use App\Services\Invoice\EInvoice\ZugferdEInvoice;
use App\Utils\Ninja;
use horstoeko\zugferd\ZugferdDocumentBuilder;
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\App;
class CreateEInvoice implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $deleteWhenMissingModels = true;
public function __construct(private Invoice $invoice, private bool $returnObject = false)
{
}
/**
* Execute the job.
*
* @return string|ZugferdDocumentBuilder
*/
public function handle(): string|ZugferdDocumentBuilder
{
/* Forget the singleton*/
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->invoice->client->locale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->invoice->client->getMergedSettings()));
$e_invoice_type = $this->invoice->client->getSetting('e_invoice_type');
switch ($e_invoice_type) {
case "EN16931":
case "XInvoice_3_0":
case "XInvoice_2_3":
case "XInvoice_2_2":
case "XInvoice_2_1":
case "XInvoice_2_0":
case "XInvoice_1_0":
case "XInvoice-Extended":
case "XInvoice-BasicWL":
case "XInvoice-Basic":
$zugferd = (new ZugferdEInvoice($this->invoice))->run();
return $this->returnObject ? $zugferd->xrechnung : $zugferd->getXml();
case "Facturae_3.2":
case "Facturae_3.2.1":
case "Facturae_3.2.2":
return (new FacturaEInvoice($this->invoice, str_replace("Facturae_", "", $e_invoice_type)))->run();
default:
$zugferd = (new ZugferdEInvoice($this->invoice))->run();
return $this->returnObject ? $zugferd : $zugferd->getXml();
}
}
}

View File

@ -67,6 +67,11 @@ class ZipPurchaseOrders implements ShouldQueue
try {
foreach ($invitations as $invitation) {
if ($invitation->purchase_order->vendor->getSetting("enable_e_invoice")) {
$xml = $invitation->purchase_order->service()->getEInvoice();
$zipFile->addFromString($invitation->purchase_order->getFileName("xml"), $xml);
}
$file = (new CreateRawPdf($invitation))->handle();
$zipFile->addFromString($invitation->purchase_order->numberFormatter().".pdf", $file);

View File

@ -63,6 +63,10 @@ class ZipQuotes implements ShouldQueue
try {
foreach ($invitations as $invitation) {
if ($invitation->quote->client->getSetting('enable_e_invoice')) {
$xml = $invitation->quote->service()->getEInvoice();
$zipFile->addFromString($invitation->quote->getFileName("xml"), $xml);
}
$file = (new \App\Jobs\Entity\CreateRawPdf($invitation))->handle();
$zipFile->addFromString($invitation->quote->numberFormatter() . '.pdf', $file);
}

View File

@ -12,7 +12,7 @@
namespace App\Livewire;
use App\Jobs\Invoice\CreateEInvoice;
use App\Jobs\EDocument\CreateEDocument;
use App\Libraries\MultiDB;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
@ -113,7 +113,7 @@ class PdfSlot extends Component
$file_name = $this->entity->numberFormatter().'.xml';
$file = (new CreateEInvoice($this->entity))->handle();
$file = (new CreateEDocument($this->entity))->handle();
$headers = ['Content-Type' => 'application/xml'];

View File

@ -222,6 +222,8 @@ class RequiredClientInfo extends Component
$this->show_form = true;
$hash = Cache::get(request()->input('hash'));
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::find($this->decodePrimaryKey($hash['invoice_id']));
$this->invoice_terms = $invoice->terms;

View File

@ -140,7 +140,7 @@ class TemplateEmail extends Mailable
'whitelabel' => $this->client->user->account->isPaid() ? true : false,
'logo' => $this->company->present()->logo($settings),
'links' => $this->build_email->getAttachmentLinks(),
'email_preferences' => (Ninja::isHosted() && in_array($settings->email_sending_method, ['default', 'mailgun'])) ? $this->company->domain() . URL::signedRoute('client.email_preferences', ['entity' => $this->invitation->getEntityString(), 'invitation_key' => $this->invitation->key], absolute: false) : false,
'email_preferences' => (Ninja::isHosted() && $this->invitation && in_array($settings->email_sending_method, ['default', 'mailgun'])) ? $this->company->domain() . URL::signedRoute('client.email_preferences', ['entity' => $this->invitation->getEntityString(), 'invitation_key' => $this->invitation->key], absolute: false) : false,
]);
foreach ($this->build_email->getAttachments() as $file) {
@ -159,15 +159,46 @@ class TemplateEmail extends Mailable
}
}
if ($this->invitation && $this->invitation->invoice && $this->invitation->invoice->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$xml_string = $this->invitation->invoice->service()->getEInvoice($this->invitation->contact);
if ($this->invitation->invoice) {
if ($this->invitation && $this->invitation->invoice && $this->invitation->invoice->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$xml_string = $this->invitation->invoice->service()->getEInvoice($this->invitation->contact);
if ($xml_string) {
$this->attachData($xml_string, $this->invitation->invoice->getEFileName("xml"));
}
if($xml_string) {
$this->attachData($xml_string, $this->invitation->invoice->getEFileName("xml"));
}
}
elseif ($this->invitation->credit){
if ($this->invitation && $this->invitation->credit && $this->invitation->credit->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$xml_string = $this->invitation->credit->service()->getECredit($this->invitation->contact);
if ($xml_string) {
$this->attachData($xml_string, $this->invitation->credit->getEFileName("xml"));
}
}
}
elseif ($this->invitation->quote){
if ($this->invitation && $this->invitation->quote && $this->invitation->quote->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$xml_string = $this->invitation->quote->service()->getEQuote($this->invitation->contact);
if ($xml_string) {
$this->attachData($xml_string, $this->invitation->quote->getEFileName("xml"));
}
}
}
elseif ($this->invitation->purchase_order){
if ($this->invitation && $this->invitation->purchase_order && $this->invitation->purchase_order->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$xml_string = $this->invitation->purchase_order->service()->getEPurchaseOrder($this->invitation->contact);
if ($xml_string) {
$this->attachData($xml_string, $this->invitation->purchase_order->getEFileName("xml"));
}
}
}
return $this;
}
}

View File

@ -754,7 +754,7 @@ class Client extends BaseModel implements HasLocalePreference
return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/invoices/';
}
public function e_invoice_filepath($invitation): string
public function e_document_filepath($invitation): string
{
$contact_key = $invitation->contact->contact_key;

View File

@ -202,7 +202,6 @@ class CompanyUser extends Pivot
*/
public function portalType(): bool
{
nlog(isset($this->react_settings->react_notification_link) && $this->react_settings->react_notification_link);
return isset($this->react_settings->react_notification_link) && $this->react_settings->react_notification_link;
}

View File

@ -698,7 +698,7 @@ class RecurringInvoice extends BaseModel
public function subscription(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Subscription::class);
return $this->belongsTo(Subscription::class)->withTrashed();
}
public function translate_entity()

View File

@ -12,6 +12,7 @@
namespace App\Models;
use App\Utils\Traits\MakesHash;
use Carbon\CarbonInterval;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
@ -248,6 +249,7 @@ class Task extends BaseModel
$duration += max($end_time - $start_time, 0);
}
// return CarbonInterval::seconds(round($duration))->locale($this->company->locale())->cascade()->forHumans();
return round($duration);
}

View File

@ -226,6 +226,7 @@ class User extends Authenticatable implements MustVerifyEmail
return $truth->getCompanyToken();
}
// if (request()->header('X-API-TOKEN')) {
if (request()->header('X-API-TOKEN')) {
return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first();
}

View File

@ -41,6 +41,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property string|null $phone
* @property string|null $private_notes
* @property string|null $website
* @property string|null $routing_id
* @property bool $is_deleted
* @property string|null $vat_number
* @property string|null $transaction_name

View File

@ -126,14 +126,14 @@ class BaseDriver extends AbstractPaymentDriver
$fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required'];
}
if ($this->company_gateway->require_contact_name) {
// if ($this->company_gateway->require_contact_name) {
$fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required'];
}
// }
if ($this->company_gateway->require_contact_email) {
// if ($this->company_gateway->require_contact_email) {
$fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc'];
}
// }
if ($this->company_gateway->require_client_phone) {
$fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required'];
@ -579,15 +579,18 @@ class BaseDriver extends AbstractPaymentDriver
$nmo->company = $this->client->company;
$nmo->settings = $this->client->company->settings;
$invoices = Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
if (! $invitation->contact->trashed()) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
}
});
if($this->payment_hash)
{
$invoices = Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
if (! $invitation->contact->trashed()) {
$nmo->to_user = $invitation->contact;
NinjaMailerJob::dispatch($nmo);
}
});
}
$message = [
'server_response' => $response,
'data' => $this->payment_hash->data,

View File

@ -292,7 +292,32 @@ class PayPalPPCPPaymentDriver extends BaseDriver
}
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
try {
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
} catch(\Exception $e) {
//Rescue for duplicate invoice_id
if(stripos($e->getMessage(), 'DUPLICATE_INVOICE_ID') !== false) {
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$new_invoice_number = $invoice->number."_".Str::random(5);
$update_data =
[[
"op" => "replace",
"path" => "/purchase_units/@reference_id=='default'/invoice_id",
"value" => $new_invoice_number,
]];
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $update_data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
}
}
$response = $r;

View File

@ -18,6 +18,7 @@ use App\Models\Invoice;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Support\Str;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
@ -211,7 +212,8 @@ class PayPalRestPaymentDriver extends BaseDriver
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
$response = json_decode($request['gateway_response'], true);
// nlog($response);
//capture
$orderID = $response['orderID'];
@ -235,7 +237,33 @@ class PayPalRestPaymentDriver extends BaseDriver
}
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
try{
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
}
catch(\Exception $e) {
//Rescue for duplicate invoice_id
if(stripos($e->getMessage(), 'DUPLICATE_INVOICE_ID') !== false){
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$new_invoice_number = $invoice->number."_".Str::random(5);
$update_data =
[[
"op" => "replace",
"path" => "/purchase_units/@reference_id=='default'/invoice_id",
"value" => $new_invoice_number,
]];
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $update_data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
}
}
$response = $r;

View File

@ -252,6 +252,19 @@ class PaytracePaymentDriver extends BaseDriver
return false;
}
public function getClientRequiredFields(): array
{
$fields = parent::getClientRequiredFields();
$fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
return $fields;
}
public function auth(): bool
{
try {

View File

@ -109,15 +109,36 @@ class ACH
public function verificationView(ClientGatewayToken $token)
{
if (isset($token->meta->state) && $token->meta->state === 'authorized') {
return redirect()
->route('client.payment_methods.show', $token->hashed_id)
->with('message', __('texts.payment_method_verified'));
}
//double check here if we need to show the verification view.
$this->stripe->init();
if(substr($token->token,0,2) == 'pm'){
$pm = $this->stripe->getStripePaymentMethod($token->token);
if(!$pm->customer){
$meta = $token->meta;
$meta->state = 'unauthorized';
$token->meta = $meta;
$token->save();
return redirect()
->route('client.payment_methods.show', $token->hashed_id);
}
if (isset($token->meta->state) && $token->meta->state === 'authorized') {
return redirect()
->route('client.payment_methods.show', $token->hashed_id)
->with('message', __('texts.payment_method_verified'));
}
if($token->meta->next_action)
return redirect($token->meta->next_action);
}
$bank_account = Customer::retrieveSource($token->gateway_customer_reference, $token->token, [], $this->stripe->stripe_connect_auth);
/* Catch externally validated bank accounts and mark them as verified */
@ -319,6 +340,9 @@ class ACH
$data['message'] = 'Too many requests made to the API too quickly';
break;
case $e instanceof InvalidRequestException:
return redirect()->route('client.payment_methods.verification', ['payment_method' => $cgt->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
$data['message'] = 'Invalid parameters were supplied to Stripe\'s API';
break;
case $e instanceof AuthenticationException:

View File

@ -332,14 +332,14 @@ class StripePaymentDriver extends BaseDriver
$fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required'];
}
if ($this->company_gateway->require_contact_name) {
// if ($this->company_gateway->require_contact_name) {
$fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required'];
}
// }
if ($this->company_gateway->require_contact_email) {
// if ($this->company_gateway->require_contact_email) {
$fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc'];
}
// }
if ($this->company_gateway->require_client_phone) {
$fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required'];

View File

@ -206,7 +206,8 @@ class UserRepository extends BaseRepository
->first();
$cu->restore();
$cu->tokens()->restore();
event(new UserWasRestored($user, auth()->user(), auth()->user()->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}

View File

@ -12,6 +12,7 @@
namespace App\Services\Credit;
use App\Factory\PaymentFactory;
use App\Jobs\EDocument\CreateEDocument;
use App\Models\Credit;
use App\Models\Payment;
use App\Models\PaymentType;
@ -37,6 +38,11 @@ class CreditService
return (new GetCreditPdf($invitation))->run();
}
public function getECredit($contact = null)
{
return (new CreateEDocument($this->credit))->handle();
}
/**
* Applies the invoice number.
* @return $this InvoiceService object
@ -232,6 +238,27 @@ class CreditService
return $this;
}
public function deleteECredit()
{
$this->credit->load('invitations');
$this->credit->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->credit->client->e_document_filepath($invitation).$this->credit->getFileName("xml"));
// }
// if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) {
if (Ninja::isHosted()) {
Storage::disk('public')->delete($this->credit->client->e_document_filepath($invitation).$this->credit->getFileName("xml"));
}
} catch (\Exception $e) {
nlog($e->getMessage());
}
});
return $this;
}
public function triggeredActions($request)
{
$this->credit = (new TriggeredActions($this->credit, $request))->run();

View File

@ -9,7 +9,7 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Invoice\EInvoice;
namespace App\Services\EDocument\Standards;
use App\Models\Invoice;
use App\Models\PaymentType;

View File

@ -9,7 +9,7 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Invoice\EInvoice;
namespace App\Services\EDocument\Standards;
use App\Models\Invoice;
use App\Services\AbstractService;

View File

@ -0,0 +1,250 @@
<?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\EDocument\Standards;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Product;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Services\AbstractService;
use horstoeko\orderx\codelists\OrderDocumentTypes;
use horstoeko\orderx\codelists\OrderDutyTaxFeeCategories;
use horstoeko\orderx\OrderDocumentBuilder;
use horstoeko\orderx\OrderProfiles;
class OrderXDocument extends AbstractService
{
public OrderDocumentBuilder $orderxdocument;
public function __construct(public object $document, private readonly bool $returnObject = false, private array $tax_map = [])
{
}
public function run(): self
{
$company = $this->document->company;
$settings_entity = ($this->document instanceof PurchaseOrder) ? $this->document->vendor : $this->document->client;
$profile = $settings_entity->getSetting('e_quote_type') ? $settings_entity->getSetting('e_quote_type') : "OrderX_Extended";
$profile = match ($profile) {
"OrderX_Basic" => OrderProfiles::PROFILE_BASIC,
"OrderX_Comfort" => OrderProfiles::PROFILE_COMFORT,
"OrderX_Extended" => OrderProfiles::PROFILE_EXTENDED,
default => OrderProfiles::PROFILE_EXTENDED,
};
$this->orderxdocument = OrderDocumentBuilder::CreateNew($profile);
$this->orderxdocument
->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($this->document->user->present()->getFullName(), "", $this->document->user->present()->phone(), "", $this->document->user->email)
->setDocumentBuyer($settings_entity->present()->name(), $settings_entity->number)
->setDocumentBuyerAddress($settings_entity->address1, "", "", $settings_entity->postal_code, $settings_entity->city, $settings_entity->country->iso_3166_2, $settings_entity->state)
->setDocumentBuyerContact($settings_entity->present()->primary_contact_name(), "", $settings_entity->present()->phone(), "", $settings_entity->present()->email())
->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->document->date ?? now()->format('Y-m-d'))->diff(date_create($this->document->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->document->due_date]));
if (!empty($this->document->public_notes)) {
$this->orderxdocument->addDocumentNote($this->document->public_notes ?? '');
}
// Document type
$document_class = get_class($this->document);
switch ($document_class){
case Quote::class:
// Probably wrong file code https://github.com/horstoeko/zugferd/blob/master/src/codelists/ZugferdInvoiceType.php
if (empty($this->document->number)) {
$this->orderxdocument->setDocumentInformation("DRAFT", OrderDocumentTypes::ORDER, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode());
$this->orderxdocument->setIsTestDocument(true);
} else {
$this->orderxdocument->setDocumentInformation($this->document->number, OrderDocumentTypes::ORDER, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode());
};
break;
case PurchaseOrder::class:
if (empty($this->document->number)) {
$this->orderxdocument->setDocumentInformation("DRAFT", OrderDocumentTypes::ORDER_RESPONSE, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode());
$this->orderxdocument->setIsTestDocument(true);
} else {
$this->orderxdocument->setDocumentInformation($this->document->number, OrderDocumentTypes::ORDER_RESPONSE, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode());
}
break;
}
if (isset($this->document->po_number)) {
$this->orderxdocument->setDocumentBuyerOrderReferencedDocument($this->document->po_number);
}
if (empty($settings_entity->routing_id)) {
$this->orderxdocument->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference"));
} else {
$this->orderxdocument->setDocumentBuyerReference($settings_entity->routing_id);
}
if (isset($settings_entity->shipping_address1) && $settings_entity->shipping_country) {
$this->orderxdocument->setDocumentShipToAddress($settings_entity->shipping_address1, $settings_entity->shipping_address2, "", $settings_entity->shipping_postal_code, $settings_entity->shipping_city, $settings_entity->shipping_country->iso_3166_2, $settings_entity->shipping_state);
}
$this->orderxdocument->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment"));
if (str_contains($company->getSetting('vat_number'), "/")) {
$this->orderxdocument->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number'));
} else {
$this->orderxdocument->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number'));
}
$invoicing_data = $this->document->calc();
//Create line items and calculate taxes
foreach ($this->document->line_items as $index => $item) {
/** @var \App\DataMapper\InvoiceItem $item **/
$this->orderxdocument->addNewPosition($index)
->setDocumentPositionGrossPrice($item->gross_line_total)
->setDocumentPositionNetPrice($item->line_total);
if (!empty($item->product_key)) {
if (!empty($item->notes)) {
$this->orderxdocument->setDocumentPositionProductDetails($item->product_key, $item->notes);
} else {
$this->orderxdocument->setDocumentPositionProductDetails($item->product_key);
}
} else {
if (!empty($item->notes)) {
$this->orderxdocument->setDocumentPositionProductDetails($item->notes);
} else {
$this->orderxdocument->setDocumentPositionProductDetails("no product name defined");
}
}
// TODO: add item classification (kg, m^3, ...)
// if (isset($item->task_id)) {
// $this->orderxdocument->setDocumentPositionQuantity($item->quantity, "HUR");
// } else {
// $this->orderxdocument->setDocumentPositionQuantity($item->quantity, "H87");
// }
$linenetamount = $item->line_total;
if ($item->discount > 0) {
if ($this->document->is_amount_discount) {
$linenetamount -= $item->discount;
} else {
$linenetamount -= $linenetamount * ($item->discount / 100);
}
}
$this->orderxdocument->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))) {
$taxtype = $this->getTaxType($item->tax_id);
if (!empty($item->tax_name1)) {
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1);
$this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate1);
} elseif (!empty($item->tax_name2)) {
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate2);
$this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate2);
} elseif (!empty($item->tax_name3)) {
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate3);
$this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate3);
} else {
nlog("Can't add correct tax position");
}
} else {
if (!empty($this->document->tax_name1)) {
$taxtype = $this->getTaxType($this->document->tax_name1);
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate1);
$this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate1);
} elseif (!empty($this->document->tax_name2)) {
$taxtype = $this->getTaxType($this->document->tax_name2);
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate2);
$this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate2);
} elseif (!empty($this->document->tax_name3)) {
$taxtype = $this->getTaxType($this->document->tax_name3);
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate3);
$this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate3);
} else {
$taxtype = OrderDutyTaxFeeCategories::ZERO_RATED_GOODS;
$this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', 0);
$this->addtoTaxMap($taxtype, $linenetamount, 0);
// nlog("Can't add correct tax position");
}
}
}
$this->orderxdocument->setDocumentSummation($this->document->amount, $this->document->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), 0.0, $this->document->amount - $this->document->balance);
foreach ($this->tax_map as $item) {
$this->orderxdocument->addDocumentTax($item["tax_type"], "VAT", $item["net_amount"], $item["tax_rate"] * $item["net_amount"], $item["tax_rate"] * 100);
}
// The validity can be checked using https://portal3.gefeg.com/invoice/validation or https://e-rechnung.bayern.de/app/#/upload
return $this;
}
/**
* Returns the XML document
* in string format
*
* @return string
*/
public function getXml(): string
{
return $this->orderxdocument->getContent();
}
private function getTaxType($name): string
{
$tax_type = 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:
$tax_type = OrderDutyTaxFeeCategories::STANDARD_RATE;
break;
case Product::PRODUCT_TYPE_EXEMPT:
$tax_type = OrderDutyTaxFeeCategories::EXEMPT_FROM_TAX;
break;
case Product::PRODUCT_TYPE_ZERO_RATED:
$tax_type = OrderDutyTaxFeeCategories::ZERO_RATED_GOODS;
break;
case Product::PRODUCT_TYPE_REVERSE_TAX:
$tax_type = OrderDutyTaxFeeCategories::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($tax_type)) {
if ((in_array($this->document->company->country()->iso_3166_2, $eu_states) && in_array($this->document->client->country->iso_3166_2, $eu_states)) && $this->document->company->country()->iso_3166_2 != $this->document->client->country->iso_3166_2) {
$tax_type = OrderDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES;
} elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) {
$tax_type = OrderDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX;
} elseif ($this->document->client->country->iso_3166_2 == "ES-CN") {
$tax_type = OrderDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX;
} elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {
$tax_type = OrderDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA;
} else {
nlog("Unkown tax case for xinvoice");
$tax_type = OrderDutyTaxFeeCategories::STANDARD_RATE;
}
}
return $tax_type;
}
private function addtoTaxMap(string $tax_type, float $net_amount, float $tax_rate): void
{
$hash = hash("md5", $tax_type."-".$tax_rate);
if (array_key_exists($hash, $this->tax_map)) {
$this->tax_map[$hash]["net_amount"] += $net_amount;
} else {
$this->tax_map[$hash] = [
"tax_type" => $tax_type,
"net_amount" => $net_amount,
"tax_rate" => $tax_rate / 100
];
}
}
}

View File

@ -9,28 +9,28 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Invoice\EInvoice;
namespace App\Services\EDocument\Standards;
use App\Models\Invoice;
use App\Services\AbstractService;
use CleverIt\UBL\Invoice\Address;
use CleverIt\UBL\Invoice\ClassifiedTaxCategory;
use CleverIt\UBL\Invoice\Contact;
use CleverIt\UBL\Invoice\Country;
use CleverIt\UBL\Invoice\Generator;
use CleverIt\UBL\Invoice\Invoice as UBLInvoice;
use CleverIt\UBL\Invoice\InvoiceLine;
use CleverIt\UBL\Invoice\Item;
use CleverIt\UBL\Invoice\LegalEntity;
use CleverIt\UBL\Invoice\LegalMonetaryTotal;
use CleverIt\UBL\Invoice\Party;
use CleverIt\UBL\Invoice\PayeeFinancialAccount;
use CleverIt\UBL\Invoice\PaymentMeans;
use CleverIt\UBL\Invoice\Price;
use CleverIt\UBL\Invoice\TaxCategory;
use CleverIt\UBL\Invoice\TaxScheme;
use CleverIt\UBL\Invoice\TaxSubTotal;
use CleverIt\UBL\Invoice\TaxTotal;
use CleverIt\UBL\Invoice\PaymentMeans;
use CleverIt\UBL\Invoice\PayeeFinancialAccount;
use CleverIt\UBL\Invoice\LegalEntity;
use CleverIt\UBL\Invoice\ClassifiedTaxCategory;
use CleverIt\UBL\Invoice\Price;
class RoEInvoice extends AbstractService
{

View File

@ -9,28 +9,31 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Invoice\EInvoice;
namespace App\Services\EDocument\Standards;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Product;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Services\AbstractService;
use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories;
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;
class ZugferdEInvoice extends AbstractService
class ZugferdEDokument extends AbstractService
{
public ZugferdDocumentBuilder $xrechnung;
public ZugferdDocumentBuilder $xdocument;
public function __construct(public Invoice $invoice, private readonly bool $returnObject = false, private array $tax_map = [])
public function __construct(public object $document, private readonly bool $returnObject = false, private array $tax_map = [])
{
}
public function run(): self
{
$company = $this->invoice->company;
$client = $this->invoice->client;
$company = $this->document->company;
$client = $this->document->client;
$profile = $client->getSetting('e_invoice_type');
$profile = match ($profile) {
@ -46,123 +49,143 @@ class ZugferdEInvoice extends AbstractService
default => ZugferdProfiles::PROFILE_EN16931,
};
$this->xrechnung = ZugferdDocumentBuilder::CreateNew($profile);
$this->xdocument = ZugferdDocumentBuilder::CreateNew($profile);
$this->xrechnung
->setDocumentSupplyChainEvent(date_create($this->invoice->date ?? now()->format('Y-m-d')))
$this->xdocument
->setDocumentSupplyChainEvent(date_create($this->document->date ?? now()->format('Y-m-d')))
->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($this->invoice->user->present()->getFullName(), "", $this->invoice->user->present()->phone(), "", $this->invoice->user->email)
->setDocumentSellerContact($this->document->user->present()->getFullName(), "", $this->document->user->present()->phone(), "", $this->document->user->email)
->setDocumentBuyer($client->present()->name(), $client->number)
->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2, $client->state)
->setDocumentBuyerContact($client->present()->primary_contact_name(), "", $client->present()->phone(), "", $client->present()->email())
->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->invoice->date ?? now()->format('Y-m-d'))->diff(date_create($this->invoice->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->invoice->due_date]));
->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->document->date ?? now()->format('Y-m-d'))->diff(date_create($this->document->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->document->due_date]));
if (!empty($this->invoice->public_notes)) {
$this->xrechnung->addDocumentNote($this->invoice->public_notes ?? '');
if (!empty($this->document->public_notes)) {
$this->xdocument->addDocumentNote($this->document->public_notes ?? '');
}
if (empty($this->invoice->number)) {
$this->xrechnung->setDocumentInformation("DRAFT", "380", date_create($this->invoice->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
} else {
$this->xrechnung->setDocumentInformation($this->invoice->number, "380", date_create($this->invoice->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
// Document type
$document_class = get_class($this->document);
switch ($document_class){
case Quote::class:
// Probably wrong file code https://github.com/horstoeko/zugferd/blob/master/src/codelists/ZugferdInvoiceType.php
if (empty($this->document->number)) {
$this->xdocument->setDocumentInformation("DRAFT", "84", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
} else {
$this->xdocument->setDocumentInformation($this->document->number, "84", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
};
break;
case Invoice::class:
if (empty($this->document->number)) {
$this->xdocument->setDocumentInformation("DRAFT", "380", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
} else {
$this->xdocument->setDocumentInformation($this->document->number, "380", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
}
break;
case Credit::class:
if (empty($this->document->number)) {
$this->xdocument->setDocumentInformation("DRAFT", "389", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
} else {
$this->xdocument->setDocumentInformation($this->document->number, "389", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode());
}
}
if (isset($this->invoice->po_number)) {
$this->xrechnung->setDocumentBuyerOrderReferencedDocument($this->invoice->po_number);
if (isset($this->document->po_number)) {
$this->xdocument->setDocumentBuyerOrderReferencedDocument($this->document->po_number);
}
if (empty($client->routing_id)) {
$this->xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference"));
$this->xdocument->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference"));
} else {
$this->xrechnung->setDocumentBuyerReference($client->routing_id);
$this->xdocument->setDocumentBuyerReference($client->routing_id);
}
if (isset($client->shipping_address1) && $client->shipping_country) {
$this->xrechnung->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state);
$this->xdocument->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state);
}
$this->xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment"));
$this->xdocument->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment"));
if (str_contains($company->getSetting('vat_number'), "/")) {
$this->xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number'));
$this->xdocument->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number'));
} else {
$this->xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number'));
$this->xdocument->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number'));
}
$invoicing_data = $this->invoice->calc();
$invoicing_data = $this->document->calc();
//Create line items and calculate taxes
foreach ($this->invoice->line_items as $index => $item) {
foreach ($this->document->line_items as $index => $item) {
/** @var \App\DataMapper\InvoiceItem $item **/
$this->xrechnung->addNewPosition($index)
$this->xdocument->addNewPosition($index)
->setDocumentPositionGrossPrice($item->gross_line_total)
->setDocumentPositionNetPrice($item->line_total);
if (!empty($item->product_key)) {
if (!empty($item->notes)) {
$this->xrechnung->setDocumentPositionProductDetails($item->product_key, $item->notes);
$this->xdocument->setDocumentPositionProductDetails($item->product_key, $item->notes);
} else {
$this->xrechnung->setDocumentPositionProductDetails($item->product_key);
$this->xdocument->setDocumentPositionProductDetails($item->product_key);
}
} else {
if (!empty($item->notes)) {
$this->xrechnung->setDocumentPositionProductDetails($item->notes);
$this->xdocument->setDocumentPositionProductDetails($item->notes);
} else {
$this->xrechnung->setDocumentPositionProductDetails("no product name defined");
$this->xdocument->setDocumentPositionProductDetails("no product name defined");
}
}
if (isset($item->task_id)) {
$this->xrechnung->setDocumentPositionQuantity($item->quantity, "HUR");
if ($item->type_id == 2) {
$this->xdocument->setDocumentPositionQuantity($item->quantity, "HUR");
} else {
$this->xrechnung->setDocumentPositionQuantity($item->quantity, "H87");
$this->xdocument->setDocumentPositionQuantity($item->quantity, "H87");
}
$linenetamount = $item->line_total;
if ($item->discount > 0) {
if ($this->invoice->is_amount_discount) {
if ($this->document->is_amount_discount) {
$linenetamount -= $item->discount;
} else {
$linenetamount -= $linenetamount * ($item->discount / 100);
}
}
$this->xrechnung->setDocumentPositionLineSummation($linenetamount);
$this->xdocument->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))) {
$taxtype = $this->getTaxType($item->tax_id);
if (!empty($item->tax_name1)) {
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1);
$this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate1);
} elseif (!empty($item->tax_name2)) {
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate2);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate2);
$this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate2);
} elseif (!empty($item->tax_name3)) {
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate3);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate3);
$this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate3);
} else {
// nlog("Can't add correct tax position");
}
} else {
if (!empty($this->invoice->tax_name1)) {
$taxtype = $this->getTaxType($this->invoice->tax_name1);
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $this->invoice->tax_rate1);
$this->addtoTaxMap($taxtype, $linenetamount, $this->invoice->tax_rate1);
} elseif (!empty($this->invoice->tax_name2)) {
$taxtype = $this->getTaxType($this->invoice->tax_name2);
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $this->invoice->tax_rate2);
$this->addtoTaxMap($taxtype, $linenetamount, $this->invoice->tax_rate2);
} elseif (!empty($this->invoice->tax_name3)) {
$taxtype = $this->getTaxType($this->invoice->tax_name3);
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $this->invoice->tax_rate3);
$this->addtoTaxMap($taxtype, $linenetamount, $this->invoice->tax_rate3);
if (!empty($this->document->tax_name1)) {
$taxtype = $this->getTaxType($this->document->tax_name1);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate1);
$this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate1);
} elseif (!empty($this->document->tax_name2)) {
$taxtype = $this->getTaxType($this->document->tax_name2);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate2);
$this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate2);
} elseif (!empty($this->document->tax_name3)) {
$taxtype = $this->getTaxType($this->document->tax_name3);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate3);
$this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate3);
} else {
$taxtype = ZugferdDutyTaxFeeCategories::ZERO_RATED_GOODS;
$this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', 0);
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', 0);
$this->addtoTaxMap($taxtype, $linenetamount, 0);
// nlog("Can't add correct tax position");
}
}
}
$this->xrechnung->setDocumentSummation($this->invoice->amount, $this->invoice->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), 0.0, $this->invoice->amount - $this->invoice->balance);
$this->xdocument->setDocumentSummation($this->document->amount, $this->document->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), 0.0, $this->document->amount - $this->document->balance);
foreach ($this->tax_map as $item) {
$this->xrechnung->addDocumentTax($item["tax_type"], "VAT", $item["net_amount"], $item["tax_rate"] * $item["net_amount"], $item["tax_rate"] * 100);
$this->xdocument->addDocumentTax($item["tax_type"], "VAT", $item["net_amount"], $item["tax_rate"] * $item["net_amount"], $item["tax_rate"] * 100);
}
// The validity can be checked using https://portal3.gefeg.com/invoice/validation or https://e-rechnung.bayern.de/app/#/upload
@ -178,7 +201,7 @@ class ZugferdEInvoice extends AbstractService
*/
public function getXml(): string
{
return $this->xrechnung->getContent();
return $this->xdocument->getContent();
}
private function getTaxType($name): string
@ -204,13 +227,13 @@ class ZugferdEInvoice extends AbstractService
}
$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($tax_type)) {
if ((in_array($this->invoice->company->country()->iso_3166_2, $eu_states) && in_array($this->invoice->client->country->iso_3166_2, $eu_states)) && $this->invoice->company->country()->iso_3166_2 != $this->invoice->client->country->iso_3166_2) {
if ((in_array($this->document->company->country()->iso_3166_2, $eu_states) && in_array($this->document->client->country->iso_3166_2, $eu_states)) && $this->document->company->country()->iso_3166_2 != $this->document->client->country->iso_3166_2) {
$tax_type = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES;
} elseif (!in_array($this->invoice->client->country->iso_3166_2, $eu_states)) {
} elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) {
$tax_type = ZugferdDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX;
} elseif ($this->invoice->client->country->iso_3166_2 == "ES-CN") {
} elseif ($this->document->client->country->iso_3166_2 == "ES-CN") {
$tax_type = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX;
} elseif (in_array($this->invoice->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {
} elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {
$tax_type = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA;
} else {
nlog("Unkown tax case for xinvoice");

View File

@ -17,6 +17,8 @@ use App\Jobs\Invoice\CreateUbl;
use App\Models\Account;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\Task;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
@ -318,7 +320,7 @@ class EmailDefaults
}
}
/** E-Invoice xml file */
if ($this->email->email_object->settings->enable_e_invoice && $this->email->email_object->entity instanceof Invoice) {
if ($this->email->email_object->settings->enable_e_invoice && ! $this->email->email_object->entity instanceof PurchaseOrder) {
$xml_string = $this->email->email_object->entity->service()->getEInvoice();
if($xml_string) {

View File

@ -11,21 +11,21 @@
namespace App\Services\Invoice;
use App\Models\Task;
use App\Utils\Ninja;
use App\Events\Invoice\InvoiceWasArchived;
use App\Jobs\EDocument\CreateEDocument;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Inventory\AdjustProductInventory;
use App\Libraries\Currency\Conversion\CurrencyApi;
use App\Models\CompanyGateway;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Subscription;
use App\Models\CompanyGateway;
use Illuminate\Support\Carbon;
use App\Models\Task;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Invoice\CreateEInvoice;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use App\Events\Invoice\InvoiceWasArchived;
use App\Jobs\Inventory\AdjustProductInventory;
use App\Libraries\Currency\Conversion\CurrencyApi;
class InvoiceService
{
@ -201,7 +201,7 @@ class InvoiceService
public function getEInvoice($contact = null)
{
return (new CreateEInvoice($this->invoice))->handle();
return (new CreateEDocument($this->invoice))->handle();
}
public function sendEmail($contact = null)
@ -409,12 +409,12 @@ class InvoiceService
$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"));
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->e_document_filepath($invitation).$this->invoice->getFileName("xml"));
// }
// if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) {
if (Ninja::isHosted()) {
Storage::disk('public')->delete($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"));
Storage::disk('public')->delete($this->invoice->client->e_document_filepath($invitation).$this->invoice->getFileName("xml"));
}
} catch (\Exception $e) {
nlog($e->getMessage());

View File

@ -11,7 +11,7 @@
namespace App\Services\Pdf;
use App\Jobs\Invoice\CreateEInvoice;
use App\Jobs\EDocument\CreateEDocument;
use App\Models\Company;
use App\Models\CreditInvitation;
use App\Models\Invoice;
@ -216,7 +216,7 @@ class PdfService
{
try {
$e_rechnung = (new CreateEInvoice($this->config->entity, true))->handle();
$e_rechnung = (new CreateEDocument($this->config->entity, true))->handle();
$pdfBuilder = new ZugferdDocumentPdfBuilder($e_rechnung, $pdf);
$pdfBuilder->generateDocument();

View File

@ -11,8 +11,11 @@
namespace App\Services\PurchaseOrder;
use App\Jobs\EDocument\CreateEDocument;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Storage;
class PurchaseOrderService
{
@ -75,6 +78,32 @@ class PurchaseOrderService
return (new GetPurchaseOrderPdf($this->purchase_order, $contact))->run();
}
public function getEPurchaseOrder($contact = null)
{
return (new CreateEDocument($this->purchase_order))->handle();
}
public function deleteEPurchaseOrder()
{
$this->purchase_order->load('invitations');
$this->purchase_order->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->purchase_order->vendor->e_document_filepath($invitation).$this->purchase_order->getFileName("xml"));
// }
// if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) {
if (Ninja::isHosted()) {
Storage::disk('public')->delete($this->purchase_order->vendor->e_document_filepath($invitation).$this->purchase_order->getFileName("xml"));
}
} catch (\Exception $e) {
nlog($e->getMessage());
}
});
return $this;
}
public function setStatus($status)
{
$this->purchase_order->status_id = $status;

View File

@ -13,6 +13,7 @@ namespace App\Services\Quote;
use App\Events\Quote\QuoteWasApproved;
use App\Exceptions\QuoteConversion;
use App\Jobs\EDocument\CreateEDocument;
use App\Models\Project;
use App\Models\Quote;
use App\Repositories\QuoteRepository;
@ -72,6 +73,11 @@ class QuoteService
return (new GetQuotePdf($this->quote, $contact))->run();
}
public function getEQuote($contact = null)
{
return (new CreateEDocument($this->quote))->handle();
}
public function sendEmail($contact = null): self
{
$send_email = new SendEmail($this->quote, null, $contact);
@ -226,6 +232,27 @@ class QuoteService
return $this;
}
public function deleteEQuote()
{
$this->quote->load('invitations');
$this->quote->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->quote->client->e_document_filepath($invitation).$this->quote->getFileName("xml"));
// }
// if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) {
if (Ninja::isHosted()) {
Storage::disk('public')->delete($this->quote->client->e_document_filepath($invitation).$this->quote->getFileName("xml"));
}
} catch (\Exception $e) {
nlog($e->getMessage());
}
});
return $this;
}
/**
* Saves the quote.

View File

@ -92,7 +92,8 @@ class AccountTransformer extends EntityTransformer
'account_sms_verified' => (bool) $account->account_sms_verified,
'has_iap_plan' => (bool)$account->inapp_transaction_id,
'tax_api_enabled' => (bool) config('services.tax.zip_tax.key') ? true : false,
'nordigen_enabled' => (bool) (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')) ? true : false
'nordigen_enabled' => (bool) (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')) ? true : false,
'upload_extensions' => (string) config('ninja.upload_extensions'),
];
}

View File

@ -109,7 +109,10 @@ class UserTransformer extends EntityTransformer
$transformer = new CompanyUserTransformer($this->serializer);
$cu = $user->company_users()->whereCompanyId($user->company_id)->first();
$cu = $user->company_users()->where('company_id',$user->company_id)->first();
if(!$cu)
return null;
return $this->includeItem($cu, $transformer, CompanyUser::class);
}

View File

@ -107,6 +107,7 @@ class VendorTransformer extends EntityTransformer
'display_name' => (string) $vendor->present()->name(),
'invoicing_email' => (string) $vendor->invoicing_email ?: '',
'invoicing_domain' => (string) $vendor->invoicing_domain ?: '',
'routing_id' => (string) $vendor->routing_id ?: '',
];
}
}

View File

@ -56,6 +56,7 @@
"hashids/hashids": "^4.0",
"hedii/laravel-gelf-logger": "^8",
"horstoeko/zugferd": "^1",
"horstoeko/orderx": "^1",
"imdhemy/laravel-purchases": "^1.7",
"intervention/image": "^2.5",
"invoiceninja/inspector": "^2.0",
@ -115,7 +116,6 @@
"barryvdh/laravel-ide-helper": "^2.13",
"beyondcode/laravel-query-detector": "^1.8",
"brianium/paratest": "^7",
"fakerphp/faker": "^1.14",
"filp/whoops": "^2.7",
"friendsofphp/php-cs-fixer": "^3.14",
"laracasts/cypress": "^3.0",

201
composer.lock generated
View File

@ -4,8 +4,48 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fdea921aefca562c17db327acd8df062",
"content-hash": "a7fb762c099a95d6168ef390844bb87b",
"packages": [
{
"name": "adrienrn/php-mimetyper",
"version": "0.2.2",
"source": {
"type": "git",
"url": "https://github.com/adrienrn/php-mimetyper.git",
"reference": "702e00a604b4baed34d69730ce055e05c0f43932"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/adrienrn/php-mimetyper/zipball/702e00a604b4baed34d69730ce055e05c0f43932",
"reference": "702e00a604b4baed34d69730ce055e05c0f43932",
"shasum": ""
},
"require": {
"dflydev/apache-mime-types": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"MimeTyper\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Hussard",
"email": "adrien.ricartnoblet@gmail.com"
}
],
"description": "PHP mime type and extension mapping library: compatible with Symfony, powered by jshttp/mime-db",
"support": {
"issues": "https://github.com/adrienrn/php-mimetyper/issues",
"source": "https://github.com/adrienrn/php-mimetyper/tree/0.2.2"
},
"time": "2018-09-27T09:45:05+00:00"
},
{
"name": "afosto/yaac",
"version": "v1.5.2",
@ -2096,6 +2136,65 @@
],
"time": "2023-11-20T14:41:54+00:00"
},
{
"name": "dflydev/apache-mime-types",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-apache-mime-types.git",
"reference": "f30a57e59b7476e4c5270b6a0727d79c9c0eb861"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-apache-mime-types/zipball/f30a57e59b7476e4c5270b6a0727d79c9c0eb861",
"reference": "f30a57e59b7476e4c5270b6a0727d79c9c0eb861",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"twig/twig": "1.*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-0": {
"Dflydev\\ApacheMimeTypes": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
}
],
"description": "Apache MIME Types",
"keywords": [
"apache",
"mime",
"mimetypes"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-apache-mime-types/issues",
"source": "https://github.com/dflydev/dflydev-apache-mime-types/tree/v1.0.1"
},
"time": "2013-05-14T02:02:01+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
@ -4379,6 +4478,74 @@
},
"time": "2023-09-22T20:17:48+00:00"
},
{
"name": "horstoeko/orderx",
"version": "v1.0.20",
"source": {
"type": "git",
"url": "https://github.com/horstoeko/orderx.git",
"reference": "d8957cc0fea19b098d799a0c438a73504e7b326c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/horstoeko/orderx/zipball/d8957cc0fea19b098d799a0c438a73504e7b326c",
"reference": "d8957cc0fea19b098d799a0c438a73504e7b326c",
"shasum": ""
},
"require": {
"adrienrn/php-mimetyper": "^0.2",
"ext-simplexml": "*",
"goetas-webservices/xsd2php-runtime": "^0.2.13",
"horstoeko/stringmanagement": "^1",
"jms/serializer": "^3",
"php": "^7.3|^7.4|^8",
"setasign/fpdf": "^1",
"setasign/fpdi": "^2",
"smalot/pdfparser": "^0",
"symfony/validator": "^5|^6",
"symfony/yaml": "^5|^6"
},
"require-dev": {
"goetas-webservices/xsd2php": "^0",
"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\\orderx\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Erling",
"email": "daniel@erling.com.de",
"role": "lead"
}
],
"description": "A library for creating and reading Order-X document",
"homepage": "https://github.com/horstoeko/orderx",
"keywords": [
"electronic",
"order",
"order-x",
"orderx"
],
"support": {
"issues": "https://github.com/horstoeko/orderx/issues",
"source": "https://github.com/horstoeko/orderx/tree/v1.0.20"
},
"time": "2024-03-21T04:28:54+00:00"
},
{
"name": "horstoeko/stringmanagement",
"version": "v1.0.11",
@ -4435,16 +4602,16 @@
},
{
"name": "horstoeko/zugferd",
"version": "v1.0.36",
"version": "v1.0.37",
"source": {
"type": "git",
"url": "https://github.com/horstoeko/zugferd.git",
"reference": "0d15c305328c137365648fe1c34a584d877127fa"
"reference": "05f58ad4dbcc23d767fceb15f46b46097ffd43f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/0d15c305328c137365648fe1c34a584d877127fa",
"reference": "0d15c305328c137365648fe1c34a584d877127fa",
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/05f58ad4dbcc23d767fceb15f46b46097ffd43f1",
"reference": "05f58ad4dbcc23d767fceb15f46b46097ffd43f1",
"shasum": ""
},
"require": {
@ -4462,6 +4629,7 @@
},
"require-dev": {
"goetas-webservices/xsd2php": "^0",
"nette/php-generator": "*",
"pdepend/pdepend": "^2",
"phploc/phploc": "^7",
"phpmd/phpmd": "^2",
@ -4502,9 +4670,9 @@
],
"support": {
"issues": "https://github.com/horstoeko/zugferd/issues",
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.36"
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.37"
},
"time": "2024-03-11T04:34:59+00:00"
"time": "2024-03-24T11:31:03+00:00"
},
{
"name": "http-interop/http-factory-guzzle",
@ -11638,24 +11806,27 @@
},
{
"name": "smalot/pdfparser",
"version": "v2.9.0",
"version": "v0.19.0",
"source": {
"type": "git",
"url": "https://github.com/smalot/pdfparser.git",
"reference": "6b53144fcb24af77093d4150dd7d0dd571f25761"
"reference": "1895c17417aefa4508e35836c46da61988d61f26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/6b53144fcb24af77093d4150dd7d0dd571f25761",
"reference": "6b53144fcb24af77093d4150dd7d0dd571f25761",
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/1895c17417aefa4508e35836c46da61988d61f26",
"reference": "1895c17417aefa4508e35836c46da61988d61f26",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"ext-zlib": "*",
"php": ">=7.1",
"php": ">=5.6",
"symfony/polyfill-mbstring": "^1.18"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"symfony/phpunit-bridge": "^5.2"
},
"type": "library",
"autoload": {
"psr-0": {
@ -11683,9 +11854,9 @@
],
"support": {
"issues": "https://github.com/smalot/pdfparser/issues",
"source": "https://github.com/smalot/pdfparser/tree/v2.9.0"
"source": "https://github.com/smalot/pdfparser/tree/v0.19.0"
},
"time": "2024-03-01T09:51:10+00:00"
"time": "2021-04-13T08:27:56+00:00"
},
{
"name": "socialiteproviders/apple",

View File

@ -1,98 +0,0 @@
<?php
return [
/*
* This value will be sent along with your trace.
*
* When set to `null`, the app name will be used
*/
'default_trace_name' => null,
/*
* A driver is responsible for transmitting any measurements.
*/
'drivers' => [
Spatie\OpenTelemetry\Drivers\HttpDriver::class => [
'url' => 'http://localhost:9411/api/v2/spans',
// 'url' => 'http://localhost:4318/v1/traces'
],
],
/*
* This class determines if your measurements should actually be sent
* to the reporting drivers.
*/
'sampler' => Spatie\OpenTelemetry\Support\Samplers\AlwaysSampler::class,
/*
* Tags can be added to any measurement. These classes will determine the
* values of the tags when a new trace starts.
*/
'trace_tag_providers' => [
\Spatie\OpenTelemetry\Support\TagProviders\DefaultTagsProvider::class,
],
/*
* Tags can be added to any measurement. These classes will determine the
* values of the tags when a new span starts.
*/
'span_tag_providers' => [
],
'queue' => [
/*
* When enabled, any measurements (spans) you make in a queued job that implements
* `TraceAware` will automatically belong to the same trace that was
* started in the process that dispatched the job.
*/
'make_queue_trace_aware' => true,
/*
* When this is set to `false`, only jobs the implement
* `TraceAware` will be trace aware.
*/
'all_jobs_are_trace_aware_by_default' => true,
/*
* When set to `true` all jobs will
* automatically start a span.
*/
'all_jobs_auto_start_a_span' => true,
/*
* These jobs will be trace aware even if they don't
* implement the `TraceAware` interface.
*/
'trace_aware_jobs' => [
],
/*
* These jobs will never trace aware, regardless of `all_jobs_are_trace_aware_by_default`.
*/
'not_trace_aware_jobs' => [
],
],
/*
* These actions can be overridden to have fine-grained control over how
* the package performs certain tasks.
*
* In most cases, you should use the default values.
*/
'actions' => [
'make_queue_trace_aware' => Spatie\OpenTelemetry\Actions\MakeQueueTraceAwareAction::class,
],
/*
* This class determines how the package measures time.
*/
'stopwatch' => Spatie\OpenTelemetry\Support\Stopwatch::class,
/*
* This class generates IDs for traces and spans.
*/
'id_generator' => Spatie\OpenTelemetry\Support\IdGenerator::class,
];

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('vendors', function (Blueprint $table) {
$table->string('routing_id')->nullable();
});
\App\Models\Company::query()
->cursor()
->each(function ($c){
$settings = $c->settings;
$settings->e_quote_type = 'OrderX_Comfort';
$settings->enable_rappen_rounding = false;
$c->settings = $settings;
$c->save();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
};

View File

@ -461,8 +461,8 @@ $lang = array(
'delete_token' => 'Delete Token',
'token' => 'Token',
'add_gateway' => 'Add Payment Gateway',
'delete_gateway' => 'Delete Gateway',
'edit_gateway' => 'Edit Gateway',
'delete_gateway' => 'Delete Payment Gateway',
'edit_gateway' => 'Edit Payment Gateway',
'updated_gateway' => 'Successfully updated gateway',
'created_gateway' => 'Successfully created gateway',
'deleted_gateway' => 'Successfully deleted gateway',
@ -5104,6 +5104,8 @@ $lang = array(
'drop_files_here' => 'Drop files here',
'upload_files' => 'Upload Files',
'download_e_invoice' => 'Download E-Invoice',
'download_e_credit' => 'Download E-Credit',
'download_e_quote' => 'Download E-Quote',
'triangular_tax_info' => 'Intra-community triangular transaction',
'intracommunity_tax_info' => 'Tax-free intra-community delivery',
'reverse_tax_info' => 'Please note that this supply is subject to reverse charge',
@ -5262,8 +5264,13 @@ $lang = array(
'purchase_order_items' => 'Purchase Order Items',
'csv_rows_length' => 'No data found in this CSV file',
'accept_payments_online' => 'Accept Payments Online',
'all_payment_gateways' => 'View all payment gateways',
'all_payment_gateways' => 'View all payment gateways',
'product_cost' => 'Product cost',
'enable_rappen_roudning' => 'Enable Rappen Rounding',
'enable_rappen_rounding_help' => 'Rounds totals to nearest 5',
'duration_words' => 'Duration in words',
'upcoming_recurring_invoices' => 'Upcoming Recurring Invoices',
'total_invoices' => 'Total Invoices',
);
return $lang;

View File

@ -2194,6 +2194,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'encryption' => 'Cryptage',
'mailgun_domain' => 'Domaine Mailgun',
'mailgun_private_key' => 'Clé privée Mailgun',
'brevo_domain' => 'Domaine Brevo',
'brevo_private_key' => 'Clé privée Brevo',
'send_test_email' => 'Envoyer un courriel test',
'select_label' => 'Sélectionnez le libellé',
'label' => 'Libellé',
@ -4844,6 +4846,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'email_alignment' => 'Justification du courriel',
'pdf_preview_location' => 'Emplacement de prévisualisation du PDF',
'mailgun' => 'Mailgun',
'brevo' => 'Brevo',
'postmark' => 'Postmark',
'microsoft' => 'Microsoft',
'click_plus_to_create_record' => 'Cliquez sur + pour créer un enregistrement',
@ -5096,6 +5099,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'drop_files_here' => 'Déposez les fichiers ici',
'upload_files' => 'Téléverser les fichiers',
'download_e_invoice' => 'Télécharger la facture électronique',
'download_e_credit' => 'Télécharger E-Credit',
'download_e_quote' => 'Télécharger E-Quote',
'triangular_tax_info' => 'Transactions intra-communautaire triangulaire',
'intracommunity_tax_info' => 'Livraison intra-communautaure sans taxe',
'reverse_tax_info' => 'Veuillez noter que cette provision est sujette à une charge renversée',
@ -5253,6 +5258,9 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'select_email_provider' => 'Définir le courriel pour l\'envoi',
'purchase_order_items' => 'Articles du bon d\'achat',
'csv_rows_length' => 'Aucune donnée dans ce fichier CSV',
'accept_payments_online' => 'Accepter les paiements en ligne',
'all_payment_gateways' => 'Voir toutes les passerelles de paiements',
'product_cost' => 'Coût du produit',
);
return $lang;

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{
"name": "@invoiceninja/invoiceninja",
"name": "invoiceninja",
"lockfileVersion": 2,
"requires": true,
"packages": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
]
},
"resources/js/app.js": {
"file": "assets/app-b98bbdda.js",
"file": "assets/app-c80ec97e.js",
"imports": [
"_index-08e160a7.js",
"__commonjsHelpers-725317a4.js"
@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
"file": "assets/app-17bd8d2c.css",
"file": "assets/app-91a05c24.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}

View File

@ -20,6 +20,40 @@
</div>
</button>
@endif
@if($entity_type == 'credit' && $settings->enable_e_invoice)
<button wire:loading.attr="disabled" wire:click="downloadECreit" class="bg-primary text-white px-4 py-4 lg:px-2 lg:py-2 rounded" type="button">
<span>{{ ctrans('texts.download_e_credit') }}</span>
<div wire:loading wire:target="downloadECredit">
<svg class="animate-spin h-5 w-5 text-blue" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
@endif
@if($entity_type == 'quote' && $settings->enable_e_invoice)
<button wire:loading.attr="disabled" wire:click="downloadEQuote" class="bg-primary text-white px-4 py-4 lg:px-2 lg:py-2 rounded" type="button">
<span>{{ ctrans('texts.download_e_quote') }}</span>
<div wire:loading wire:target="downloadEQuote">
<svg class="animate-spin h-5 w-5 text-blue" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
@endif
{{-- Not implemented yet--}}
{{-- @if($entity_type == 'purchase_order' && $settings->enable_e_invoice)
<button wire:loading.attr="disabled" wire:click="downloadEInvoice" class="bg-primary text-white px-4 py-4 lg:px-2 lg:py-2 rounded" type="button">
<span>{{ ctrans('texts.download_e_invoice') }}</span>
<div wire:loading wire:target="downloadEInvoice">
<svg class="animate-spin h-5 w-5 text-blue" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
@endif--}}
</div>
@if($html_entity_option)
<div class="hidden lg:block">

View File

@ -0,0 +1,113 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.dashboard'))
@section('header')
@if(!empty($client->getSetting('custom_message_dashboard')))
@component('portal.ninja2020.components.message')
{!! CustomMessage::client($client)
->company($client->company)
->message($client->getSetting('custom_message_dashboard')) !!}
@endcomponent
@endif
@endsection
@section('body')
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.hello') }}, {{ $contact->first_name }}
</h3>
<div class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2">
<div class="bg-white overflow-hidden shadow rounded">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm leading-5 font-medium text-gray-500 truncate">
{{ ctrans('texts.paid_to_date') }}
</dt>
<dd class="mt-1 text-3xl leading-9 font-semibold text-gray-900">
{{ App\Utils\Number::formatMoney($client->paid_to_date, $client) }}
</dd>
</dl>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm leading-5 font-medium text-gray-500 truncate">
{{ ctrans('texts.open_balance') }}
</dt>
<dd class="mt-1 text-3xl leading-9 font-semibold text-gray-900">
{{ App\Utils\Number::formatMoney($client->balance, $client) }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="grid md:grid-cols-12 gap-4 mt-6">
<div class="col-span-6">
<div class="bg-white rounded shadow px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-4 w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 capitalize">
{{ ctrans('texts.group_documents') }}
</h3>
<div class="flex flex-col h-auto overflow-y-auto">
@if($client->group_settings)
@forelse($client->group_settings->documents as $document)
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block inline-flex items-center text-sm button-link text-primary">
<span>{{ Illuminate\Support\Str::limit($document->name, 40) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="ml-2 text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
@empty
<p class="text-sm">{{ ctrans('texts.no_records_found') }}.</p>
@endforelse
@endif
</div>
</div>
</div>
</div>
</div>
<div class="col-span-6">
<div class="bg-white rounded shadow px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-4 w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 capitalize">
{{ ctrans('texts.default_documents') }}
</h3>
<div class="flex flex-col h-auto overflow-y-auto">
@forelse($client->company->documents as $document)
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block inline-flex items-center text-sm button-link text-primary">
<span>{{ Illuminate\Support\Str::limit($document->name, 40) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="ml-2 text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
@empty
<p class="text-sm">{{ ctrans('texts.no_records_found') }}.</p>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -1,113 +1,96 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.dashboard'))
@section('header')
@if(!empty($client->getSetting('custom_message_dashboard')))
@component('portal.ninja2020.components.message')
{!! CustomMessage::client($client)
->company($client->company)
->message($client->getSetting('custom_message_dashboard')) !!}
@endcomponent
@endif
@endsection
@section('body')
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.hello') }}, {{ $contact->first_name }}
</h3>
<div class="flex flex-col xl:flex-row gap-4">
<div class="w-full rounded-md border border-[#E5E7EB] bg-white p-5 text-sm text-[#6C727F]">
<h3 class="mb-4 text-xl font-semibold text-[#212529]">{{ $contact->first_name }} {{ $contact->last_name }}</h3>
<p class="mb-1.5">{{ $contact->phone }}</p>
<p class="mb-4">{{ $client->address1 }}</p>
<p class="mb-1.5">{{ $client->city }}, {{ $client->state }}</p>
<p class="mb-1.5">{{ $client->postal_code }}</p>
<p>{{ App\Models\Country::find($client->country_id)?->name }}</p>
</div>
<div class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2">
<div class="bg-white overflow-hidden shadow rounded">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm leading-5 font-medium text-gray-500 truncate">
{{ ctrans('texts.paid_to_date') }}
</dt>
<dd class="mt-1 text-3xl leading-9 font-semibold text-gray-900">
{{ App\Utils\Number::formatMoney($client->paid_to_date, $client) }}
</dd>
</dl>
</div>
<div class="w-full flex flex-row items-center rounded-md border border-[#E5E7EB] bg-white p-5 md:flex-col md:justify-center">
<div class="bg-blue-light mr-3 flex h-12 w-12 items-center justify-center rounded md:mb-6 md:mr-0">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="24" viewBox="0 0 25 24" fill="none">
<path d="M14 9.75C14 9.55109 13.921 9.36032 13.7803 9.21967C13.6397 9.07902 13.4489 9 13.25 9H7.25C7.05109 9 6.86032 9.07902 6.71967 9.21967C6.57902 9.36032 6.5 9.55109 6.5 9.75C6.5 9.94891 6.57902 10.1397 6.71967 10.2803C6.86032 10.421 7.05109 10.5 7.25 10.5H13.25C13.4489 10.5 13.6397 10.421 13.7803 10.2803C13.921 10.1397 14 9.94891 14 9.75ZM13 12.75C13 12.5511 12.921 12.3603 12.7803 12.2197C12.6397 12.079 12.4489 12 12.25 12H7.25C7.05109 12 6.86032 12.079 6.71967 12.2197C6.57902 12.3603 6.5 12.5511 6.5 12.75C6.5 12.9489 6.57902 13.1397 6.71967 13.2803C6.86032 13.421 7.05109 13.5 7.25 13.5H12.25C12.4489 13.5 12.6397 13.421 12.7803 13.2803C12.921 13.1397 13 12.9489 13 12.75ZM13.25 15C13.4489 15 13.6397 15.079 13.7803 15.2197C13.921 15.3603 14 15.5511 14 15.75C14 15.9489 13.921 16.1397 13.7803 16.2803C13.6397 16.421 13.4489 16.5 13.25 16.5H7.25C7.05109 16.5 6.86032 16.421 6.71967 16.2803C6.57902 16.1397 6.5 15.9489 6.5 15.75C6.5 15.5511 6.57902 15.3603 6.71967 15.2197C6.86032 15.079 7.05109 15 7.25 15H13.25Z" fill="{{ $settings->primary_color }}" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 21.7499H19.5C20.2293 21.7499 20.9288 21.4601 21.4445 20.9444C21.9603 20.4287 22.25 19.7292 22.25 18.9999V13.4999C22.25 13.301 22.171 13.1102 22.0303 12.9695C21.8897 12.8289 21.6989 12.7499 21.5 12.7499H18.25V4.94287C18.25 3.51987 16.641 2.69188 15.483 3.51888L15.308 3.64388C14.9248 3.9159 14.4663 4.0617 13.9964 4.06099C13.5264 4.06027 13.0684 3.91307 12.686 3.63988C12.0476 3.18554 11.2835 2.94141 10.5 2.94141C9.71645 2.94141 8.95238 3.18554 8.314 3.63988C7.93162 3.91307 7.47359 4.06027 7.00364 4.06099C6.53369 4.0617 6.07521 3.9159 5.692 3.64388L5.517 3.51888C4.359 2.69188 2.75 3.51887 2.75 4.94287V17.9999C2.75 18.9944 3.14509 19.9483 3.84835 20.6515C4.55161 21.3548 5.50544 21.7499 6.5 21.7499ZM9.186 4.85988C9.56995 4.58732 10.0291 4.4409 10.5 4.4409C10.9709 4.4409 11.4301 4.58732 11.814 4.85988C12.4507 5.31499 13.2136 5.56009 13.9962 5.56099C14.7788 5.56188 15.5423 5.31853 16.18 4.86487L16.355 4.73987C16.3923 4.71328 16.4363 4.69747 16.482 4.69418C16.5277 4.69088 16.5735 4.70022 16.6143 4.72117C16.6551 4.74213 16.6893 4.7739 16.7132 4.813C16.7372 4.8521 16.7499 4.89703 16.75 4.94287V18.9999C16.75 19.4499 16.858 19.8749 17.05 20.2499H6.5C5.90326 20.2499 5.33097 20.0128 4.90901 19.5909C4.48705 19.1689 4.25 18.5966 4.25 17.9999V4.94287C4.25012 4.89703 4.26284 4.8521 4.28678 4.813C4.31072 4.7739 4.34495 4.74213 4.38573 4.72117C4.4265 4.70022 4.47226 4.69088 4.51798 4.69418C4.56371 4.69747 4.60765 4.71328 4.645 4.73987L4.82 4.86487C5.45775 5.31853 6.22116 5.56188 7.0038 5.56099C7.78644 5.56009 8.54929 5.31499 9.186 4.85988ZM18.25 18.9999V14.2499H20.75V18.9999C20.75 19.3314 20.6183 19.6493 20.3839 19.8838C20.1495 20.1182 19.8315 20.2499 19.5 20.2499C19.1685 20.2499 18.8505 20.1182 18.6161 19.8838C18.3817 19.6493 18.25 19.3314 18.25 18.9999Z" fill="{{ $settings->primary_color }}" />
</svg>
</div>
<div class="bg-white overflow-hidden shadow rounded">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm leading-5 font-medium text-gray-500 truncate">
{{ ctrans('texts.open_balance') }}
</dt>
<dd class="mt-1 text-3xl leading-9 font-semibold text-gray-900">
{{ App\Utils\Number::formatMoney($client->balance, $client) }}
</dd>
</dl>
</div>
<div class="md:text-center">
<p class="text-light-grey-text mb-2 text-xs md:text-sm">{{ ctrans('texts.total_invoices') }}</p>
<p class="text-2xl font-semibold text-[#212529] md:text-[32px]">
{{ App\Utils\Number::formatMoney($total_invoices, $client) }}
</p>
</div>
</div>
<div class="flex flex-row items-center rounded-md border border-[#E5E7EB] bg-white p-5 md:flex-col md:justify-center w-full">
<div class="bg-blue-light mr-3 flex h-12 w-12 items-center justify-center rounded md:mb-6 md:mr-0">
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.20216 6.89268C7.96951 7.13715 7.9 7.34117 7.9 7.5C7.9 7.65883 7.96951 7.86285 8.20216 8.10732C8.43825 8.35539 8.81295 8.61064 9.32952 8.84023C10.3609 9.29862 11.8348 9.6 13.5 9.6C15.1652 9.6 16.6391 9.29862 17.6705 8.84023C18.187 8.61064 18.5618 8.35539 18.7978 8.10732C19.0305 7.86285 19.1 7.65883 19.1 7.5C19.1 7.34117 19.0305 7.13715 18.7978 6.89268C18.5618 6.64461 18.187 6.38936 17.6705 6.15977C16.6391 5.70138 15.1652 5.4 13.5 5.4C11.8348 5.4 10.3609 5.70138 9.32952 6.15977C8.81295 6.38936 8.43825 6.64461 8.20216 6.89268ZM8.76093 4.88043C10.0097 4.32542 11.6858 4 13.5 4C15.3142 4 16.9903 4.32542 18.2391 4.88043C18.8626 5.15755 19.4105 5.50564 19.812 5.92754C20.2169 6.35305 20.5 6.88563 20.5 7.5C20.5 8.11437 20.2169 8.64695 19.812 9.07246C19.4105 9.49436 18.8626 9.84246 18.2391 10.1196C16.9903 10.6746 15.3142 11 13.5 11C11.6858 11 10.0097 10.6746 8.76093 10.1196C8.13743 9.84246 7.58952 9.49436 7.18801 9.07246C6.78307 8.64695 6.5 8.11437 6.5 7.5C6.5 6.88563 6.78307 6.35305 7.18801 5.92754C7.58952 5.50564 8.13743 5.15755 8.76093 4.88043Z" fill="{{ $settings->primary_color }}" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.2 7C7.5866 7 7.9 7.32335 7.9 7.72222V16.3889C7.9 16.552 7.96903 16.762 8.20027 17.0138C8.4349 17.2692 8.80769 17.5325 9.32271 17.7696C10.351 18.243 11.8245 18.5556 13.5 18.5556C15.1755 18.5556 16.649 18.243 17.6773 17.7696C18.1923 17.5325 18.5651 17.2692 18.7997 17.0138C19.031 16.762 19.1 16.552 19.1 16.3889V7.72222C19.1 7.32335 19.4134 7 19.8 7C20.1866 7 20.5 7.32335 20.5 7.72222V16.3889C20.5 17.0202 20.219 17.5685 19.8159 18.0074C19.4161 18.4426 18.8702 18.8022 18.2477 19.0887C17.001 19.6626 15.3245 20 13.5 20C11.6755 20 9.99897 19.6626 8.75229 19.0887C8.12981 18.8022 7.58385 18.4426 7.18411 18.0074C6.78097 17.5685 6.5 17.0202 6.5 16.3889V7.72222C6.5 7.32335 6.8134 7 7.2 7Z" fill="{{ $settings->primary_color }}" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.62478 0.0127665C9.72231 -0.0922152 11.802 0.452205 13.5894 1.57442C13.9251 1.7852 14.0293 2.23294 13.8221 2.57448C13.615 2.91602 13.1749 3.02202 12.8392 2.81125C11.2931 1.84058 9.49357 1.37113 7.67921 1.4652C7.6671 1.46583 7.65498 1.46614 7.64286 1.46614C5.94066 1.46614 4.43684 1.78055 3.3853 2.25712C2.85861 2.49582 2.4767 2.76099 2.23613 3.01836C1.9989 3.27215 1.92857 3.4832 1.92857 3.64622C1.92857 3.81909 2.0091 4.05001 2.29175 4.33065C2.57662 4.6135 3.02405 4.90009 3.62983 5.15464C3.99444 5.30786 4.16793 5.73278 4.01733 6.10372C3.86673 6.47467 3.44907 6.65117 3.08446 6.49795C2.37595 6.20023 1.75195 5.82552 1.29396 5.37078C0.833752 4.91383 0.5 4.33085 0.5 3.64622C0.5 3.00988 0.788603 2.4579 1.20092 2.01679C1.60991 1.57926 2.16817 1.21766 2.80399 0.929505C4.0733 0.354242 5.7768 0.0149655 7.62478 0.0127665ZM6.92857 11.6398C7.32306 11.6398 7.64286 11.9652 7.64286 12.3665C7.64286 12.5307 7.71329 12.742 7.94925 12.9953C8.18867 13.2523 8.56907 13.5173 9.0946 13.7558C10.1439 14.2321 11.6475 14.5466 13.3571 14.5466C15.0668 14.5466 16.5704 14.2321 17.6197 13.7558C18.1452 13.5173 18.5256 13.2523 18.765 12.9953C19.001 12.742 19.0714 12.5307 19.0714 12.3665C19.0714 11.9652 19.3912 11.6398 19.7857 11.6398C20.1802 11.6398 20.5 11.9652 20.5 12.3665C20.5 13.0017 20.2133 13.5535 19.8019 13.9951C19.394 14.4329 18.8369 14.7948 18.2017 15.0831C16.9296 15.6605 15.2189 16 13.3571 16C11.4954 16 9.78466 15.6605 8.51254 15.0831C7.87735 14.7948 7.32026 14.4329 6.91235 13.9951C6.50099 13.5535 6.21429 13.0017 6.21429 12.3665C6.21429 11.9652 6.53408 11.6398 6.92857 11.6398Z" fill="{{ $settings->primary_color }}" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.29996 3C1.74176 3 2.09992 3.31603 2.09992 3.70587V12.1763C2.09992 12.3442 2.19011 12.5685 2.50666 12.8411C2.82569 13.1159 3.32679 13.3943 4.00522 13.6415C4.41357 13.7904 4.60787 14.2031 4.4392 14.5634C4.27054 14.9237 3.80278 15.0952 3.39444 14.9464C2.60096 14.6572 1.90212 14.2932 1.38919 13.8515C0.873783 13.4076 0.5 12.8413 0.5 12.1763V3.70587C0.5 3.31603 0.858153 3 1.29996 3Z" fill="{{ $settings->primary_color }}" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.29996 7C1.74176 7 2.09992 7.35815 2.09992 7.79996C2.09992 7.99025 2.19011 8.24445 2.50666 8.55339C2.82569 8.86476 3.32679 9.18024 4.00522 9.46046C4.41357 9.62913 4.60787 10.0969 4.4392 10.5052C4.27054 10.9136 3.80278 11.1079 3.39444 10.9392C2.60096 10.6115 1.90212 10.199 1.38919 9.69838C0.873783 9.19536 0.5 8.55361 0.5 7.79996C0.5 7.35815 0.858153 7 1.29996 7Z" fill="{{ $settings->primary_color }}" />
</svg>
</div>
<div class="md:text-center">
<p class="text-light-grey-text mb-2 text-xs md:text-sm">{{ ctrans('texts.paid_to_date') }}</p>
<p class="text-2xl font-semibold text-[#212529] md:text-[32px]">
{{ App\Utils\Number::formatMoney($client->paid_to_date, $client) }}
</p>
</div>
</div>
<div class="flex flex-row items-center rounded-md border border-[#E5E7EB] bg-white p-5 md:flex-col md:justify-center w-full">
<div class="bg-blue-light mr-3 flex h-12 w-12 items-center justify-center rounded md:mb-6 md:mr-0">
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="22" viewBox="0 0 23 22" fill="none">
<path d="M11.5 8.5V1V8.5ZM5.5 10L11.5 8.5L17.5 7M13.5 14L17.5 7L13.5 14ZM21.5 14L17.5 7L21.5 14ZM9.5 17L5.5 10L9.5 17ZM1.5 17L5.5 10L1.5 17Z" fill="{{ $settings->primary_color }}" />
<path d="M11.5 8.5V1M11.5 8.5L5.5 10M11.5 8.5L17.5 7M5.5 10L9.5 17M5.5 10L1.5 17M17.5 7L13.5 14M17.5 7L21.5 14" stroke="{{ $settings->primary_color }}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M5.5 21C6.56087 21 7.57828 20.5786 8.32843 19.8284C9.07857 19.0783 9.5 18.0609 9.5 17H1.5C1.5 18.0609 1.92143 19.0783 2.67157 19.8284C3.42172 20.5786 4.43913 21 5.5 21ZM17.5 18C18.5609 18 19.5783 17.5786 20.3284 16.8284C21.0786 16.0783 21.5 15.0609 21.5 14H13.5C13.5 15.0609 13.9214 16.0783 14.6716 16.8284C15.4217 17.5786 16.4391 18 17.5 18Z" stroke="{{ $settings->primary_color }}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<div class="md:text-center">
<p class="text-light-grey-text mb-2 text-xs md:text-sm">
{{ ctrans('texts.open_balance') }}
</p>
<p class="text-2xl font-semibold text-[#212529] md:text-[32px]">
{{ App\Utils\Number::formatMoney($client->balance, $client) }}
</p>
</div>
</div>
</div>
<div class="grid md:grid-cols-12 gap-4 mt-6">
<div class="col-span-6">
<div class="bg-white rounded shadow px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-4 w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 capitalize">
{{ ctrans('texts.group_documents') }}
</h3>
<div class="flex flex-col h-auto overflow-y-auto">
@if($client->group_settings)
@forelse($client->group_settings->documents as $document)
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block inline-flex items-center text-sm button-link text-primary">
<span>{{ Illuminate\Support\Str::limit($document->name, 40) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="ml-2 text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
@empty
<p class="text-sm">{{ ctrans('texts.no_records_found') }}.</p>
@endforelse
@endif
</div>
</div>
<div class="flex flex-wrap items-stretch rounded-md border border-[#E5E7EB] bg-white p-4 md:gap-y-6 xl:flex-nowrap mt-4">
<div class="flex basis-1/2 items-center xl:basis-auto xl:border-r xl:border-[#E5E7EB] xl:pr-20">
<p class="text-base font-semibold text-[#212529]">{{ ctrans('texts.invoice_from') }}</p>
</div>
<div class="flex w-full xl:w-auto mt-2 xl:mt-0 items-center xl:basis-auto xl:justify-center xl:border-r xl:border-[#E5E7EB] xl:px-20">
<div class="flex items-center">
<div class="h-6 w-6 overflow-hidden rounded">
<img src="{{ $client->company->getLogo() }}" alt="company-logo" class="h-fit w-full" />
</div>
<div class="pl-1.5">
<p class="text-xs font-semibold leading-normal text-black">
{{ $client->company->settings->name }}
</p>
</div>
</div>
</div>
<div class="col-span-6">
<div class="bg-white rounded shadow px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-4 w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 capitalize">
{{ ctrans('texts.default_documents') }}
</h3>
<div class="flex flex-col h-auto overflow-y-auto">
@forelse($client->company->documents as $document)
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block inline-flex items-center text-sm button-link text-primary">
<span>{{ Illuminate\Support\Str::limit($document->name, 40) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="ml-2 text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
@empty
<p class="text-sm">{{ ctrans('texts.no_records_found') }}.</p>
@endforelse
</div>
</div>
</div>
</div>
<div class="text-light-grey-text flex grow basis-full flex-col justify-center pt-5 text-sm md:basis-1/2 md:border-r md:border-[#E5E7EB] md:pt-0 xl:basis-auto xl:px-5">
<p class="mb-2">{{ $client->company->settings->address1 }}</p>
<p class="mb-2">{{ $client->company->settings->address2 }}</p>
<p class="mb-2">{{ $client->company->settings->postal_code }}</p>
<p>{{ App\Models\Country::find($client->company->settings->country_id)?->name }}</p>
</div>
<div class="text-light-grey-text flex grow basis-full flex-col justify-center text-sm md:basis-1/2 md:pl-4 xl:basis-auto xl:px-5">
<p class="mb-2">{{ $client->company->settings->email }}</p>
<p class="mb-2">{{ $client->company->settings->phone }}</p>
<p>{{ $client->company->settings->website }}</p>
</div>
</div>
@endsection
@stop

View File

@ -46,8 +46,8 @@
var errorDetail = Array.isArray(data.details) && data.details[0];
if (errorDetail && ['INSTRUMENT_DECLINED', 'PAYER_ACTION_REQUIRED'].includes(errorDetail.issue)) {
return actions.restart();
}
return actions.restart();
}
document.getElementById("gateway_response").value =JSON.stringify( data );
document.getElementById("server_response").submit();

View File

@ -208,6 +208,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::get('credits/{credit}/{action}', [CreditController::class, 'action'])->name('credits.action');
Route::post('credits/bulk', [CreditController::class, 'bulk'])->name('credits.bulk');
Route::get('credit/{invitation_key}/download', [CreditController::class, 'downloadPdf'])->name('credits.downloadPdf');
Route::get('credit/{invitation_key}/download_e_credit', [CreditController::class, 'downloadECredit'])->name('credits.downloadECredit');
Route::resource('designs', DesignController::class); // name = (payments. index / create / show / update / destroy / edit
Route::post('designs/bulk', [DesignController::class, 'bulk'])->name('designs.bulk');
@ -285,12 +287,14 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::put('purchase_orders/{purchase_order}/upload', [PurchaseOrderController::class, 'upload']);
Route::get('purchase_orders/{purchase_order}/{action}', [PurchaseOrderController::class, 'action'])->name('purchase_orders.action');
Route::get('purchase_order/{invitation_key}/download', [PurchaseOrderController::class, 'downloadPdf'])->name('purchase_orders.downloadPdf');
Route::get('purchase_order/{invitation_key}/download_e_purchase_order', [PurchaseOrderController::class, 'downloadEPurchaseOrder'])->name('purchase_orders.downloadEPurchaseOrder');
Route::resource('quotes', QuoteController::class); // name = (quotes. index / create / show / update / destroy / edit
Route::get('quotes/{quote}/{action}', [QuoteController::class, 'action'])->name('quotes.action');
Route::post('quotes/bulk', [QuoteController::class, 'bulk'])->name('quotes.bulk');
Route::put('quotes/{quote}/upload', [QuoteController::class, 'upload']);
Route::get('quote/{invitation_key}/download', [QuoteController::class, 'downloadPdf'])->name('quotes.downloadPdf');
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
Route::resource('recurring_expenses', RecurringExpenseController::class);
Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');

View File

@ -49,7 +49,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie
Route::get('dashboard', [App\Http\Controllers\ClientPortal\DashboardController::class, 'index'])->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
Route::get('plan', [App\Http\Controllers\ClientPortal\NinjaPlanController::class, 'plan'])->name('plan'); // name = (dashboard. index / create / show / update / destroy / edit
Route::get('showBlob/{hash}', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'showBlob'])->name('invoices.showBlob');
Route::get('invoices', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'index'])->name('invoices.index')->middleware('portal_enabled');
Route::post('invoices/payment', [App\Http\Controllers\ClientPortal\InvoiceController::class, 'bulk'])->name('invoices.bulk');
@ -131,7 +131,9 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key')->middleware('token_auth');
Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoice.download_e_invoice')->middleware('token_auth');
Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key')->middleware('token_auth');
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, "downloadEQuote"])->name('invoice.download_e_quote')->middleware('token_auth');
Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key')->middleware('token_auth');
Route::get('credit/{invitation_key}/download_e_credit', [CreditController::class, 'downloadECredit'])->name('credit.download_e_credit')->middleware('token_auth');
Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload'])->middleware('token_auth');
Route::get('pay/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'payInvoice'])->name('pay.invoice');
@ -142,7 +144,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
});
Route::get('route/{hash}', function ($hash) {
return redirect(decrypt($hash));
});

View File

@ -11,10 +11,8 @@
namespace Tests\Feature\EInvoice;
use App\Services\Invoice\EInvoice\FacturaEInvoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Storage;
use Tests\MockAccountData;
use Tests\TestCase;
@ -40,11 +38,11 @@ class FacturaeTest extends TestCase
public function testInvoiceGeneration()
{
$f = new FacturaEInvoice($this->invoice, "3.2.2");
$f = new \App\Services\EDocument\Standards\FacturaEInvoice($this->invoice, "3.2.2");
$path = $f->run();
$this->assertNotNull($f->run());
// nlog($f->run());
// $this->assertTrue($this->validateInvoiceXML($path));

View File

@ -11,7 +11,7 @@
namespace Tests\Feature\EInvoice;
use App\Services\Invoice\EInvoice\FatturaPA;
use App\Services\EDocument\Standards\FatturaPA;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
@ -42,7 +42,7 @@ class FatturaPATest extends TestCase
$xml = $fat->run();
// nlog($xml);
$this->assertnotNull($xml);
}
}

View File

@ -62,6 +62,127 @@ class PaymentTest extends TestCase
);
}
public function testClientIdValidation()
{
$p = Payment::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Payment::STATUS_COMPLETED,
'amount' => 100
]);
$data = [
'date' => now()->addDay()->format('Y-m-d')
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/payments/'.$p->hashed_id, $data);
$response->assertStatus(200);
$data = [
'date' => now()->addDay()->format('Y-m-d'),
'client_id' => $this->client->hashed_id,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/payments/'.$p->hashed_id, $data);
$response->assertStatus(200);
$c = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
]);
$data = [
'date' => now()->addDay()->format('Y-m-d'),
'client_id' => $c->hashed_id,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/payments/'.$p->hashed_id, $data);
$response->assertStatus(422);
}
public function testNegativeAppliedAmounts()
{
$p = Payment::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Payment::STATUS_COMPLETED,
'amount' => 100
]);
$i = Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'status_id' => Invoice::STATUS_SENT,
]);
$i->calc()->getInvoice()->service()->markSent()->save();
$this->assertGreaterThan(0, $i->balance);
$data = [
'amount' => 5,
'client_id' => $this->client->hashed_id,
'invoices' => [
[
'invoice_id' => $this->invoice->hashed_id,
'amount' => 5,
],
],
'date' => '2020/12/11',
'idempotency_key' => \Illuminate\Support\Str::uuid()->toString()
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/payments/', $data);
$response->assertStatus(200);
$payment_id = $response->json()['data']['id'];
$payment = Payment::find($this->decodePrimaryKey($payment_id));
$this->assertNotNull($payment);
$data = [
'client_id' => $this->client->hashed_id,
'invoices' => [
[
'invoice_id' => $this->invoice->hashed_id,
'amount' => -5,
],
],
'date' => '2020/12/11',
'idempotency_key' => \Illuminate\Support\Str::uuid()->toString()
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/payments/'.$payment_id, $data);
$response->assertStatus(422);
}
public function testCompletedPaymentLogic()
{
@ -299,10 +420,9 @@ class PaymentTest extends TestCase
public function testPaymentRESTEndPoints()
{
Payment::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]);
$Payment = Payment::all()->last();
$Payment = Payment::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]);
$Payment->name = \Illuminate\Support\Str::random(54);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
@ -470,19 +590,13 @@ class PaymentTest extends TestCase
$response = false;
// try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/payments?include=invoices', $data);
// } catch (ValidationException $e) {
// $message = json_decode($e->validator->getMessageBag(), 1);
// $this->assertNotNull($message);
// }
// if ($response) {
$response->assertStatus(200);
// }
}
public function testPartialPaymentAmount()
@ -1395,8 +1509,9 @@ class PaymentTest extends TestCase
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
$invoice = $invoice_calc->getInvoice()->service()->markSent()->save();
$this->assertEquals(10, $invoice->amount);
$this->assertEquals(10, $invoice->balance);
$credit = CreditFactory::create($this->company->id, $this->user->id);
$credit->client_id = $client->id;
@ -1410,8 +1525,10 @@ class PaymentTest extends TestCase
$credit_calc = new InvoiceSum($credit);
$credit_calc->build();
$credit = $credit_calc->getCredit();
$credit->save(); //$10 credit
$credit = $credit_calc->getCredit()->service()->markSent()->save(); //$10 credit
$this->assertEquals(10, $credit->amount);
$this->assertEquals(10, $credit->balance);
$data = [
'amount' => $invoice->amount,

View File

@ -163,18 +163,11 @@ class CreditPaymentTest extends TestCase
'date' => '2019/12/12',
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($e->validator->getMessageBag());
}
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/payments/', $data);
$response->assertStatus(200);
$arr = $response->json();

View File

@ -11,22 +11,24 @@
namespace Tests\Feature;
use App\Factory\ClientFactory;
use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Models\ClientContact;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Payment;
use Tests\MockAccountData;
use App\Models\ClientContact;
use App\Factory\ClientFactory;
use App\Factory\CreditFactory;
use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory;
use App\Utils\Traits\MakesHash;
use App\Helpers\Invoice\InvoiceSum;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
@ -59,6 +61,132 @@ class RefundTest extends TestCase
// $this->withoutExceptionHandling();
}
public function testRefundAndAppliedAmounts()
{
$data = [
'amount' => 500,
'client_id' => $this->client->hashed_id,
'date' => '2020/12/12',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/payments', $data);
$response->assertStatus(200);
$arr = $response->json();
$payment_id = $arr['data']['id'];
$item = new InvoiceItem;
$item->cost = 300;
$item->quantity = 1;
$i = Invoice::factory()
->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'line_items' => [$item],
'discount' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
]);
$i->calc()->getInvoice();
$i->service()->markSent()->save();
$this->assertEquals(300, $i->balance);
$data = [
'client_id' => $this->client->hashed_id,
'invoices' => [
[
'invoice_id' => $i->hashed_id,
'amount' => 300
],
]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/payments/'.$payment_id, $data);
$response->assertStatus(200);
$i = $i->fresh();
$this->assertEquals(0, $i->balance);
$payment = Payment::find($this->decodePrimaryKey($payment_id));
$this->assertNotNull($payment);
$this->assertEquals(500, $payment->amount);
$this->assertEquals(300, $payment->applied);
$this->assertEquals(0, $payment->refunded);
$data = [
'id' => $this->encodePrimaryKey($payment->id),
'invoices' => [
[
'invoice_id' => $i->hashed_id,
'amount' => $i->amount,
],
],
'date' => '2020/12/12',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/payments/refund', $data);
$response->assertStatus(200);
$payment = $payment->fresh();
$i = $i->fresh();
$this->assertEquals(300, $payment->refunded);
$this->assertEquals(300, $i->balance);
$this->assertEquals(2, $i->status_id);
$data = [
'client_id' => $this->client->hashed_id,
'invoices' => [
[
'invoice_id' => $i->hashed_id,
'amount' => 200
],
]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/payments/'.$payment_id, $data);
$response->assertStatus(200);
$payment = $payment->fresh();
$i = $i->fresh();
$this->assertEquals(300, $payment->refunded);
$this->assertEquals(100, $i->balance);
$this->assertEquals(3, $i->status_id);
$this->assertEquals(500, $payment->applied);
}
/**
* Test that a simple payment of $50
* is able to be refunded.
@ -552,29 +680,22 @@ class RefundTest extends TestCase
$this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id
$this->invoice->client_id = $client->id;
$this->invoice->status_id = Invoice::STATUS_SENT;
$this->invoice->line_items = $this->buildLineItems();
$this->invoice->uses_inclusive_taxes = false;
$this->invoice->client_id = $client->id;
$this->invoice->save();
$invoice_calc = new InvoiceSum($this->invoice);
$invoice_calc->build();
$this->invoice = $invoice_calc->getInvoice();
$this->invoice->save();
$this->invoice->calc()->getInvoice()->service()->markSent()->save();
$this->credit = CreditFactory::create($this->company->id, $this->user->id);
$this->credit->client_id = $client->id;
$this->credit->status_id = 2;
$this->credit->line_items = $this->buildLineItems();
$this->credit->amount = 10;
$this->credit->balance = 10;
$this->credit->uses_inclusive_taxes = false;
$this->credit->save();
$this->credit->date = now()->format('Y-m-d');
$this->credit->due_date = now()->addMonth()->format('Y-m-d');
$this->credit->calc()->getCredit()->service()->markSent()->save();
$this->assertEquals(10, $this->credit->amount);
$this->assertEquals(10, $this->credit->balance);
$data = [
'amount' => 50,
@ -656,26 +777,62 @@ class RefundTest extends TestCase
public function testRefundsWhenCreditsArePresent()
{
$cl = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
nlog($cl->id);
$i = Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'client_id' => $cl->id,
'status_id' => Invoice::STATUS_SENT,
'amount' => 1000,
'balance' => 1000,
]);
$item = new InvoiceItem;
$item->cost = 1000;
$item->quantity = 1;
$i->line_items = [$item];
$i->service()->markSent()->save();
$this->assertEquals(1000, $i->balance);
$c = Credit::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'client_id' => $cl->id,
'status_id' => Invoice::STATUS_SENT,
'amount' => 100,
'balance' => 100,
'date' => now()->format('Y-m-d'),
'due_date' => now()->addMonth()->format('Y-m-d'),
]);
$item = new InvoiceItem();
$item->cost = 100;
$item->quantity = 1;
$c->line_items = [$item];
$c->service()->markSent()->save();
$this->assertEquals(100, $c->balance);
$this->assertNotNull($c);
$this->assertEquals(2, $c->status_id);
$this->assertEquals($cl->id, $c->client_id);
$this->assertEquals($cl->id, $i->client_id);
$data = [
'client_id' => $this->client->hashed_id,
'amount' => 900,
'client_id' => $cl->hashed_id,
'invoices' => [
[
'invoice_id' => $i->hashed_id,
@ -706,7 +863,7 @@ class RefundTest extends TestCase
$refund = [
'id' => $payment_id,
'client_id' => $this->client->hashed_id,
'client_id' => $cl->hashed_id,
'amount' => 10,
'date' => now()->format('Y-m-d'),
'invoices' => [

View File

@ -9,8 +9,8 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
use App\Jobs\EDocument\CreateEDocument;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Invoice\CreateEInvoice;
use horstoeko\zugferd\ZugferdDocumentReader;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
@ -41,7 +41,7 @@ class EInvoiceTest extends TestCase
$this->company->e_invoice_type = "EN16931";
$this->invoice->client->routing_id = 'DE123456789';
$this->invoice->client->save();
$e_invoice = (new CreateEInvoice($this->invoice))->handle();
$e_invoice = (new CreateEDocument($this->invoice))->handle();
$this->assertIsString($e_invoice);
}
@ -54,7 +54,7 @@ class EInvoiceTest extends TestCase
$this->invoice->client->routing_id = 'DE123456789';
$this->invoice->client->save();
$e_invoice = (new CreateEInvoice($this->invoice))->handle();
$e_invoice = (new CreateEDocument($this->invoice))->handle();
$document = ZugferdDocumentReader::readAndGuessFromContent($e_invoice);
$document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest);
$this->assertEquals($this->invoice->number, $documentno);

View File

@ -61,6 +61,8 @@ class InvoiceTest extends TestCase
'settings' => $c_settings,
]);
$this->assertEquals(0, $c->balance);
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 10.01;
@ -88,6 +90,10 @@ class InvoiceTest extends TestCase
$this->assertEquals(10, $ii->amount);
$ii->service()->markSent()->save();
$this->assertEquals(10, $c->fresh()->balance);
}
public function testRappenRoundingUp()
@ -129,6 +135,26 @@ class InvoiceTest extends TestCase
$this->assertEquals(10.10, round($ii->amount,2));
$ii->service()->markSent()->save();
$this->assertEquals(10.10, $c->fresh()->balance);
$item = InvoiceItemFactory::create();
$item->quantity = 2;
$item->cost = 10.09;
$item->type_id = '1';
$item->tax_id = '1';
$i->line_items = [$item];
$invoice_calc = new InvoiceSum($i);
$ii = $invoice_calc->build()->getInvoice();
$ii->client->service()->calculateBalance($ii);
$this->assertEquals(20.20, round($ii->amount,2));
$this->assertEquals(20.20, round($ii->balance,2));
$this->assertEquals(20.20, round($c->fresh()->balance,2));
}
public function testPartialDueDateCast()