This commit is contained in:
David Bomba 2024-11-15 18:41:00 +11:00
parent f510d1a9b5
commit 583ed3eefb
11 changed files with 433 additions and 350 deletions

View File

@ -1 +1 @@
5.10.51 5.10.52

View File

@ -38,17 +38,17 @@ class InvoiceSum
public $total_taxes = 0; public $total_taxes = 0;
private $total; private $total = 0;
private $total_discount; private $total_discount = 0;
private $total_custom_values; private $total_custom_values;
private $total_tax_map; private $total_tax_map;
private $sub_total; private $sub_total = 0;
private $gross_sub_total; private $gross_sub_total = 0;
private $precision; private $precision;

View File

@ -36,17 +36,17 @@ class InvoiceSumInclusive
public $invoice_item; public $invoice_item;
public $total_taxes; public $total_taxes = 0;
private $total; private $total = 0;
private $total_discount; private $total_discount = 0;
private $total_custom_values; private $total_custom_values;
private $total_tax_map; private $total_tax_map;
private $sub_total; private $sub_total = 0;
private $precision; private $precision;

View File

@ -582,6 +582,21 @@ class InvoiceController extends BaseController
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()); return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
} }
if(in_array($action, ['email','send_email'])) {
$invoice = $invoices->first();
if($user->can('edit', $invoice)){
$template = $request->input('email_type', $invoice->calculateTemplate('invoice'));
BulkInvoiceJob::dispatch($invoices->pluck('id')->toArray(), $user->company()->db, $template);
}
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
}
/* /*
* Send the other actions to the switch * Send the other actions to the switch
*/ */
@ -752,19 +767,6 @@ class InvoiceController extends BaseController
} }
break; break;
case 'email':
case 'send_email':
//check query parameter for email_type and set the template else use calculateTemplate
$template = request()->has('email_type') ? request()->input('email_type') : $invoice->calculateTemplate('invoice');
BulkInvoiceJob::dispatch($invoice, $template);
if (! $bulk) {
return response()->json(['message' => 'email sent'], 200);
}
break;
default: default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400); return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
} }

View File

@ -16,6 +16,7 @@ use App\Models\Webhook;
use App\Services\Email\Email; use App\Services\Email\Email;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Libraries\MultiDB;
use App\Services\Email\EmailObject; use App\Services\Email\EmailObject;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@ -29,6 +30,10 @@ class BulkInvoiceJob implements ShouldQueue
use Queueable; use Queueable;
use SerializesModels; use SerializesModels;
public $tries = 1;
public $timeout = 3600;
private array $templates = [ private array $templates = [
'email_template_invoice', 'email_template_invoice',
'email_template_quote', 'email_template_quote',
@ -46,7 +51,7 @@ class BulkInvoiceJob implements ShouldQueue
'email_template_purchase_order', 'email_template_purchase_order',
]; ];
public function __construct(public Invoice $invoice, public string $reminder_template){} public function __construct(public array $invoice_ids, public string $db, public string $reminder_template){}
/** /**
* Execute the job. * Execute the job.
@ -55,34 +60,48 @@ class BulkInvoiceJob implements ShouldQueue
* @return void * @return void
*/ */
public function handle() public function handle()
{ //only the reminder should mark the reminder sent field {
MultiDB::setDb($this->db);
$this->invoice->service()->markSent()->save(); Invoice::with([
'invitations',
'invitations.contact.client.country',
'invitations.invoice.client.country',
'invitations.invoice.company'
])
->withTrashed()
->whereIn('id', $this->invoice_ids)
->cursor()
->each(function ($invoice){
$this->invoice->invitations->load('contact.client.country', 'invoice.client.country', 'invoice.company')->each(function ($invitation) { $invoice->service()->markSent()->save();
//@refactor 2024-11-10 - move email into EmailObject/Email::class
$template = $this->resolveTemplateString($this->reminder_template);
$mo = new EmailObject(); $invoice->invitations->each(function ($invitation) {
$mo->entity_id = $invitation->invoice_id;
$mo->template = $template; //full template name in use $template = $this->resolveTemplateString($this->reminder_template);
$mo->email_template_body = $template;
$mo->email_template_subject = str_replace("template", "subject", $template);
$mo->entity_class = get_class($invitation->invoice); $mo = new EmailObject();
$mo->invitation_id = $invitation->id; $mo->entity_id = $invitation->invoice_id;
$mo->client_id = $invitation->contact->client_id ?? null; $mo->template = $template; //full template name in use
$mo->vendor_id = $invitation->contact->vendor_id ?? null; $mo->email_template_body = $template;
$mo->email_template_subject = str_replace("template", "subject", $template);
Email::dispatch($mo, $invitation->company); $mo->entity_class = get_class($invitation->invoice);
$mo->invitation_id = $invitation->id;
$mo->client_id = $invitation->contact->client_id ?? null;
$mo->vendor_id = $invitation->contact->vendor_id ?? null;
Email::dispatch($mo, $invitation->company->withoutRelations());
});
if ($invoice->invitations->count() >= 1) {
$invoice->entityEmailEvent($invoice->invitations->first(), 'invoice', $this->reminder_template);
$invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
}
sleep(1); // this is needed to slow down the amount of data that is pushed into cache
}); });
if ($this->invoice->invitations->count() >= 1) {
$this->invoice->entityEmailEvent($this->invoice->invitations->first(), 'invoice', $this->reminder_template);
$this->invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
}
} }
private function resolveTemplateString(string $template): string private function resolveTemplateString(string $template): string

View File

@ -53,8 +53,15 @@ class StorecoveAdapter
private string $nexus; private string $nexus;
private bool $has_error = false;
public function validate(): self public function validate(): self
{ {
if ($this->has_error) {
return $this;
}
return $this; return $this;
} }
@ -97,14 +104,6 @@ class StorecoveAdapter
$storecove_object = $serializer->normalize($obj, null, [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]); $storecove_object = $serializer->normalize($obj, null, [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
// return $storecove_object;
// $storecove_object = $serializer->encode($storecove_object, 'json', $context);
// return $storecove_object;
// return $data;
// $object = $serializer->denormalize(json_encode($storecove_object['document']['invoice']), \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class, 'json', [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
// return $storecove_object;
return $serializer->deserialize(json_encode($storecove_object), \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class, 'json', $context); return $serializer->deserialize(json_encode($storecove_object), \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class, 'json', $context);
} }
@ -117,26 +116,33 @@ class StorecoveAdapter
*/ */
public function transform($invoice): self public function transform($invoice): self
{ {
$this->ninja_invoice = $invoice; try {
$serializer = $this->getSerializer(); $this->ninja_invoice = $invoice;
$serializer = $this->getSerializer();
/** Currently - due to class structures, the serialization process goes like this: /** Currently - due to class structures, the serialization process goes like this:
* *
* e-invoice => Peppol -> XML -> Peppol Decoded -> encode to Peppol -> deserialize to Storecove * e-invoice => Peppol -> XML -> Peppol Decoded -> encode to Peppol -> deserialize to Storecove
*/ */
$p = (new Peppol($invoice))->run()->toXml(); $p = (new Peppol($invoice))->run()->toXml();
$context = [ $context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d', DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true, AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
]; ];
$e = new \InvoiceNinja\EInvoice\EInvoice(); $e = new \InvoiceNinja\EInvoice\EInvoice();
$peppolInvoice = $e->decode('Peppol', $p, 'xml'); $peppolInvoice = $e->decode('Peppol', $p, 'xml');
$parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class; $parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$peppolInvoice = $e->encode($peppolInvoice, 'json'); $peppolInvoice = $e->encode($peppolInvoice, 'json');
$this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context); $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context);
$this->buildNexus(); $this->buildNexus();
}
catch(\Throwable $th){
$this->addError($th->getMessage());
$this->has_error = true;
}
return $this; return $this;
@ -149,6 +155,9 @@ class StorecoveAdapter
public function decorate(): self public function decorate(): self
{ {
if($this->has_error)
return $this;
//set all taxmap countries - resolve the taxing country //set all taxmap countries - resolve the taxing country
$lines = $this->storecove_invoice->getInvoiceLines(); $lines = $this->storecove_invoice->getInvoiceLines();
@ -298,6 +307,11 @@ class StorecoveAdapter
*/ */
public function getDocument(): mixed public function getDocument(): mixed
{ {
if($this->has_error)
return ['errors' => $this->getErrors(), 'document' => false];
$serializer = $this->getSerializer(); $serializer = $this->getSerializer();
$context = [ $context = [

View File

@ -155,6 +155,8 @@ class Peppol extends AbstractService
private string $tax_category_id; private string $tax_category_id;
private array $errors = [];
public function __construct(public Invoice $invoice) public function __construct(public Invoice $invoice)
{ {
$this->company = $invoice->company; $this->company = $invoice->company;
@ -171,69 +173,68 @@ class Peppol extends AbstractService
*/ */
public function run(): self public function run(): self
{ {
$this->getJurisdiction(); //Sets the nexus object into the Peppol document. try {
$this->getAllUsedTaxes(); //Maps all used line item taxes $this->getJurisdiction(); //Sets the nexus object into the Peppol document.
$this->getAllUsedTaxes(); //Maps all used line item taxes
/** Invoice Level Props */ /** Invoice Level Props */
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomizationID(); $id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomizationID();
$id->value = $this->customizationID; $id->value = $this->customizationID;
$this->p_invoice->CustomizationID = $id; $this->p_invoice->CustomizationID = $id;
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ProfileID(); $id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ProfileID();
$id->value = $this->profileID; $id->value = $this->profileID;
$this->p_invoice->ProfileID = $id; $this->p_invoice->ProfileID = $id;
$this->p_invoice->ID = $this->invoice->number; $this->p_invoice->ID = $this->invoice->number;
$this->p_invoice->IssueDate = new \DateTime($this->invoice->date); $this->p_invoice->IssueDate = new \DateTime($this->invoice->date);
if($this->invoice->due_date) if($this->invoice->due_date)
$this->p_invoice->DueDate = new \DateTime($this->invoice->due_date); $this->p_invoice->DueDate = new \DateTime($this->invoice->due_date);
if(strlen($this->invoice->public_notes ?? '') > 0) if(strlen($this->invoice->public_notes ?? '') > 0)
$this->p_invoice->Note = strip_tags($this->invoice->public_notes); $this->p_invoice->Note = strip_tags($this->invoice->public_notes);
$this->p_invoice->DocumentCurrencyCode = $this->invoice->client->currency()->code; $this->p_invoice->DocumentCurrencyCode = $this->invoice->client->currency()->code;
if ($this->invoice->date && $this->invoice->due_date) { if ($this->invoice->date && $this->invoice->due_date) {
$ip = new InvoicePeriod(); $ip = new InvoicePeriod();
$ip->StartDate = new \DateTime($this->invoice->date); $ip->StartDate = new \DateTime($this->invoice->date);
$ip->EndDate = new \DateTime($this->invoice->due_date); $ip->EndDate = new \DateTime($this->invoice->due_date);
$this->p_invoice->InvoicePeriod = [$ip]; $this->p_invoice->InvoicePeriod = [$ip];
}
if ($this->invoice->project_id) {
$pr = new \InvoiceNinja\EInvoice\Models\Peppol\ProjectReferenceType\ProjectReference();
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = $this->invoice->project->number;
$pr->ID = $id;
$this->p_invoice->ProjectReference = [$pr];
}
/** Auto switch between Invoice / Credit based on the amount value */
$this->p_invoice->InvoiceTypeCode = ($this->invoice->amount >= 0) ? 380 : 381;
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
$this->p_invoice->AllowanceCharge = $this->getAllowanceCharges();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->p_invoice->Delivery = $this->getDelivery();
$this->setOrderReference()->setTaxBreakdown();
//isolate this class to only peppol changes
$this->p_invoice = $this->gateway
->mutator
->senderSpecificLevelMutators()
->receiverSpecificLevelMutators()
->getPeppol();
}catch(\Throwable $th) {
nlog("Unable to create Peppol Invoice" . $th->getMessage());
$this->errors[] = $th->getMessage();
} }
if ($this->invoice->project_id) {
$pr = new \InvoiceNinja\EInvoice\Models\Peppol\ProjectReferenceType\ProjectReference();
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = $this->invoice->project->number;
$pr->ID = $id;
$this->p_invoice->ProjectReference = [$pr];
}
/** Auto switch between Invoice / Credit based on the amount value */
$this->p_invoice->InvoiceTypeCode = ($this->invoice->amount >= 0) ? 380 : 381;
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
$this->p_invoice->AllowanceCharge = $this->getAllowanceCharges();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->p_invoice->Delivery = $this->getDelivery();
$this->setOrderReference()->setTaxBreakdown();
//isolate this class to only peppol changes
$this->p_invoice = $this->gateway
->mutator
->senderSpecificLevelMutators()
->receiverSpecificLevelMutators()
->getPeppol();
//** @todo double check this logic, this will only ever write the doc once */
// if(is_null($this->invoice->backup))
// {
// $this->invoice->e_invoice = $this->toObject();
// $this->invoice->save();
// }
return $this; return $this;
@ -1451,4 +1452,9 @@ class Peppol extends AbstractService
return '0037'; return '0037';
} }
public function getErrors(): array
{
return $this->errors;
}
} }

View File

@ -26,6 +26,39 @@ use XSLTProcessor;
class EntityLevel class EntityLevel
{ {
private array $eu_country_codes = [
'AT', // Austria
'BE', // Belgium
'BG', // Bulgaria
'CY', // Cyprus
'CZ', // Czech Republic
'DE', // Germany
'DK', // Denmark
'EE', // Estonia
'ES', // Spain
'ES-CN', // Canary Islands
'ES-CE', // Ceuta
'ES-ML', // Melilla
'FI', // Finland
'FR', // France
'GR', // Greece
'HR', // Croatia
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
'LT', // Lithuania
'LU', // Luxembourg
'LV', // Latvia
'MT', // Malta
'NL', // Netherlands
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SE', // Sweden
'SI', // Slovenia
'SK', // Slovakia
];
private array $client_fields = [ private array $client_fields = [
'address1', 'address1',
'city', 'city',
@ -110,10 +143,22 @@ class EntityLevel
try{ try{
$xml = $p->run()->toXml(); $xml = $p->run()->toXml();
if(count($p->getErrors()) >= 1){
foreach($p->getErrors() as $error)
{
$this->errors['invoice'][] = ['field' => $error, 'label' => 'error'];
}
}
} }
catch(PeppolValidationException $e) { catch(PeppolValidationException $e) {
$this->errors['invoice'] = ['field' => $e->getInvalidField(), 'label' => $e->getInvalidField()]; $this->errors['invoice'] = ['field' => $e->getInvalidField(), 'label' => $e->getInvalidField()];
}; }
catch(\Throwable $th){
}
if($xml){ if($xml){
// Second pass through the XSLT validator // Second pass through the XSLT validator
@ -158,8 +203,8 @@ class EntityLevel
} }
//If not an individual, you MUST have a VAT number //If not an individual, you MUST have a VAT number if you are in the EU
if (!in_array($client->classification, ['government','individual']) && !$this->validString($client->vat_number)) { if (!in_array($client->classification, ['government', 'individual']) && in_array($client->country->iso_3166_2, $this->eu_country_codes) && !$this->validString($client->vat_number)) {
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")]; $errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
} }

View File

@ -90,7 +90,7 @@
"predis/predis": "^2", "predis/predis": "^2",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"pusher/pusher-php-server": "^7.2", "pusher/pusher-php-server": "^7.2",
"quickbooks/v3-php-sdk": "6.1.4-alpha", "quickbooks/v3-php-sdk": "^6.1",
"razorpay/razorpay": "2.*", "razorpay/razorpay": "2.*",
"sentry/sentry-laravel": "^4", "sentry/sentry-laravel": "^4",
"setasign/fpdf": "^1.8", "setasign/fpdf": "^1.8",
@ -218,12 +218,8 @@
{ {
"type": "vcs", "type": "vcs",
"url": "https://github.com/turbo124/snappdf" "url": "https://github.com/turbo124/snappdf"
},
{
"type": "vcs",
"url": "https://github.com/karneaud/QuickBooks-V3-PHP-SDK.git"
} }
], ],
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true "prefer-stable": true
} }

431
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.10.51'), 'app_version' => env('APP_VERSION', '5.10.52'),
'app_tag' => env('APP_TAG', '5.10.51'), 'app_tag' => env('APP_TAG', '5.10.52'),
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false), 'api_secret' => env('API_SECRET', false),