mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-04 22:31:01 +00:00
chore(sales): remove legacy-era sales sources
This commit is contained in:
@@ -1,269 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Application;
|
||||
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
|
||||
class SerialNumberService
|
||||
{
|
||||
public const VALID_PLACEHOLDERS = ['CUSTOMER_SERIES', 'SEQUENCE', 'DATE_FORMAT', 'SERIES', 'RANDOM_SEQUENCE', 'DELIMITER', 'CUSTOMER_SEQUENCE'];
|
||||
|
||||
private $model;
|
||||
|
||||
private $ob;
|
||||
|
||||
private $customer;
|
||||
|
||||
private $company;
|
||||
|
||||
private $settingKey;
|
||||
|
||||
private $sequenceScope = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $nextSequenceNumber;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $nextCustomerSequenceNumber;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setModel($model)
|
||||
{
|
||||
$this->model = $model;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setModelObject($id = null)
|
||||
{
|
||||
$this->ob = $this->model::find($id);
|
||||
|
||||
if ($this->ob && $this->ob->sequence_number) {
|
||||
$this->nextSequenceNumber = $this->ob->sequence_number;
|
||||
}
|
||||
|
||||
if (isset($this->ob) && isset($this->ob->customer_sequence_number) && isset($this->customer) && $this->ob->customer_id == $this->customer->id) {
|
||||
$this->nextCustomerSequenceNumber = $this->ob->customer_sequence_number;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCompany($company)
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomer($customer = null)
|
||||
{
|
||||
$this->customer = Customer::find($customer);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the company setting the number format is read from.
|
||||
*
|
||||
* Without this the key is derived from the model class name, which is not
|
||||
* enough for documents that share a table (credit notes are Invoice rows
|
||||
* but carry their own format).
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSettingKey(string $key)
|
||||
{
|
||||
$this->settingKey = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict the sequence lookups to a subset of the model's rows.
|
||||
*
|
||||
* Takes column => value constraints that are applied on top of the company
|
||||
* (and customer) filters, so documents sharing a table can each keep an
|
||||
* independent, gapless sequence.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSequenceScope(array $constraints)
|
||||
{
|
||||
$this->sequenceScope = $constraints;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNextNumber(?string $format = null)
|
||||
{
|
||||
$modelName = strtolower(class_basename($this->model));
|
||||
$settingKey = $this->settingKey ?: $modelName.'_number_format';
|
||||
$companyId = $this->company;
|
||||
|
||||
if ($format === null) {
|
||||
$format = CompanySetting::getSetting(
|
||||
$settingKey,
|
||||
$companyId
|
||||
);
|
||||
}
|
||||
$this->setNextNumbers();
|
||||
|
||||
$serialNumber = $this->generateSerialNumber(
|
||||
$format
|
||||
);
|
||||
|
||||
return $serialNumber;
|
||||
}
|
||||
|
||||
public function setNextNumbers()
|
||||
{
|
||||
$this->nextSequenceNumber ?
|
||||
$this->nextSequenceNumber : $this->setNextSequenceNumber();
|
||||
|
||||
$this->nextCustomerSequenceNumber ?
|
||||
$this->nextCustomerSequenceNumber : $this->setNextCustomerSequenceNumber();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setNextSequenceNumber()
|
||||
{
|
||||
$companyId = $this->company;
|
||||
|
||||
$query = $this->model::orderBy('sequence_number', 'desc')
|
||||
->where('company_id', $companyId)
|
||||
->where('sequence_number', '<>', null);
|
||||
|
||||
foreach ($this->sequenceScope as $column => $value) {
|
||||
$query->where($column, $value);
|
||||
}
|
||||
|
||||
$last = $query->take(1)->first();
|
||||
|
||||
$this->nextSequenceNumber = ($last) ? $last->sequence_number + 1 : 1;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function setNextCustomerSequenceNumber()
|
||||
{
|
||||
$customer_id = ($this->customer) ? $this->customer->id : 1;
|
||||
|
||||
$query = $this->model::orderBy('customer_sequence_number', 'desc')
|
||||
->where('company_id', $this->company)
|
||||
->where('customer_id', $customer_id)
|
||||
->where('customer_sequence_number', '<>', null);
|
||||
|
||||
foreach ($this->sequenceScope as $column => $value) {
|
||||
$query->where($column, $value);
|
||||
}
|
||||
|
||||
$last = $query->take(1)->first();
|
||||
|
||||
$this->nextCustomerSequenceNumber = ($last) ? $last->customer_sequence_number + 1 : 1;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function getPlaceholders(string $format)
|
||||
{
|
||||
$regex = '/{{([A-Z_]{1,})(?::)?([a-zA-Z0-9_]{1,6}|.{1})?}}/';
|
||||
|
||||
preg_match_all($regex, $format, $placeholders);
|
||||
array_shift($placeholders);
|
||||
$validPlaceholders = collect();
|
||||
|
||||
/** @var array */
|
||||
$mappedPlaceholders = array_map(
|
||||
null,
|
||||
current($placeholders),
|
||||
end($placeholders)
|
||||
);
|
||||
|
||||
foreach ($mappedPlaceholders as $placeholder) {
|
||||
$name = current($placeholder);
|
||||
$value = end($placeholder);
|
||||
|
||||
if (in_array($name, self::VALID_PLACEHOLDERS)) {
|
||||
$validPlaceholders->push([
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $validPlaceholders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function generateSerialNumber(string $format)
|
||||
{
|
||||
$serialNumber = '';
|
||||
|
||||
$placeholders = self::getPlaceholders($format);
|
||||
|
||||
foreach ($placeholders as $placeholder) {
|
||||
$name = $placeholder['name'];
|
||||
$value = $placeholder['value'];
|
||||
|
||||
switch ($name) {
|
||||
case 'SEQUENCE':
|
||||
$value = $value ? $value : 6;
|
||||
$serialNumber .= str_pad($this->nextSequenceNumber, $value, 0, STR_PAD_LEFT);
|
||||
|
||||
break;
|
||||
case 'DATE_FORMAT':
|
||||
$value = $value ? $value : 'Y';
|
||||
$serialNumber .= date($value);
|
||||
|
||||
break;
|
||||
case 'RANDOM_SEQUENCE':
|
||||
$value = $value ? $value : 6;
|
||||
$serialNumber .= substr(bin2hex(random_bytes($value)), 0, $value);
|
||||
|
||||
break;
|
||||
case 'CUSTOMER_SERIES':
|
||||
if (isset($this->customer)) {
|
||||
$serialNumber .= $this->customer->prefix ?? 'CST';
|
||||
} else {
|
||||
$serialNumber .= 'CST';
|
||||
}
|
||||
|
||||
break;
|
||||
case 'CUSTOMER_SEQUENCE':
|
||||
$serialNumber .= str_pad($this->nextCustomerSequenceNumber, $value, 0, STR_PAD_LEFT);
|
||||
|
||||
break;
|
||||
default:
|
||||
$serialNumber .= $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $serialNumber;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Console;
|
||||
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckEstimateStatus extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'check:estimates:status';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check invoices status.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$date = Carbon::now();
|
||||
$status = [Estimate::STATUS_ACCEPTED, Estimate::STATUS_REJECTED, Estimate::STATUS_EXPIRED];
|
||||
$estimates = Estimate::whereNotIn('status', $status)->whereDate('expiry_date', '<', $date)->get();
|
||||
|
||||
foreach ($estimates as $estimate) {
|
||||
$estimate->status = Estimate::STATUS_EXPIRED;
|
||||
printf("Estimate %s is EXPIRED \n", $estimate->estimate_number);
|
||||
$estimate->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Console;
|
||||
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckInvoiceStatus extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'check:invoices:status';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check invoices status.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$date = Carbon::now();
|
||||
// Only real invoices can fall overdue: a credit note is never owed, so
|
||||
// it must never be flagged no matter what date it carries.
|
||||
$invoices = Invoice::where('type', Invoice::TYPE_INVOICE)
|
||||
->whereNotIn('status', [Invoice::STATUS_COMPLETED, Invoice::STATUS_DRAFT])
|
||||
->where('overdue', false)
|
||||
->whereDate('due_date', '<', $date)
|
||||
->get();
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
$invoice->overdue = true;
|
||||
printf("Invoice %s is OVERDUE \n", $invoice->invoice_number);
|
||||
$invoice->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\Company;
|
||||
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EstimateTemplatesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', Estimate::class);
|
||||
|
||||
$estimateTemplates = PdfTemplateUtils::getFormattedTemplates('estimate');
|
||||
|
||||
return response()->json([
|
||||
'estimateTemplates' => $estimateTemplates,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\Company;
|
||||
|
||||
use App\Domains\Sales\Application\EstimateService;
|
||||
use App\Domains\Sales\Http\Requests\DeleteEstimatesRequest;
|
||||
use App\Domains\Sales\Http\Requests\EstimatesRequest;
|
||||
use App\Domains\Sales\Http\Requests\SendEstimatesRequest;
|
||||
use App\Domains\Sales\Http\Resources\EstimateResource;
|
||||
use App\Domains\Sales\Http\Resources\InvoiceResource;
|
||||
use App\Domains\Sales\Jobs\GenerateEstimatePdfJob;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Mail\Markdown;
|
||||
|
||||
class EstimatesController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EstimateService $estimateService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', Estimate::class);
|
||||
|
||||
$limit = $request->has('limit') ? $request->limit : 10;
|
||||
|
||||
$estimates = Estimate::whereCompany()
|
||||
->join('customers', 'customers.id', '=', 'estimates.customer_id')
|
||||
->applyFilters($request->all())
|
||||
->select('estimates.*', 'customers.name')
|
||||
->latest()
|
||||
->paginateData($limit);
|
||||
|
||||
return EstimateResource::collection($estimates)
|
||||
->additional(['meta' => [
|
||||
'estimate_total_count' => Estimate::whereCompany()->count(),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function store(EstimatesRequest $request)
|
||||
{
|
||||
$this->authorize('create', Estimate::class);
|
||||
|
||||
$estimate = $this->estimateService->create(
|
||||
attributes: $request->getEstimatePayload(),
|
||||
items: $request->input('items'),
|
||||
taxes: $request->has('taxes') ? $request->input('taxes') : null,
|
||||
customFields: $this->customFields($request),
|
||||
);
|
||||
|
||||
if ($request->has('estimateSend')) {
|
||||
$this->estimateService->send($estimate, $request->only(['title', 'body']));
|
||||
}
|
||||
|
||||
GenerateEstimatePdfJob::dispatch($estimate);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function show(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('view', $estimate);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function update(EstimatesRequest $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('update', $estimate);
|
||||
|
||||
$estimate = $this->estimateService->update(
|
||||
estimate: $estimate,
|
||||
attributes: $request->getEstimatePayload(),
|
||||
items: $request->input('items'),
|
||||
taxes: $request->has('taxes') ? $request->input('taxes') : null,
|
||||
customFields: $this->customFields($request),
|
||||
);
|
||||
|
||||
GenerateEstimatePdfJob::dispatch($estimate, true);
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
|
||||
public function delete(DeleteEstimatesRequest $request)
|
||||
{
|
||||
$this->authorize('delete multiple estimates');
|
||||
|
||||
$ids = Estimate::whereCompany()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
Estimate::destroy($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function send(SendEstimatesRequest $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('send estimate', $estimate);
|
||||
|
||||
$response = $this->estimateService->send($estimate, $request->all());
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
public function sendPreview(SendEstimatesRequest $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('send estimate', $estimate);
|
||||
|
||||
$markdown = new Markdown(view(), config('mail.markdown'));
|
||||
|
||||
$data = $this->estimateService->sendEstimateData($estimate, $request->all());
|
||||
$data['url'] = $estimate->estimatePdfUrl;
|
||||
|
||||
return $markdown->render('emails.send.estimate', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function clone(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('view', $estimate);
|
||||
$this->authorize('create', Estimate::class);
|
||||
|
||||
$newEstimate = $this->estimateService->clone($estimate);
|
||||
|
||||
return new EstimateResource($newEstimate);
|
||||
}
|
||||
|
||||
public function convertToInvoice(Request $request, Estimate $estimate)
|
||||
{
|
||||
// Authorize access to the source estimate (tenant isolation) in addition
|
||||
// to the ability to create an invoice.
|
||||
$this->authorize('view', $estimate);
|
||||
$this->authorize('create', Invoice::class);
|
||||
|
||||
$invoice = $this->estimateService->convertToInvoice($estimate);
|
||||
|
||||
return new InvoiceResource($invoice);
|
||||
}
|
||||
|
||||
public function changeStatus(Request $request, Estimate $estimate)
|
||||
{
|
||||
$this->authorize('send estimate', $estimate);
|
||||
|
||||
$this->estimateService->changeStatus($estimate, $request->status);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function customFields(EstimatesRequest $request): ?iterable
|
||||
{
|
||||
$customFields = $request->input('customFields');
|
||||
|
||||
return is_iterable($customFields) ? $customFields : null;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\Company;
|
||||
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceTemplatesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
*
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', Invoice::class);
|
||||
|
||||
$invoiceTemplates = PdfTemplateUtils::getFormattedTemplates('invoice');
|
||||
|
||||
return response()->json([
|
||||
'invoiceTemplates' => $invoiceTemplates,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\Company;
|
||||
|
||||
use App\Domains\Sales\Application\RecurringInvoiceService;
|
||||
use App\Domains\Sales\Http\Requests\RecurringInvoiceRequest;
|
||||
use App\Domains\Sales\Http\Resources\RecurringInvoiceResource;
|
||||
use App\Domains\Sales\Models\RecurringInvoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class RecurringInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecurringInvoiceService $recurringInvoiceService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', RecurringInvoice::class);
|
||||
|
||||
$limit = $request->has('limit') ? $request->limit : 10;
|
||||
|
||||
$recurringInvoices = RecurringInvoice::whereCompany()
|
||||
->applyFilters($request->all())
|
||||
->paginateData($limit);
|
||||
|
||||
return RecurringInvoiceResource::collection($recurringInvoices)
|
||||
->additional(['meta' => [
|
||||
'recurring_invoice_total_count' => RecurringInvoice::whereCompany()->count(),
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function store(RecurringInvoiceRequest $request)
|
||||
{
|
||||
$this->authorize('create', RecurringInvoice::class);
|
||||
|
||||
$recurringInvoice = $this->recurringInvoiceService->create(
|
||||
attributes: $request->getRecurringInvoicePayload(),
|
||||
items: $request->input('items'),
|
||||
taxes: $request->has('taxes') ? $request->input('taxes') : null,
|
||||
customFields: $this->customFields($request),
|
||||
);
|
||||
|
||||
return new RecurringInvoiceResource($recurringInvoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function show(RecurringInvoice $recurringInvoice)
|
||||
{
|
||||
$this->authorize('view', $recurringInvoice);
|
||||
|
||||
return new RecurringInvoiceResource($recurringInvoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(RecurringInvoiceRequest $request, RecurringInvoice $recurringInvoice)
|
||||
{
|
||||
$this->authorize('update', $recurringInvoice);
|
||||
|
||||
$this->recurringInvoiceService->update(
|
||||
recurringInvoice: $recurringInvoice,
|
||||
attributes: $request->getRecurringInvoicePayload(),
|
||||
items: $request->input('items'),
|
||||
taxes: $request->has('taxes') ? $request->input('taxes') : null,
|
||||
customFields: $this->customFields($request),
|
||||
);
|
||||
|
||||
return new RecurringInvoiceResource($recurringInvoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param RecurringInvoice $recurringInvoice
|
||||
* @return Response
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$this->authorize('delete multiple recurring invoices');
|
||||
|
||||
$ids = RecurringInvoice::whereCompany()
|
||||
->whereIn('id', $request->ids)
|
||||
->pluck('id');
|
||||
|
||||
$this->recurringInvoiceService->delete($ids);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function customFields(RecurringInvoiceRequest $request): ?iterable
|
||||
{
|
||||
$customFields = $request->input('customFields');
|
||||
|
||||
return is_iterable($customFields) ? $customFields : null;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\Company;
|
||||
|
||||
use App\Domains\Sales\Models\RecurringInvoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RecurringInvoiceFrequencyController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$nextInvoiceAt = RecurringInvoice::getNextInvoiceDate($request->frequency, $request->starts_at);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'next_invoice_at' => $nextInvoiceAt,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\Company;
|
||||
|
||||
use App\Domains\Receivables\Models\Payment;
|
||||
use App\Domains\Sales\Application\SerialNumberService;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SerialNumberController extends Controller
|
||||
{
|
||||
public function nextNumber(Request $request, Invoice $invoice, Estimate $estimate, Payment $payment): JsonResponse
|
||||
{
|
||||
$key = $request->key;
|
||||
$nextNumber = null;
|
||||
$serial = (new SerialNumberService)
|
||||
->setCompany($request->header('company'))
|
||||
->setCustomer($request->userId);
|
||||
|
||||
try {
|
||||
switch ($key) {
|
||||
case 'invoice':
|
||||
// Scoped exactly like every invoice create path, so the
|
||||
// settings preview can never count credit-note rows.
|
||||
$nextNumber = $serial->setModel($invoice)
|
||||
->setSequenceScope(['type' => Invoice::TYPE_INVOICE])
|
||||
->setModelObject($request->model_id)
|
||||
->getNextNumber($request->input('format'));
|
||||
|
||||
break;
|
||||
|
||||
case 'credit_note':
|
||||
$nextNumber = $serial->setModel($invoice)
|
||||
->setSettingKey('credit_note_number_format')
|
||||
->setSequenceScope(['type' => Invoice::TYPE_CREDIT_NOTE])
|
||||
->setModelObject($request->model_id)
|
||||
->getNextNumber($request->input('format'));
|
||||
|
||||
break;
|
||||
|
||||
case 'estimate':
|
||||
$nextNumber = $serial->setModel($estimate)
|
||||
->setModelObject($request->model_id)
|
||||
->getNextNumber($request->input('format'));
|
||||
|
||||
break;
|
||||
|
||||
case 'payment':
|
||||
$nextNumber = $serial->setModel($payment)
|
||||
->setModelObject($request->model_id)
|
||||
->getNextNumber($request->input('format'));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'nextNumber' => $nextNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
public function placeholders(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->input('format')) {
|
||||
$placeholders = SerialNumberService::getPlaceholders($request->input('format'));
|
||||
} else {
|
||||
$placeholders = [];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'placeholders' => $placeholders,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Models\Company;
|
||||
use App\Domains\Sales\Http\Resources\CustomerPortal\EstimateResource;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AcceptEstimateController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param Estimate $estimate
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(Request $request, Company $company, $id)
|
||||
{
|
||||
$estimate = $company->estimates()
|
||||
->whereCustomer(Auth::guard('customer')->id())
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (! $estimate) {
|
||||
return response()->json(['error' => 'estimate_not_found'], 404);
|
||||
}
|
||||
|
||||
$estimate->update($request->only('status'));
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Sales\Http\Resources\EstimateResource;
|
||||
use App\Domains\Sales\Mail\EstimateViewedMail;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Mail\Models\EmailLog;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EstimatePdfController extends Controller
|
||||
{
|
||||
public function getPdf(EmailLog $emailLog, Request $request)
|
||||
{
|
||||
$estimate = $emailLog->mailable;
|
||||
abort_unless($estimate instanceof Estimate, 404);
|
||||
abort_if($emailLog->isExpired(), 403, 'Link Expired.');
|
||||
|
||||
if ($estimate->status == Estimate::STATUS_SENT || $estimate->status == Estimate::STATUS_DRAFT) {
|
||||
$estimate->status = Estimate::STATUS_VIEWED;
|
||||
$estimate->save();
|
||||
$notifyEstimateViewed = CompanySetting::getSetting(
|
||||
'notify_estimate_viewed',
|
||||
$estimate->company_id
|
||||
);
|
||||
|
||||
if ($notifyEstimateViewed == 'YES') {
|
||||
$data['estimate'] = Estimate::findOrFail($estimate->id)->toArray();
|
||||
$data['user'] = Customer::find($estimate->customer_id)->toArray();
|
||||
$notificationEmail = CompanySetting::getSetting(
|
||||
'notification_email',
|
||||
$estimate->company_id
|
||||
);
|
||||
|
||||
\Mail::to($notificationEmail)->send(new EstimateViewedMail($data));
|
||||
}
|
||||
}
|
||||
|
||||
return $estimate->getGeneratedPDFOrStream('estimate');
|
||||
}
|
||||
|
||||
public function getEstimate(EmailLog $emailLog)
|
||||
{
|
||||
$estimate = $emailLog->mailable;
|
||||
abort_unless($estimate instanceof Estimate, 404);
|
||||
abort_if($emailLog->isExpired(), 403, 'Link Expired.');
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Models\Company;
|
||||
use App\Domains\Sales\Http\Resources\CustomerPortal\EstimateResource;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class EstimatesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$limit = $request->has('limit') ? $request->limit : 10;
|
||||
|
||||
$estimates = Estimate::with([
|
||||
'items',
|
||||
'customer',
|
||||
'taxes',
|
||||
'creator',
|
||||
])
|
||||
->where('status', '<>', 'DRAFT')
|
||||
->whereCustomer(Auth::guard('customer')->id())
|
||||
->applyFilters($request->only([
|
||||
'status',
|
||||
'estimate_number',
|
||||
'from_date',
|
||||
'to_date',
|
||||
'orderByField',
|
||||
'orderBy',
|
||||
]))
|
||||
->latest()
|
||||
->paginateData($limit);
|
||||
|
||||
return EstimateResource::collection($estimates)
|
||||
->additional(['meta' => [
|
||||
'estimateTotalCount' => Estimate::where('status', '<>', 'DRAFT')->whereCustomer(Auth::guard('customer')->id())->count(),
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param Estimate $estimate
|
||||
* @return Response
|
||||
*/
|
||||
public function show(Company $company, $id)
|
||||
{
|
||||
$estimate = $company->estimates()
|
||||
->whereCustomer(Auth::guard('customer')->id())
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (! $estimate) {
|
||||
return response()->json(['error' => 'estimate_not_found'], 404);
|
||||
}
|
||||
|
||||
return new EstimateResource($estimate);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Sales\Http\Resources\CustomerPortal\InvoiceResource as CustomerInvoiceResource;
|
||||
use App\Domains\Sales\Mail\InvoiceViewedMail;
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use App\Platform\Mail\Models\EmailLog;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoicePdfController extends Controller
|
||||
{
|
||||
public function getPdf(EmailLog $emailLog, Request $request)
|
||||
{
|
||||
// Resolve the document through the morph relation and enforce the
|
||||
// expected type — a token issued for another mailable type (or whose
|
||||
// numeric id collides) must not disclose an invoice.
|
||||
$invoice = $emailLog->mailable;
|
||||
abort_unless($invoice instanceof Invoice, 404);
|
||||
abort_if($emailLog->isExpired(), 403, 'Link Expired.');
|
||||
|
||||
if ($invoice->status == Invoice::STATUS_SENT || $invoice->status == Invoice::STATUS_DRAFT) {
|
||||
$invoice->status = Invoice::STATUS_VIEWED;
|
||||
$invoice->viewed = true;
|
||||
$invoice->save();
|
||||
$notifyInvoiceViewed = CompanySetting::getSetting(
|
||||
'notify_invoice_viewed',
|
||||
$invoice->company_id
|
||||
);
|
||||
|
||||
if ($notifyInvoiceViewed == 'YES') {
|
||||
$data['invoice'] = Invoice::findOrFail($invoice->id)->toArray();
|
||||
$data['user'] = Customer::find($invoice->customer_id)->toArray();
|
||||
$notificationEmail = CompanySetting::getSetting(
|
||||
'notification_email',
|
||||
$invoice->company_id
|
||||
);
|
||||
|
||||
\Mail::to($notificationEmail)->send(new InvoiceViewedMail($data));
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('pdf')) {
|
||||
return $invoice->getGeneratedPDFOrStream('invoice');
|
||||
}
|
||||
|
||||
return view('app')->with([
|
||||
'customer_logo' => get_company_setting('customer_portal_logo', $invoice->company_id),
|
||||
'current_theme' => get_company_setting('customer_portal_theme', $invoice->company_id),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getInvoice(EmailLog $emailLog)
|
||||
{
|
||||
$invoice = $emailLog->mailable;
|
||||
abort_unless($invoice instanceof Invoice, 404);
|
||||
abort_if($emailLog->isExpired(), 403, 'Link Expired.');
|
||||
|
||||
return new CustomerInvoiceResource($invoice);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Models\Company;
|
||||
use App\Domains\Sales\Http\Resources\CustomerPortal\InvoiceResource;
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Platform\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class InvoicesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$limit = $request->has('limit') ? $request->limit : 10;
|
||||
|
||||
$invoices = Invoice::with(['items', 'customer', 'creator', 'taxes'])
|
||||
->where('status', '<>', 'DRAFT')
|
||||
->applyFilters($request->all())
|
||||
->whereCustomer(Auth::guard('customer')->id())
|
||||
->latest()
|
||||
->paginateData($limit);
|
||||
|
||||
return InvoiceResource::collection($invoices)
|
||||
->additional(['meta' => [
|
||||
// Issued invoices only: a credit note is a reversal document,
|
||||
// not another invoice the customer received.
|
||||
'invoiceTotalCount' => Invoice::where('type', Invoice::TYPE_INVOICE)->where('status', '<>', 'DRAFT')->whereCustomer(Auth::guard('customer')->id())->count(),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function show(Company $company, $id)
|
||||
{
|
||||
$invoice = $company->invoices()
|
||||
->whereCustomer(Auth::guard('customer')->id())
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (! $invoice) {
|
||||
return response()->json(['error' => 'invoice_not_found'], 404);
|
||||
}
|
||||
|
||||
return new InvoiceResource($invoice);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class DeleteEstimatesRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => [
|
||||
'required',
|
||||
],
|
||||
'ids.*' => [
|
||||
'required',
|
||||
Rule::exists('estimates', 'id'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Rules\CreditNoteDeletedTogether;
|
||||
use App\Rules\RelationNotExist;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class DeleteInvoiceRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ids' => [
|
||||
'required',
|
||||
],
|
||||
'ids.*' => [
|
||||
'required',
|
||||
Rule::exists('invoices', 'id'),
|
||||
new RelationNotExist(Invoice::class, 'payments'),
|
||||
new CreditNoteDeletedTogether((array) $this->input('ids', [])),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Platform\Pdf\Rules\PdfTemplateExists;
|
||||
use App\Support\DocumentTotals;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class EstimatesRequest extends FormRequest
|
||||
{
|
||||
use Concerns\ValidatesDocumentTaxPlaceholders;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'estimate_date' => [
|
||||
'required',
|
||||
],
|
||||
'expiry_date' => [
|
||||
'nullable',
|
||||
],
|
||||
'customer_id' => [
|
||||
'required',
|
||||
],
|
||||
'estimate_number' => [
|
||||
'required',
|
||||
Rule::unique('estimates')->where('company_id', $this->header('company')),
|
||||
],
|
||||
'exchange_rate' => [
|
||||
'nullable',
|
||||
],
|
||||
'discount' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
'discount_val' => [
|
||||
'integer',
|
||||
'required',
|
||||
],
|
||||
'sub_total' => [
|
||||
'integer',
|
||||
'required',
|
||||
],
|
||||
'total' => [
|
||||
'integer',
|
||||
'numeric',
|
||||
'max:999999999999',
|
||||
'required',
|
||||
],
|
||||
'tax' => [
|
||||
'required',
|
||||
],
|
||||
'template_name' => [
|
||||
'required',
|
||||
new PdfTemplateExists('estimate'),
|
||||
],
|
||||
'items' => [
|
||||
'required',
|
||||
'array',
|
||||
],
|
||||
'items.*.description' => [
|
||||
'nullable',
|
||||
],
|
||||
'items.*' => [
|
||||
'required',
|
||||
'max:255',
|
||||
],
|
||||
'items.*.name' => [
|
||||
'required',
|
||||
],
|
||||
'items.*.quantity' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
'items.*.price' => [
|
||||
'integer',
|
||||
'required',
|
||||
],
|
||||
];
|
||||
|
||||
$companyCurrency = CompanySetting::getSetting('currency', $this->header('company'));
|
||||
|
||||
$customer = Customer::find($this->customer_id);
|
||||
|
||||
if ($companyCurrency && $customer) {
|
||||
if ((string) $customer->currency_id !== $companyCurrency) {
|
||||
$rules['exchange_rate'] = [
|
||||
'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isMethod('PUT')) {
|
||||
$rules['estimate_number'] = [
|
||||
'required',
|
||||
Rule::unique('estimates')
|
||||
->ignore($this->route('estimate')->id)
|
||||
->where('company_id', $this->header('company')),
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$this->validateDocumentTaxPlaceholders($validator);
|
||||
}
|
||||
|
||||
public function getEstimatePayload()
|
||||
{
|
||||
$company_currency = CompanySetting::getSetting('currency', $this->header('company'));
|
||||
$current_currency = $this->currency_id;
|
||||
$exchange_rate = $company_currency != $current_currency ? $this->exchange_rate : 1;
|
||||
$currency = Customer::find($this->customer_id)->currency_id;
|
||||
|
||||
$tax_per_item = CompanySetting::getSetting('tax_per_item', $this->header('company')) ?? 'NO ';
|
||||
$discount_per_item = CompanySetting::getSetting('discount_per_item', $this->header('company')) ?? 'NO';
|
||||
|
||||
// Recompute totals server-side from the line items (GHSA-8c69).
|
||||
$totals = DocumentTotals::compute(
|
||||
$this->items ?? [],
|
||||
$this->taxes ?? [],
|
||||
$this->discount_val,
|
||||
$tax_per_item,
|
||||
(bool) $this->tax_included,
|
||||
$discount_per_item
|
||||
);
|
||||
|
||||
return collect($this->except('items', 'taxes'))
|
||||
->merge([
|
||||
'creator_id' => $this->user()->id ?? null,
|
||||
'status' => $this->has('estimateSend') ? Estimate::STATUS_SENT : Estimate::STATUS_DRAFT,
|
||||
'company_id' => $this->header('company'),
|
||||
'tax_per_item' => $tax_per_item,
|
||||
'discount_per_item' => $discount_per_item,
|
||||
'sub_total' => $totals['sub_total'],
|
||||
'total' => $totals['total'],
|
||||
'tax' => $totals['tax'],
|
||||
'exchange_rate' => $exchange_rate,
|
||||
'base_discount_val' => $this->discount_val * $exchange_rate,
|
||||
'base_sub_total' => $totals['sub_total'] * $exchange_rate,
|
||||
'base_total' => $totals['total'] * $exchange_rate,
|
||||
'base_tax' => $totals['tax'] * $exchange_rate,
|
||||
'currency_id' => $currency,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Platform\Pdf\Rules\PdfTemplateExists;
|
||||
use App\Support\DocumentTotals;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class InvoicesRequest extends FormRequest
|
||||
{
|
||||
use Concerns\ValidatesDocumentTaxPlaceholders;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.s
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'invoice_date' => [
|
||||
'required',
|
||||
],
|
||||
'due_date' => [
|
||||
'nullable',
|
||||
],
|
||||
'customer_id' => [
|
||||
'required',
|
||||
],
|
||||
'invoice_number' => [
|
||||
'required',
|
||||
Rule::unique('invoices')->where('company_id', $this->header('company')),
|
||||
],
|
||||
'exchange_rate' => [
|
||||
'nullable',
|
||||
],
|
||||
'discount' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
'discount_val' => [
|
||||
'integer',
|
||||
'required',
|
||||
],
|
||||
'sub_total' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
'total' => [
|
||||
'numeric',
|
||||
'max:999999999999',
|
||||
'required',
|
||||
],
|
||||
'tax' => [
|
||||
'required',
|
||||
],
|
||||
'template_name' => [
|
||||
'required',
|
||||
new PdfTemplateExists('invoice'),
|
||||
],
|
||||
'items' => [
|
||||
'required',
|
||||
'array',
|
||||
],
|
||||
'items.*' => [
|
||||
'required',
|
||||
'max:255',
|
||||
],
|
||||
'items.*.description' => [
|
||||
'nullable',
|
||||
],
|
||||
'items.*.name' => [
|
||||
'required',
|
||||
],
|
||||
'items.*.quantity' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
'items.*.price' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
];
|
||||
|
||||
$companyCurrency = CompanySetting::getSetting('currency', $this->header('company'));
|
||||
|
||||
$customer = Customer::find($this->customer_id);
|
||||
|
||||
if ($customer && $companyCurrency) {
|
||||
if ((string) $customer->currency_id !== $companyCurrency) {
|
||||
$rules['exchange_rate'] = [
|
||||
'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isMethod('PUT')) {
|
||||
$rules['invoice_number'] = [
|
||||
'required',
|
||||
Rule::unique('invoices')
|
||||
->ignore($this->route('invoice')->id)
|
||||
->where('company_id', $this->header('company')),
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$this->validateDocumentTaxPlaceholders($validator);
|
||||
}
|
||||
|
||||
public function getInvoicePayload(): array
|
||||
{
|
||||
$company_currency = CompanySetting::getSetting('currency', $this->header('company'));
|
||||
$current_currency = $this->currency_id;
|
||||
$exchange_rate = $company_currency != $current_currency ? $this->exchange_rate : 1;
|
||||
$currency = Customer::find($this->customer_id)->currency_id;
|
||||
|
||||
$tax_per_item = CompanySetting::getSetting('tax_per_item', $this->header('company')) ?? 'NO ';
|
||||
$discount_per_item = CompanySetting::getSetting('discount_per_item', $this->header('company')) ?? 'NO';
|
||||
|
||||
// Recompute the document totals server-side from the line items so a
|
||||
// tampered total/sub_total/tax/due_amount in the request is ignored
|
||||
// (GHSA-8c69).
|
||||
$totals = DocumentTotals::compute(
|
||||
$this->items ?? [],
|
||||
$this->taxes ?? [],
|
||||
$this->discount_val,
|
||||
$tax_per_item,
|
||||
(bool) $this->tax_included,
|
||||
$discount_per_item
|
||||
);
|
||||
|
||||
return collect($this->except('items', 'taxes'))
|
||||
->merge([
|
||||
'creator_id' => $this->user()->id ?? null,
|
||||
// Credit notes are minted only by CreditNoteService::create();
|
||||
// this payload feeds Invoice::create/update, so a client must never
|
||||
// be able to declare a document a reversal, re-point its origin,
|
||||
// or write the reason a reversal was issued for.
|
||||
'type' => Invoice::TYPE_INVOICE,
|
||||
'related_invoice_id' => null,
|
||||
'credit_reason' => null,
|
||||
'status' => $this->has('invoiceSend') ? Invoice::STATUS_SENT : Invoice::STATUS_DRAFT,
|
||||
'paid_status' => Invoice::STATUS_UNPAID,
|
||||
'company_id' => $this->header('company'),
|
||||
'tax_per_item' => $tax_per_item,
|
||||
'discount_per_item' => $discount_per_item,
|
||||
'sub_total' => $totals['sub_total'],
|
||||
'total' => $totals['total'],
|
||||
'tax' => $totals['tax'],
|
||||
'due_amount' => $totals['total'],
|
||||
'sent' => (bool) $this->sent ?? false,
|
||||
'viewed' => (bool) $this->viewed ?? false,
|
||||
'exchange_rate' => $exchange_rate,
|
||||
'base_total' => $totals['total'] * $exchange_rate,
|
||||
'base_discount_val' => $this->discount_val * $exchange_rate,
|
||||
'base_sub_total' => $totals['sub_total'] * $exchange_rate,
|
||||
'base_tax' => $totals['tax'] * $exchange_rate,
|
||||
'base_due_amount' => $totals['total'] * $exchange_rate,
|
||||
'currency_id' => $currency,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Sales\Models\RecurringInvoice;
|
||||
use App\Support\DocumentTotals;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class RecurringInvoiceRequest extends FormRequest
|
||||
{
|
||||
use Concerns\ValidatesDocumentTaxPlaceholders;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$companyCurrency = CompanySetting::getSetting('currency', $this->header('company'));
|
||||
|
||||
$rules = [
|
||||
'starts_at' => [
|
||||
'required',
|
||||
],
|
||||
'send_automatically' => [
|
||||
'required',
|
||||
'boolean',
|
||||
],
|
||||
'customer_id' => [
|
||||
'required',
|
||||
],
|
||||
'exchange_rate' => [
|
||||
'nullable',
|
||||
],
|
||||
'discount' => [
|
||||
'numeric',
|
||||
'required',
|
||||
],
|
||||
'discount_val' => [
|
||||
'integer',
|
||||
'required',
|
||||
],
|
||||
'sub_total' => [
|
||||
'integer',
|
||||
'required',
|
||||
],
|
||||
'total' => [
|
||||
'integer',
|
||||
'max:999999999999',
|
||||
'required',
|
||||
],
|
||||
'tax' => [
|
||||
'required',
|
||||
],
|
||||
'status' => [
|
||||
'required',
|
||||
],
|
||||
'exchange_rate' => [
|
||||
'nullable',
|
||||
],
|
||||
'frequency' => [
|
||||
'required',
|
||||
],
|
||||
'limit_by' => [
|
||||
'required',
|
||||
],
|
||||
'limit_count' => [
|
||||
'required_if:limit_by,COUNT',
|
||||
],
|
||||
'limit_date' => [
|
||||
'required_if:limit_by,DATE',
|
||||
],
|
||||
'items' => [
|
||||
'required',
|
||||
],
|
||||
'items.*' => [
|
||||
'required',
|
||||
],
|
||||
'items.*.description' => [
|
||||
'nullable',
|
||||
],
|
||||
];
|
||||
|
||||
$customer = Customer::find($this->customer_id);
|
||||
|
||||
if ($customer && $companyCurrency) {
|
||||
if ((string) $customer->currency_id !== $companyCurrency) {
|
||||
$rules['exchange_rate'] = [
|
||||
'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$this->validateDocumentTaxPlaceholders($validator);
|
||||
}
|
||||
|
||||
public function getRecurringInvoicePayload()
|
||||
{
|
||||
$company_currency = CompanySetting::getSetting('currency', $this->header('company'));
|
||||
$current_currency = $this->currency_id;
|
||||
$exchange_rate = $company_currency != $current_currency ? $this->exchange_rate : 1;
|
||||
$currency = Customer::find($this->customer_id)->currency_id;
|
||||
|
||||
$nextInvoiceAt = RecurringInvoice::getNextInvoiceDate($this->frequency, $this->starts_at);
|
||||
|
||||
$tax_per_item = CompanySetting::getSetting('tax_per_item', $this->header('company')) ?? 'NO ';
|
||||
$discount_per_item = CompanySetting::getSetting('discount_per_item', $this->header('company')) ?? 'NO';
|
||||
|
||||
// Recompute totals server-side from the line items (GHSA-8c69). The
|
||||
// recurring template totals propagate to every generated invoice.
|
||||
$totals = DocumentTotals::compute(
|
||||
$this->items ?? [],
|
||||
$this->taxes ?? [],
|
||||
$this->discount_val,
|
||||
$tax_per_item,
|
||||
(bool) $this->tax_included,
|
||||
$discount_per_item
|
||||
);
|
||||
|
||||
return collect($this->except('items', 'taxes'))
|
||||
->merge([
|
||||
'creator_id' => $this->user()->id,
|
||||
'company_id' => $this->header('company'),
|
||||
'next_invoice_at' => $nextInvoiceAt,
|
||||
'tax_per_item' => $tax_per_item,
|
||||
'discount_per_item' => $discount_per_item,
|
||||
'sub_total' => $totals['sub_total'],
|
||||
'total' => $totals['total'],
|
||||
'tax' => $totals['tax'],
|
||||
'due_amount' => $totals['total'],
|
||||
'exchange_rate' => $exchange_rate,
|
||||
'base_sub_total' => $totals['sub_total'] * $exchange_rate,
|
||||
'base_total' => $totals['total'] * $exchange_rate,
|
||||
'base_tax' => $totals['tax'] * $exchange_rate,
|
||||
'currency_id' => $currency,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SendEstimatesRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'subject' => [
|
||||
'required',
|
||||
],
|
||||
'body' => [
|
||||
'required',
|
||||
],
|
||||
'from' => [
|
||||
'required',
|
||||
],
|
||||
'to' => [
|
||||
'required',
|
||||
],
|
||||
'cc' => [
|
||||
'nullable',
|
||||
],
|
||||
'bcc' => [
|
||||
'nullable',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SendInvoiceRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'body' => [
|
||||
'required',
|
||||
],
|
||||
'subject' => [
|
||||
'required',
|
||||
],
|
||||
'from' => [
|
||||
'required',
|
||||
],
|
||||
'to' => [
|
||||
'required',
|
||||
],
|
||||
'cc' => [
|
||||
'nullable',
|
||||
],
|
||||
'bcc' => [
|
||||
'nullable',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class EstimateCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class EstimateItemCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
|
||||
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EstimateItemResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'discount_type' => $this->discount_type,
|
||||
'quantity' => $this->quantity,
|
||||
'unit_name' => $this->unit_name,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'price' => $this->price,
|
||||
'tax' => $this->tax,
|
||||
'total' => $this->total,
|
||||
'item_id' => $this->item_id,
|
||||
'estimate_id' => $this->estimate_id,
|
||||
'company_id' => $this->company_id,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_price' => $this->base_price,
|
||||
'base_tax' => $this->base_tax,
|
||||
'base_total' => $this->base_total,
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Http\Resources\CustomerPortal\CompanyResource;
|
||||
use App\Domains\Contacts\Http\Resources\CustomerPortal\CustomerResource;
|
||||
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
|
||||
use App\Domains\Money\Http\Resources\CustomerPortal\CurrencyResource;
|
||||
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EstimateResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'estimate_date' => $this->estimate_date,
|
||||
'expiry_date' => $this->expiry_date,
|
||||
'estimate_number' => $this->estimate_number,
|
||||
'status' => $this->status,
|
||||
'reference_number' => $this->reference_number,
|
||||
'tax_per_item' => $this->tax_per_item,
|
||||
'discount_per_item' => $this->discount_per_item,
|
||||
'notes' => $this->notes,
|
||||
'discount' => $this->discount,
|
||||
'discount_type' => $this->discount_type,
|
||||
'discount_val' => $this->discount_val,
|
||||
'sub_total' => $this->sub_total,
|
||||
'total' => $this->total,
|
||||
'tax' => $this->tax,
|
||||
'unique_hash' => $this->unique_hash,
|
||||
'template_name' => $this->template_name,
|
||||
'customer_id' => $this->customer_id,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_sub_total' => $this->base_sub_total,
|
||||
'base_total' => $this->base_total,
|
||||
'base_tax' => $this->base_tax,
|
||||
'currency_id' => $this->currency_id,
|
||||
'formatted_expiry_date' => $this->formattedExpiryDate,
|
||||
'formatted_estimate_date' => $this->formattedEstimateDate,
|
||||
'estimate_pdf_url' => $this->estimatePdfUrl,
|
||||
'items' => $this->when($this->items()->exists(), function () {
|
||||
return EstimateItemResource::collection($this->items);
|
||||
}),
|
||||
'customer' => $this->when($this->customer()->exists(), function () {
|
||||
return new CustomerResource($this->customer);
|
||||
}),
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
'company' => $this->when($this->company()->exists(), function () {
|
||||
return new CompanyResource($this->company);
|
||||
}),
|
||||
'currency' => $this->when($this->currency()->exists(), function () {
|
||||
return new CurrencyResource($this->currency);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class InvoiceCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class InvoiceItemCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
|
||||
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InvoiceItemResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'discount_type' => $this->discount_type,
|
||||
'price' => $this->price,
|
||||
'quantity' => $this->quantity,
|
||||
'unit_name' => $this->unit_name,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'tax' => $this->tax,
|
||||
'total' => $this->total,
|
||||
'invoice_id' => $this->invoice_id,
|
||||
'item_id' => $this->item_id,
|
||||
'company_id' => $this->company_id,
|
||||
'base_price' => $this->base_price,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_tax' => $this->base_tax,
|
||||
'base_total' => $this->base_total,
|
||||
'recurring_invoice_id' => $this->recurring_invoice_id,
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
|
||||
|
||||
use App\Domains\Accounts\Http\Resources\CustomerPortal\CompanyResource;
|
||||
use App\Domains\Contacts\Http\Resources\CustomerPortal\CustomerResource;
|
||||
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
|
||||
use App\Domains\Money\Http\Resources\CustomerPortal\CurrencyResource;
|
||||
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InvoiceResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'invoice_date' => $this->invoice_date,
|
||||
'due_date' => $this->due_date,
|
||||
'invoice_number' => $this->invoice_number,
|
||||
'reference_number' => $this->reference_number,
|
||||
'status' => $this->status,
|
||||
'paid_status' => $this->paid_status,
|
||||
'tax_per_item' => $this->tax_per_item,
|
||||
'discount_per_item' => $this->discount_per_item,
|
||||
'notes' => $this->getNotes(),
|
||||
'discount_type' => $this->discount_type,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'sub_total' => $this->sub_total,
|
||||
'total' => $this->total,
|
||||
'tax' => $this->tax,
|
||||
'due_amount' => $this->due_amount,
|
||||
'sent' => $this->sent,
|
||||
'viewed' => $this->viewed,
|
||||
'unique_hash' => $this->unique_hash,
|
||||
'template_name' => $this->template_name,
|
||||
'customer_id' => $this->customer_id,
|
||||
'recurring_invoice_id' => $this->recurring_invoice_id,
|
||||
'sequence_number' => $this->sequence_number,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_sub_total' => $this->base_sub_total,
|
||||
'base_total' => $this->base_total,
|
||||
'base_tax' => $this->base_tax,
|
||||
'base_due_amount' => $this->base_due_amount,
|
||||
'currency_id' => $this->currency_id,
|
||||
'formatted_created_at' => $this->formattedCreatedAt,
|
||||
'formatted_notes' => $this->formattedNotes,
|
||||
'invoice_pdf_url' => $this->invoicePdfUrl,
|
||||
'formatted_invoice_date' => $this->formattedInvoiceDate,
|
||||
'formatted_due_date' => $this->formattedDueDate,
|
||||
'payment_module_enabled' => $this->payment_module_enabled,
|
||||
'overdue' => $this->overdue,
|
||||
'items' => $this->when($this->items()->exists(), function () {
|
||||
return InvoiceItemResource::collection($this->items);
|
||||
}),
|
||||
'customer' => $this->when($this->customer()->exists(), function () {
|
||||
return new CustomerResource($this->customer);
|
||||
}),
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
'company' => $this->when($this->company()->exists(), function () {
|
||||
return new CompanyResource($this->company);
|
||||
}),
|
||||
'currency' => $this->when($this->currency()->exists(), function () {
|
||||
return new CurrencyResource($this->currency);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class EstimateCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class EstimateItemCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
|
||||
use App\Domains\Taxation\Http\Resources\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EstimateItemResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'discount_type' => $this->discount_type,
|
||||
'quantity' => $this->quantity,
|
||||
'unit_name' => $this->unit_name,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'price' => $this->price,
|
||||
'tax' => $this->tax,
|
||||
'total' => $this->total,
|
||||
'item_id' => $this->item_id,
|
||||
'estimate_id' => $this->estimate_id,
|
||||
'company_id' => $this->company_id,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_price' => $this->base_price,
|
||||
'base_tax' => $this->base_tax,
|
||||
'base_total' => $this->base_total,
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use App\Domains\Accounts\Http\Resources\CompanyResource;
|
||||
use App\Domains\Accounts\Http\Resources\UserResource;
|
||||
use App\Domains\Contacts\Http\Resources\CustomerResource;
|
||||
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
|
||||
use App\Domains\Money\Http\Resources\CurrencyResource;
|
||||
use App\Domains\Taxation\Http\Resources\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EstimateResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'estimate_date' => $this->estimate_date,
|
||||
'expiry_date' => $this->expiry_date,
|
||||
'estimate_number' => $this->estimate_number,
|
||||
'status' => $this->status,
|
||||
'reference_number' => $this->reference_number,
|
||||
'tax_per_item' => $this->tax_per_item,
|
||||
'tax_included' => $this->tax_included,
|
||||
'discount_per_item' => $this->discount_per_item,
|
||||
'notes' => $this->getNotes(),
|
||||
'discount' => $this->discount,
|
||||
'discount_type' => $this->discount_type,
|
||||
'discount_val' => $this->discount_val,
|
||||
'sub_total' => $this->sub_total,
|
||||
'total' => $this->total,
|
||||
'tax' => $this->tax,
|
||||
'unique_hash' => $this->unique_hash,
|
||||
'creator_id' => $this->creator_id,
|
||||
'template_name' => $this->template_name,
|
||||
'customer_id' => $this->customer_id,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_sub_total' => $this->base_sub_total,
|
||||
'base_total' => $this->base_total,
|
||||
'base_tax' => $this->base_tax,
|
||||
'sequence_number' => $this->sequence_number,
|
||||
'currency_id' => $this->currency_id,
|
||||
'formatted_expiry_date' => $this->formattedExpiryDate,
|
||||
'formatted_estimate_date' => $this->formattedEstimateDate,
|
||||
'estimate_pdf_url' => $this->estimatePdfUrl,
|
||||
'sales_tax_type' => $this->sales_tax_type,
|
||||
'sales_tax_address_type' => $this->sales_tax_address_type,
|
||||
'items' => $this->when($this->items()->exists(), function () {
|
||||
return EstimateItemResource::collection($this->items);
|
||||
}),
|
||||
'customer' => $this->when($this->customer()->exists(), function () {
|
||||
return new CustomerResource($this->customer);
|
||||
}),
|
||||
'creator' => $this->when($this->creator()->exists(), function () {
|
||||
return new UserResource($this->creator);
|
||||
}),
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
'company' => $this->when($this->company()->exists(), function () {
|
||||
return new CompanyResource($this->company);
|
||||
}),
|
||||
'currency' => $this->when($this->currency()->exists(), function () {
|
||||
return new CurrencyResource($this->currency);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class InvoiceCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class InvoiceItemCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
|
||||
use App\Domains\Taxation\Http\Resources\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InvoiceItemResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'discount_type' => $this->discount_type,
|
||||
'price' => $this->price,
|
||||
'quantity' => $this->quantity,
|
||||
'unit_name' => $this->unit_name,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'tax' => $this->tax,
|
||||
'total' => $this->total,
|
||||
'invoice_id' => $this->invoice_id,
|
||||
'item_id' => $this->item_id,
|
||||
'company_id' => $this->company_id,
|
||||
'base_price' => $this->base_price,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_tax' => $this->base_tax,
|
||||
'base_total' => $this->base_total,
|
||||
'recurring_invoice_id' => $this->recurring_invoice_id,
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use App\Domains\Accounts\Http\Resources\CompanyResource;
|
||||
use App\Domains\Accounts\Http\Resources\UserResource;
|
||||
use App\Domains\Contacts\Http\Resources\CustomerResource;
|
||||
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
|
||||
use App\Domains\Money\Http\Resources\CurrencyResource;
|
||||
use App\Domains\Taxation\Http\Resources\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InvoiceResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'invoice_date' => $this->invoice_date,
|
||||
'due_date' => $this->due_date,
|
||||
'invoice_number' => $this->invoice_number,
|
||||
'reference_number' => $this->reference_number,
|
||||
'type' => $this->type,
|
||||
'related_invoice_id' => $this->related_invoice_id,
|
||||
'status' => $this->status,
|
||||
'paid_status' => $this->paid_status,
|
||||
'tax_per_item' => $this->tax_per_item,
|
||||
'tax_included' => $this->tax_included,
|
||||
'discount_per_item' => $this->discount_per_item,
|
||||
'notes' => $this->notes,
|
||||
'discount_type' => $this->discount_type,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'sub_total' => $this->sub_total,
|
||||
'total' => $this->total,
|
||||
'tax' => $this->tax,
|
||||
'due_amount' => $this->due_amount,
|
||||
'sent' => $this->sent,
|
||||
'viewed' => $this->viewed,
|
||||
'unique_hash' => $this->unique_hash,
|
||||
'template_name' => $this->template_name,
|
||||
'customer_id' => $this->customer_id,
|
||||
'recurring_invoice_id' => $this->recurring_invoice_id,
|
||||
'sequence_number' => $this->sequence_number,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'base_discount_val' => $this->base_discount_val,
|
||||
'base_sub_total' => $this->base_sub_total,
|
||||
'base_total' => $this->base_total,
|
||||
'creator_id' => $this->creator_id,
|
||||
'base_tax' => $this->base_tax,
|
||||
'base_due_amount' => $this->base_due_amount,
|
||||
'currency_id' => $this->currency_id,
|
||||
'formatted_created_at' => $this->formattedCreatedAt,
|
||||
'invoice_pdf_url' => $this->invoicePdfUrl,
|
||||
'formatted_invoice_date' => $this->formattedInvoiceDate,
|
||||
'formatted_due_date' => $this->formattedDueDate,
|
||||
'allow_edit' => $this->allow_edit,
|
||||
'payment_module_enabled' => $this->payment_module_enabled,
|
||||
'sales_tax_type' => $this->sales_tax_type,
|
||||
'sales_tax_address_type' => $this->sales_tax_address_type,
|
||||
'overdue' => $this->overdue,
|
||||
// Credit notes reversing this invoice (minimal reference so the
|
||||
// UI can flag the invoice as cancelled and link to the storno
|
||||
// document, mirroring the related_invoice back-link). Emitted only
|
||||
// where the relation was eager-loaded: probing it per row costs two
|
||||
// queries each, and this resource is serialized in paginated lists.
|
||||
'credit_notes' => $this->when(
|
||||
$this->relationLoaded('creditNotes') && $this->creditNotes->isNotEmpty(),
|
||||
fn () => $this->creditNotes->map(fn ($creditNote) => [
|
||||
'id' => $creditNote->id,
|
||||
'invoice_number' => $creditNote->invoice_number,
|
||||
])->values()
|
||||
),
|
||||
// Why this invoice was credited, if it was. Set only by the
|
||||
// credit-note flow, never by the invoice form.
|
||||
'credit_reason' => $this->credit_reason,
|
||||
// How much of the invoice has been credited off it, as a positive
|
||||
// number of cents (credit notes store negative totals), and whether
|
||||
// that covers the whole document. Both are read off the same loaded
|
||||
// relation the banner uses, so they cost no extra query.
|
||||
'credited_total' => $this->when(
|
||||
$this->relationLoaded('creditNotes'),
|
||||
fn () => $this->creditedTotal()
|
||||
),
|
||||
'credited_status' => $this->when(
|
||||
$this->relationLoaded('creditNotes'),
|
||||
function () {
|
||||
$credited = $this->creditedTotal();
|
||||
|
||||
if ($credited === 0) {
|
||||
return 'NONE';
|
||||
}
|
||||
|
||||
return $credited === (int) $this->total ? 'FULL' : 'PARTIAL';
|
||||
}
|
||||
),
|
||||
// Credited quantity per ORIGINAL line, which is what a partial
|
||||
// credit form needs to offer the remaining quantities. Emitted only
|
||||
// when the credit notes' items came along.
|
||||
'credited_quantities' => $this->when(
|
||||
$this->relationLoaded('creditNotes')
|
||||
&& $this->creditNotes->every(fn ($creditNote) => $creditNote->relationLoaded('items')),
|
||||
function () {
|
||||
$quantities = [];
|
||||
|
||||
foreach ($this->creditNotes as $creditNote) {
|
||||
foreach ($creditNote->items as $item) {
|
||||
if (! $item->source_invoice_item_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quantities[$item->source_invoice_item_id] =
|
||||
($quantities[$item->source_invoice_item_id] ?? 0) + (float) $item->quantity;
|
||||
}
|
||||
}
|
||||
|
||||
// Cast to an object because the item ids are the keys: a
|
||||
// nested array whose keys are all numeric is re-indexed to a
|
||||
// list by the resource filter, which would throw the ids away.
|
||||
return (object) $quantities;
|
||||
}
|
||||
),
|
||||
// Allocation rows explain how this invoice was settled without
|
||||
// reintroducing the removed singular payment.invoice relation.
|
||||
// They are loaded for the detail response only, so index listings
|
||||
// remain free of per-row payment queries.
|
||||
'payment_allocations' => $this->when(
|
||||
$this->relationLoaded('allocations'),
|
||||
fn () => $this->allocations->map(fn ($allocation) => [
|
||||
'id' => $allocation->id,
|
||||
'payment_id' => $allocation->payment_id,
|
||||
'amount' => $allocation->amount,
|
||||
'base_amount' => $allocation->base_amount,
|
||||
'payment' => $allocation->relationLoaded('payment') && $allocation->payment ? [
|
||||
'id' => $allocation->payment->id,
|
||||
'payment_number' => $allocation->payment->payment_number,
|
||||
'formatted_payment_date' => $allocation->payment->formattedPaymentDate,
|
||||
] : null,
|
||||
])->values()
|
||||
),
|
||||
'items' => $this->when($this->items()->exists(), function () {
|
||||
return InvoiceItemResource::collection($this->items);
|
||||
}),
|
||||
'customer' => $this->when($this->customer()->exists(), function () {
|
||||
return new CustomerResource($this->customer);
|
||||
}),
|
||||
'creator' => $this->when($this->creator()->exists(), function () {
|
||||
return new UserResource($this->creator);
|
||||
}),
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
'company' => $this->when($this->company()->exists(), function () {
|
||||
return new CompanyResource($this->company);
|
||||
}),
|
||||
'currency' => $this->when($this->currency()->exists(), function () {
|
||||
return new CurrencyResource($this->currency);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of the loaded credit notes as a positive number of cents.
|
||||
*/
|
||||
protected function creditedTotal(): int
|
||||
{
|
||||
return -(int) $this->creditNotes->sum('total');
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class RecurringInvoiceCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Http\Resources;
|
||||
|
||||
use App\Domains\Accounts\Http\Resources\CompanyResource;
|
||||
use App\Domains\Accounts\Http\Resources\UserResource;
|
||||
use App\Domains\Contacts\Http\Resources\CustomerResource;
|
||||
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
|
||||
use App\Domains\Money\Http\Resources\CurrencyResource;
|
||||
use App\Domains\Taxation\Http\Resources\TaxResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class RecurringInvoiceResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'starts_at' => $this->starts_at,
|
||||
'formatted_starts_at' => $this->formattedStartsAt,
|
||||
'formatted_created_at' => $this->formattedCreatedAt,
|
||||
'formatted_next_invoice_at' => $this->formattedNextInvoiceAt,
|
||||
'formatted_limit_date' => $this->formattedLimitDate,
|
||||
'send_automatically' => $this->send_automatically,
|
||||
'customer_id' => $this->customer_id,
|
||||
'company_id' => $this->company_id,
|
||||
'creator_id' => $this->creator_id,
|
||||
'status' => $this->status,
|
||||
'next_invoice_at' => $this->next_invoice_at,
|
||||
'frequency' => $this->frequency,
|
||||
'limit_by' => $this->limit_by,
|
||||
'limit_count' => $this->limit_count,
|
||||
'limit_date' => $this->limit_date,
|
||||
'exchange_rate' => $this->exchange_rate,
|
||||
'tax_per_item' => $this->tax_per_item,
|
||||
'tax_included' => $this->tax_included,
|
||||
'discount_per_item' => $this->discount_per_item,
|
||||
'notes' => $this->notes,
|
||||
'discount_type' => $this->discount_type,
|
||||
'discount' => $this->discount,
|
||||
'discount_val' => $this->discount_val,
|
||||
'sub_total' => $this->sub_total,
|
||||
'total' => $this->total,
|
||||
'tax' => $this->tax,
|
||||
'due_amount' => $this->due_amount,
|
||||
'template_name' => $this->template_name,
|
||||
'sales_tax_type' => $this->sales_tax_type,
|
||||
'sales_tax_address_type' => $this->sales_tax_address_type,
|
||||
'fields' => $this->when($this->fields()->exists(), function () {
|
||||
return CustomFieldValueResource::collection($this->fields);
|
||||
}),
|
||||
'items' => $this->when($this->items()->exists(), function () {
|
||||
return InvoiceItemResource::collection($this->items);
|
||||
}),
|
||||
'customer' => $this->when($this->customer()->exists(), function () {
|
||||
return new CustomerResource($this->customer);
|
||||
}),
|
||||
'company' => $this->when($this->company()->exists(), function () {
|
||||
return new CompanyResource($this->company);
|
||||
}),
|
||||
'invoices' => $this->when($this->invoices()->exists(), function () {
|
||||
return InvoiceResource::collection($this->invoices);
|
||||
}),
|
||||
'taxes' => $this->when($this->taxes()->exists(), function () {
|
||||
return TaxResource::collection($this->taxes);
|
||||
}),
|
||||
'creator' => $this->when($this->creator()->exists(), function () {
|
||||
return new UserResource($this->creator);
|
||||
}),
|
||||
'currency' => $this->when($this->currency()->exists(), function () {
|
||||
return new CurrencyResource($this->currency);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GenerateEstimatePdfJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $estimate;
|
||||
|
||||
public $deleteExistingFile;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($estimate, $deleteExistingFile = false)
|
||||
{
|
||||
$this->estimate = $estimate;
|
||||
$this->deleteExistingFile = $deleteExistingFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->estimate->generatePDF('estimate', $this->estimate->estimate_number, $this->deleteExistingFile);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GenerateInvoicePdfJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $invoice;
|
||||
|
||||
public $deleteExistingFile;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($invoice, $deleteExistingFile = false)
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
$this->deleteExistingFile = $deleteExistingFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->invoice->generatePDF('invoice', $this->invoice->invoice_number, $this->deleteExistingFile);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class EstimateViewedMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->subject(__('notification_view_estimate'))
|
||||
->markdown('emails.viewed.estimate', ['data', $this->data]);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InvoiceViewedMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->subject(__('notification_view_invoice'))
|
||||
->markdown('emails.viewed.invoice', ['data', $this->data]);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Mail;
|
||||
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use App\Facades\Hashids;
|
||||
use App\Platform\Mail\Models\EmailLog;
|
||||
use App\Platform\Persistence\ModelIdentityMap;
|
||||
use App\Support\Hashids\HashidConnection;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SendEstimateMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$log = EmailLog::create([
|
||||
'from' => $this->data['from'],
|
||||
'to' => $this->data['to'],
|
||||
'cc' => $this->data['cc'] ?? null,
|
||||
'bcc' => $this->data['bcc'] ?? null,
|
||||
'subject' => $this->data['subject'],
|
||||
'body' => $this->data['body'],
|
||||
'mailable_type' => ModelIdentityMap::aliasFor(Estimate::class),
|
||||
'mailable_id' => $this->data['estimate']['id'],
|
||||
]);
|
||||
|
||||
$log->token = Hashids::connection(HashidConnection::EmailLog->value)->encode($log->id);
|
||||
$log->save();
|
||||
|
||||
$this->data['url'] = route('estimate', ['email_log' => $log->token]);
|
||||
|
||||
$mailContent = $this->from($this->data['from'], config('mail.from.name'))
|
||||
->subject($this->data['subject'])
|
||||
->markdown('emails.send.estimate', ['data', $this->data]);
|
||||
|
||||
if ($this->data['attach']['data']) {
|
||||
$mailContent->attachData(
|
||||
$this->data['attach']['data']->output(),
|
||||
$this->data['estimate']['estimate_number'].'.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
return $mailContent;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Mail;
|
||||
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use App\Facades\Hashids;
|
||||
use App\Platform\Mail\Models\EmailLog;
|
||||
use App\Platform\Persistence\ModelIdentityMap;
|
||||
use App\Support\Hashids\HashidConnection;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SendInvoiceMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$log = EmailLog::create([
|
||||
'from' => $this->data['from'],
|
||||
'to' => $this->data['to'],
|
||||
'cc' => $this->data['cc'] ?? null,
|
||||
'bcc' => $this->data['bcc'] ?? null,
|
||||
'subject' => $this->data['subject'],
|
||||
'body' => $this->data['body'],
|
||||
'mailable_type' => ModelIdentityMap::aliasFor(Invoice::class),
|
||||
'mailable_id' => $this->data['invoice']['id'],
|
||||
]);
|
||||
|
||||
$log->token = Hashids::connection(HashidConnection::EmailLog->value)->encode($log->id);
|
||||
$log->save();
|
||||
|
||||
$this->data['url'] = route('invoice', ['email_log' => $log->token]);
|
||||
|
||||
$mailContent = $this->from($this->data['from'], config('mail.from.name'))
|
||||
->subject($this->data['subject'])
|
||||
->markdown('emails.send.invoice', ['data', $this->data]);
|
||||
|
||||
if ($this->data['attach']['data']) {
|
||||
$mailContent->attachData(
|
||||
$this->data['attach']['data']->output(),
|
||||
$this->data['invoice']['invoice_number'].'.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
return $mailContent;
|
||||
}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Models;
|
||||
|
||||
use App\Domains\Accounts\Models\Company;
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Metadata\Concerns\HasCustomFields;
|
||||
use App\Domains\Money\Models\Currency;
|
||||
use App\Domains\Sales\Contracts\EstimatePdfDataProvider;
|
||||
use App\Domains\Taxation\Models\Tax;
|
||||
use App\Platform\Mail\Models\EmailLog;
|
||||
use App\Platform\Pdf\Concerns\GeneratesPdf;
|
||||
use App\Platform\Pdf\Rendering\PdfHtmlSanitizer;
|
||||
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
|
||||
use App\Support\SafeOrderBy;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
class Estimate extends Model implements HasMedia
|
||||
{
|
||||
protected $table = 'estimates';
|
||||
|
||||
use GeneratesPdf;
|
||||
use HasCustomFields;
|
||||
use HasFactory;
|
||||
use InteractsWithMedia;
|
||||
|
||||
public const STATUS_DRAFT = 'DRAFT';
|
||||
|
||||
public const STATUS_SENT = 'SENT';
|
||||
|
||||
public const STATUS_VIEWED = 'VIEWED';
|
||||
|
||||
public const STATUS_EXPIRED = 'EXPIRED';
|
||||
|
||||
public const STATUS_ACCEPTED = 'ACCEPTED';
|
||||
|
||||
public const STATUS_REJECTED = 'REJECTED';
|
||||
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
'estimate_date',
|
||||
'expiry_date',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'formattedExpiryDate',
|
||||
'formattedEstimateDate',
|
||||
'estimatePdfUrl',
|
||||
];
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'total' => 'integer',
|
||||
'tax' => 'integer',
|
||||
'sub_total' => 'integer',
|
||||
'discount' => 'float',
|
||||
'discount_val' => 'integer',
|
||||
'exchange_rate' => 'float',
|
||||
];
|
||||
}
|
||||
|
||||
public function getEstimatePdfUrlAttribute()
|
||||
{
|
||||
return url('/estimates/pdf/'.$this->unique_hash);
|
||||
}
|
||||
|
||||
public function emailLogs(): MorphMany
|
||||
{
|
||||
return $this->morphMany(EmailLog::class, 'mailable');
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(EstimateItem::class);
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function company(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
public function currency(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Currency::class);
|
||||
}
|
||||
|
||||
public function taxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Tax::class);
|
||||
}
|
||||
|
||||
public function getFormattedExpiryDateAttribute($value)
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->expiry_date)->translatedFormat($dateFormat);
|
||||
}
|
||||
|
||||
public function getFormattedEstimateDateAttribute($value)
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->estimate_date)->translatedFormat($dateFormat);
|
||||
}
|
||||
|
||||
public function scopeEstimatesBetween($query, $start, $end)
|
||||
{
|
||||
return $query->whereBetween(
|
||||
'estimates.estimate_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeWhereStatus($query, $status)
|
||||
{
|
||||
return $query->where('estimates.status', $status);
|
||||
}
|
||||
|
||||
public function scopeWhereEstimateNumber($query, $estimateNumber)
|
||||
{
|
||||
return $query->where('estimates.estimate_number', 'LIKE', '%'.$estimateNumber.'%');
|
||||
}
|
||||
|
||||
public function scopeWhereEstimate($query, $estimate_id)
|
||||
{
|
||||
$query->orWhere('id', $estimate_id);
|
||||
}
|
||||
|
||||
public function scopeWhereSearch($query, $search)
|
||||
{
|
||||
foreach (explode(' ', $search) as $term) {
|
||||
$query->whereHas('customer', function ($query) use ($term) {
|
||||
$query->where('name', 'LIKE', '%'.$term.'%')
|
||||
->orWhere('contact_name', 'LIKE', '%'.$term.'%')
|
||||
->orWhere('company_name', 'LIKE', '%'.$term.'%');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeApplyFilters($query, array $filters)
|
||||
{
|
||||
$filters = collect($filters);
|
||||
|
||||
if ($filters->get('search')) {
|
||||
$query->whereSearch($filters->get('search'));
|
||||
}
|
||||
|
||||
if ($filters->get('estimate_number')) {
|
||||
$query->whereEstimateNumber($filters->get('estimate_number'));
|
||||
}
|
||||
|
||||
if ($filters->get('status')) {
|
||||
$query->whereStatus($filters->get('status'));
|
||||
}
|
||||
|
||||
if ($filters->get('estimate_id')) {
|
||||
$query->whereEstimate($filters->get('estimate_id'));
|
||||
}
|
||||
|
||||
if ($filters->get('from_date') && $filters->get('to_date')) {
|
||||
$start = Carbon::createFromFormat('Y-m-d', $filters->get('from_date'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $filters->get('to_date'));
|
||||
$query->estimatesBetween($start, $end);
|
||||
}
|
||||
|
||||
if ($filters->get('customer_id')) {
|
||||
$query->whereCustomer($filters->get('customer_id'));
|
||||
}
|
||||
|
||||
if ($filters->get('orderByField') || $filters->get('orderBy')) {
|
||||
$field = $filters->get('orderByField') ? $filters->get('orderByField') : 'sequence_number';
|
||||
$orderBy = $filters->get('orderBy') ? $filters->get('orderBy') : 'desc';
|
||||
$query->whereOrder($field, $orderBy);
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeWhereOrder($query, $orderByField, $orderBy)
|
||||
{
|
||||
SafeOrderBy::apply($query, $orderByField, $orderBy);
|
||||
}
|
||||
|
||||
public function scopeWhereCompany($query)
|
||||
{
|
||||
$query->where('estimates.company_id', request()->header('company'));
|
||||
}
|
||||
|
||||
public function scopeWhereCustomer($query, $customer_id)
|
||||
{
|
||||
$query->where('estimates.customer_id', $customer_id);
|
||||
}
|
||||
|
||||
public function scopePaginateData($query, $limit)
|
||||
{
|
||||
if ($limit == 'all') {
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
return $query->paginate($limit);
|
||||
}
|
||||
|
||||
public function getPDFData(): mixed
|
||||
{
|
||||
return app(EstimatePdfDataProvider::class)->getPdfData($this);
|
||||
}
|
||||
|
||||
public function getCompanyAddress(): string|false
|
||||
{
|
||||
if ($this->company && (! $this->company->address()->exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = CompanySetting::getSetting('estimate_company_address_format', $this->company_id);
|
||||
|
||||
return $this->getFormattedString($format);
|
||||
}
|
||||
|
||||
public function getCustomerShippingAddress(): string|false
|
||||
{
|
||||
if ($this->customer && (! $this->customer->shippingAddress()->exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = CompanySetting::getSetting('estimate_shipping_address_format', $this->company_id);
|
||||
|
||||
return $this->getFormattedString($format);
|
||||
}
|
||||
|
||||
public function getCustomerBillingAddress(): string|false
|
||||
{
|
||||
if ($this->customer && (! $this->customer->billingAddress()->exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = CompanySetting::getSetting('estimate_billing_address_format', $this->company_id);
|
||||
|
||||
return $this->getFormattedString($format);
|
||||
}
|
||||
|
||||
public function getNotes(): string
|
||||
{
|
||||
return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes));
|
||||
}
|
||||
|
||||
public function getEmailAttachmentSetting(): bool
|
||||
{
|
||||
$estimateAsAttachment = CompanySetting::getSetting('estimate_email_attachment', $this->company_id);
|
||||
|
||||
if ($estimateAsAttachment == 'NO') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEmailBody(string $body): string
|
||||
{
|
||||
$values = array_merge($this->getFieldsArray(), $this->getExtraFields());
|
||||
|
||||
$body = strtr($body, $values);
|
||||
|
||||
return preg_replace('/{(.*?)}/', '', $body);
|
||||
}
|
||||
|
||||
public function getExtraFields(): array
|
||||
{
|
||||
return [
|
||||
'{ESTIMATE_DATE}' => $this->formattedEstimateDate,
|
||||
'{ESTIMATE_EXPIRY_DATE}' => $this->formattedExpiryDate,
|
||||
'{ESTIMATE_NUMBER}' => $this->estimate_number,
|
||||
'{ESTIMATE_REF_NUMBER}' => $this->reference_number,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the estimate's template name to the corresponding invoice template name.
|
||||
*
|
||||
* Falls back to 'invoice1' if the mapped name does not exist in available templates.
|
||||
*/
|
||||
public function getInvoiceTemplateName(): string
|
||||
{
|
||||
$templateName = Str::replace('estimate', 'invoice', $this->template_name);
|
||||
|
||||
// Empty image format: only the names are wanted here, and the default
|
||||
// builds a base64 preview for every template to answer that.
|
||||
$name = array_column(PdfTemplateUtils::getFormattedTemplates('invoice', ''), 'name');
|
||||
|
||||
if (in_array($templateName, $name) == false) {
|
||||
$templateName = 'invoice1';
|
||||
}
|
||||
|
||||
return $templateName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the post-conversion action for this estimate based on company settings.
|
||||
*
|
||||
* Either deletes the estimate or marks it as accepted, depending on the
|
||||
* 'estimate_convert_action' company setting.
|
||||
*/
|
||||
public function checkForEstimateConvertAction(): bool
|
||||
{
|
||||
$convertEstimateAction = CompanySetting::getSetting(
|
||||
'estimate_convert_action',
|
||||
$this->company_id
|
||||
);
|
||||
|
||||
if ($convertEstimateAction === 'delete_estimate') {
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
if ($convertEstimateAction === 'mark_estimate_as_accepted') {
|
||||
$this->status = self::STATUS_ACCEPTED;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Models;
|
||||
|
||||
use App\Domains\Catalog\Models\Item;
|
||||
use App\Domains\Metadata\Concerns\HasCustomFields;
|
||||
use App\Domains\Taxation\Models\Tax;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class EstimateItem extends Model
|
||||
{
|
||||
protected $table = 'estimate_items';
|
||||
|
||||
use HasCustomFields;
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'price' => 'integer',
|
||||
'total' => 'integer',
|
||||
'discount' => 'float',
|
||||
'quantity' => 'float',
|
||||
'discount_val' => 'integer',
|
||||
'tax' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function estimate(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Estimate::class);
|
||||
}
|
||||
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class);
|
||||
}
|
||||
|
||||
public function taxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Tax::class);
|
||||
}
|
||||
|
||||
public function scopeWhereCompany(Builder $query, int $company_id): void
|
||||
{
|
||||
$query->where('company_id', $company_id);
|
||||
}
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Models;
|
||||
|
||||
use App\Domains\Accounts\Models\Company;
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Metadata\Concerns\HasCustomFields;
|
||||
use App\Domains\Money\Models\Currency;
|
||||
use App\Domains\Receivables\Models\Payment;
|
||||
use App\Domains\Receivables\Models\PaymentAllocation;
|
||||
use App\Domains\Receivables\Models\Transaction;
|
||||
use App\Domains\Sales\Contracts\InvoicePdfDataProvider;
|
||||
use App\Domains\Taxation\Models\Tax;
|
||||
use App\Platform\Mail\Models\EmailLog;
|
||||
use App\Platform\Pdf\Concerns\GeneratesPdf;
|
||||
use App\Platform\Pdf\Rendering\PdfHtmlSanitizer;
|
||||
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
|
||||
use App\Support\SafeOrderBy;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Str;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
class Invoice extends Model implements HasMedia
|
||||
{
|
||||
protected $table = 'invoices';
|
||||
|
||||
use GeneratesPdf;
|
||||
use HasCustomFields;
|
||||
use HasFactory;
|
||||
use InteractsWithMedia;
|
||||
|
||||
public const STATUS_DRAFT = 'DRAFT';
|
||||
|
||||
public const STATUS_SENT = 'SENT';
|
||||
|
||||
public const STATUS_VIEWED = 'VIEWED';
|
||||
|
||||
public const STATUS_COMPLETED = 'COMPLETED';
|
||||
|
||||
public const STATUS_UNPAID = 'UNPAID';
|
||||
|
||||
public const STATUS_PARTIALLY_PAID = 'PARTIALLY_PAID';
|
||||
|
||||
public const STATUS_PAID = 'PAID';
|
||||
|
||||
public const TYPE_INVOICE = 'INVOICE';
|
||||
|
||||
public const TYPE_CREDIT_NOTE = 'CREDIT_NOTE';
|
||||
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
];
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'formattedCreatedAt',
|
||||
'formattedInvoiceDate',
|
||||
'formattedDueDate',
|
||||
'formattedDueAmount',
|
||||
'invoicePdfUrl',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'total' => 'integer',
|
||||
'tax' => 'integer',
|
||||
'sub_total' => 'integer',
|
||||
'discount' => 'float',
|
||||
'discount_val' => 'integer',
|
||||
'exchange_rate' => 'float',
|
||||
];
|
||||
}
|
||||
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(Transaction::class);
|
||||
}
|
||||
|
||||
public function emailLogs(): MorphMany
|
||||
{
|
||||
return $this->morphMany(EmailLog::class, 'mailable');
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(InvoiceItem::class);
|
||||
}
|
||||
|
||||
public function taxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Tax::class);
|
||||
}
|
||||
|
||||
public function allocations(): HasMany
|
||||
{
|
||||
return $this->hasMany(PaymentAllocation::class);
|
||||
}
|
||||
|
||||
public function payments(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Payment::class, 'payment_allocations')
|
||||
->withPivot(['amount', 'base_amount'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function currency(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Currency::class);
|
||||
}
|
||||
|
||||
public function company(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function recurringInvoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RecurringInvoice::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creator_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The original invoice this credit note reverses (null for normal invoices).
|
||||
*/
|
||||
public function relatedInvoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'related_invoice_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit notes that reverse this invoice.
|
||||
*/
|
||||
public function creditNotes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Invoice::class, 'related_invoice_id')
|
||||
->where('type', self::TYPE_CREDIT_NOTE);
|
||||
}
|
||||
|
||||
public function isCreditNote(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_CREDIT_NOTE;
|
||||
}
|
||||
|
||||
public function getInvoicePdfUrlAttribute()
|
||||
{
|
||||
return url('/invoices/pdf/'.$this->unique_hash);
|
||||
}
|
||||
|
||||
public function getPaymentModuleEnabledAttribute()
|
||||
{
|
||||
if (Module::has('Payments')) {
|
||||
return Module::isEnabled('Payments');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getAllowEditAttribute()
|
||||
{
|
||||
// A credited invoice is immutable: its line item ids anchor the lines of
|
||||
// every credit note that reverses it.
|
||||
$hasCreditNotes = $this->relationLoaded('creditNotes')
|
||||
? $this->creditNotes->isNotEmpty()
|
||||
: $this->creditNotes()->exists();
|
||||
|
||||
if ($hasCreditNotes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$retrospective_edit = CompanySetting::getSetting('retrospective_edits', $this->company_id);
|
||||
|
||||
$allowed = true;
|
||||
|
||||
$status = [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_SENT,
|
||||
self::STATUS_VIEWED,
|
||||
self::STATUS_COMPLETED,
|
||||
];
|
||||
|
||||
if ($retrospective_edit == 'disable_on_invoice_sent' && (in_array($this->status, $status)) && ($this->paid_status === Invoice::STATUS_PARTIALLY_PAID || $this->paid_status === Invoice::STATUS_PAID)) {
|
||||
$allowed = false;
|
||||
} elseif ($retrospective_edit == 'disable_on_invoice_partial_paid' && ($this->paid_status === Invoice::STATUS_PARTIALLY_PAID || $this->paid_status === Invoice::STATUS_PAID)) {
|
||||
$allowed = false;
|
||||
} elseif ($retrospective_edit == 'disable_on_invoice_paid' && $this->paid_status === Invoice::STATUS_PAID) {
|
||||
$allowed = false;
|
||||
}
|
||||
|
||||
return $allowed;
|
||||
}
|
||||
|
||||
public function getPreviousStatus(): string
|
||||
{
|
||||
if ($this->viewed) {
|
||||
return self::STATUS_VIEWED;
|
||||
} elseif ($this->sent) {
|
||||
return self::STATUS_SENT;
|
||||
} else {
|
||||
return self::STATUS_DRAFT;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFormattedNotesAttribute($value)
|
||||
{
|
||||
return $this->getNotes();
|
||||
}
|
||||
|
||||
public function getFormattedCreatedAtAttribute($value)
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->created_at)->format($dateFormat);
|
||||
}
|
||||
|
||||
public function getFormattedDueDateAttribute($value)
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->due_date)->translatedFormat($dateFormat);
|
||||
}
|
||||
|
||||
public function getFormattedDueAmountAttribute($value)
|
||||
{
|
||||
$currency = $this->currency;
|
||||
|
||||
if (! $currency) {
|
||||
$currency = Currency::findOrFail(CompanySetting::getSetting('currency', $this->company_id));
|
||||
}
|
||||
|
||||
return format_money_pdf($this->due_amount, $currency);
|
||||
}
|
||||
|
||||
public function getFormattedInvoiceDateAttribute($value)
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
$timeFormat = CompanySetting::getSetting('carbon_time_format', $this->company_id);
|
||||
$invoiceTimeEnabled = CompanySetting::getSetting('invoice_use_time', $this->company_id);
|
||||
|
||||
if ($invoiceTimeEnabled === 'YES') {
|
||||
$dateFormat .= ' '.$timeFormat;
|
||||
}
|
||||
|
||||
return Carbon::parse($this->invoice_date)->translatedFormat($dateFormat);
|
||||
}
|
||||
|
||||
public function scopeWhereStatus($query, $status)
|
||||
{
|
||||
return $query->where('invoices.status', $status);
|
||||
}
|
||||
|
||||
public function scopeWherePaidStatus($query, $status)
|
||||
{
|
||||
return $query->where('invoices.paid_status', $status);
|
||||
}
|
||||
|
||||
public function scopeWhereDueStatus($query, $status)
|
||||
{
|
||||
return $query->whereIn('invoices.paid_status', [
|
||||
self::STATUS_UNPAID,
|
||||
self::STATUS_PARTIALLY_PAID,
|
||||
]);
|
||||
}
|
||||
|
||||
public function scopeWhereInvoiceNumber($query, $invoiceNumber)
|
||||
{
|
||||
return $query->where('invoices.invoice_number', 'LIKE', '%'.$invoiceNumber.'%');
|
||||
}
|
||||
|
||||
public function scopeInvoicesBetween($query, $start, $end)
|
||||
{
|
||||
return $query->whereBetween(
|
||||
'invoices.invoice_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeWhereSearch($query, $search)
|
||||
{
|
||||
foreach (explode(' ', $search) as $term) {
|
||||
$query->whereHas('customer', function ($query) use ($term) {
|
||||
$query->where('name', 'LIKE', '%'.$term.'%')
|
||||
->orWhere('contact_name', 'LIKE', '%'.$term.'%')
|
||||
->orWhere('company_name', 'LIKE', '%'.$term.'%');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeWhereOrder($query, $orderByField, $orderBy)
|
||||
{
|
||||
SafeOrderBy::apply($query, $orderByField, $orderBy);
|
||||
}
|
||||
|
||||
public function scopeApplyFilters($query, array $filters)
|
||||
{
|
||||
$filters = collect($filters)->filter()->all();
|
||||
|
||||
return $query->when($filters['search'] ?? null, function ($query, $search) {
|
||||
$query->whereSearch($search);
|
||||
})->when($filters['status'] ?? null, function ($query, $status) {
|
||||
match ($status) {
|
||||
self::STATUS_UNPAID, self::STATUS_PARTIALLY_PAID, self::STATUS_PAID => $query->wherePaidStatus($status),
|
||||
'DUE' => $query->whereDueStatus($status),
|
||||
default => $query->whereStatus($status),
|
||||
};
|
||||
})->when($filters['paid_status'] ?? null, function ($query, $paidStatus) {
|
||||
$query->wherePaidStatus($paidStatus);
|
||||
})->when($filters['invoice_id'] ?? null, function ($query, $invoiceId) {
|
||||
$query->whereInvoice($invoiceId);
|
||||
})->when($filters['invoice_number'] ?? null, function ($query, $invoiceNumber) {
|
||||
$query->whereInvoiceNumber($invoiceNumber);
|
||||
})->when(($filters['from_date'] ?? null) && ($filters['to_date'] ?? null), function ($query) use ($filters) {
|
||||
$start = Carbon::parse($filters['from_date']);
|
||||
$end = Carbon::parse($filters['to_date']);
|
||||
$query->invoicesBetween($start, $end);
|
||||
})->when($filters['customer_id'] ?? null, function ($query, $customerId) {
|
||||
$query->where('customer_id', $customerId);
|
||||
})->when($filters['orderByField'] ?? null, function ($query, $orderByField) use ($filters) {
|
||||
$orderBy = $filters['orderBy'] ?? 'desc';
|
||||
|
||||
SafeOrderBy::apply($query, $orderByField, $orderBy);
|
||||
}, function ($query) {
|
||||
$query->orderBy('sequence_number', 'desc');
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeWhereInvoice($query, $invoice_id)
|
||||
{
|
||||
$query->orWhere('id', $invoice_id);
|
||||
}
|
||||
|
||||
public function getEstimateTemplateName(): string
|
||||
{
|
||||
$templateName = Str::replace('invoice', 'estimate', $this->template_name);
|
||||
|
||||
// Empty image format: only the names are wanted here, and the default
|
||||
// builds a base64 preview for every template to answer that.
|
||||
$names = array_column(PdfTemplateUtils::getFormattedTemplates('estimate', ''), 'name');
|
||||
|
||||
if (! in_array($templateName, $names)) {
|
||||
$templateName = 'estimate1';
|
||||
}
|
||||
|
||||
return $templateName;
|
||||
}
|
||||
|
||||
public function scopeWhereCompany($query)
|
||||
{
|
||||
$query->where('invoices.company_id', request()->header('company'));
|
||||
}
|
||||
|
||||
public function scopeWhereCompanyId($query, $company)
|
||||
{
|
||||
$query->where('invoices.company_id', $company);
|
||||
}
|
||||
|
||||
public function scopeWhereCustomer($query, $customer_id)
|
||||
{
|
||||
$query->where('invoices.customer_id', $customer_id);
|
||||
}
|
||||
|
||||
public function scopePaginateData($query, $limit)
|
||||
{
|
||||
if ($limit == 'all') {
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
return $query->paginate($limit);
|
||||
}
|
||||
|
||||
public function getPDFData(): mixed
|
||||
{
|
||||
return app(InvoicePdfDataProvider::class)->getPdfData($this);
|
||||
}
|
||||
|
||||
public function getEmailAttachmentSetting(): bool
|
||||
{
|
||||
$invoiceAsAttachment = CompanySetting::getSetting('invoice_email_attachment', $this->company_id);
|
||||
|
||||
if ($invoiceAsAttachment == 'NO') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCompanyAddress(): string|false
|
||||
{
|
||||
if ($this->company && (! $this->company->address()->exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = CompanySetting::getSetting('invoice_company_address_format', $this->company_id);
|
||||
|
||||
return $this->getFormattedString($format);
|
||||
}
|
||||
|
||||
public function getCustomerShippingAddress(): string|false
|
||||
{
|
||||
if ($this->customer && (! $this->customer->shippingAddress()->exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = CompanySetting::getSetting('invoice_shipping_address_format', $this->company_id);
|
||||
|
||||
return $this->getFormattedString($format);
|
||||
}
|
||||
|
||||
public function getCustomerBillingAddress(): string|false
|
||||
{
|
||||
if ($this->customer && (! $this->customer->billingAddress()->exists())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$format = CompanySetting::getSetting('invoice_billing_address_format', $this->company_id);
|
||||
|
||||
return $this->getFormattedString($format);
|
||||
}
|
||||
|
||||
public function getNotes(): string
|
||||
{
|
||||
return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes));
|
||||
}
|
||||
|
||||
public function getEmailString(string $body): string
|
||||
{
|
||||
$values = array_merge($this->getFieldsArray(), $this->getExtraFields());
|
||||
|
||||
$body = strtr($body, $values);
|
||||
|
||||
return preg_replace('/{(.*?)}/', '', $body);
|
||||
}
|
||||
|
||||
public function getExtraFields(): array
|
||||
{
|
||||
return [
|
||||
'{INVOICE_DATE}' => $this->formattedInvoiceDate,
|
||||
'{INVOICE_DUE_DATE}' => $this->formattedDueDate,
|
||||
'{INVOICE_NUMBER}' => $this->invoice_number,
|
||||
'{INVOICE_REF_NUMBER}' => $this->reference_number,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an amount to the invoice's due balance and recalculate the paid status.
|
||||
*/
|
||||
public function addInvoicePayment(int $amount): void
|
||||
{
|
||||
$this->due_amount += $amount;
|
||||
$this->base_due_amount = $this->due_amount * $this->exchange_rate;
|
||||
|
||||
$this->changeInvoiceStatus($this->due_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract an amount from the invoice's due balance and recalculate the paid status.
|
||||
*/
|
||||
public function subtractInvoicePayment(int $amount): void
|
||||
{
|
||||
$this->due_amount -= $amount;
|
||||
$this->base_due_amount = $this->due_amount * $this->exchange_rate;
|
||||
|
||||
$this->changeInvoiceStatus($this->due_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the invoice status and paid_status based on the remaining due amount.
|
||||
*
|
||||
* Returns an empty array for negative amounts, marks as paid when zero,
|
||||
* unpaid when equal to total, or partially paid otherwise.
|
||||
*/
|
||||
public function getInvoiceStatusByAmount(int $amount): array
|
||||
{
|
||||
if ($amount < 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($amount == 0) {
|
||||
$data = [
|
||||
'status' => Invoice::STATUS_COMPLETED,
|
||||
'paid_status' => Invoice::STATUS_PAID,
|
||||
'overdue' => false,
|
||||
];
|
||||
} elseif ($amount == $this->total) {
|
||||
$data = [
|
||||
'status' => $this->getPreviousStatus(),
|
||||
'paid_status' => Invoice::STATUS_UNPAID,
|
||||
];
|
||||
} else {
|
||||
$data = [
|
||||
'status' => $this->getPreviousStatus(),
|
||||
'paid_status' => Invoice::STATUS_PARTIALLY_PAID,
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the invoice status change immediately based on the given due amount.
|
||||
*/
|
||||
public function changeInvoiceStatus(int $amount): void
|
||||
{
|
||||
$status = $this->getInvoiceStatusByAmount($amount);
|
||||
if (! empty($status)) {
|
||||
foreach ($status as $key => $value) {
|
||||
$this->setAttribute($key, $value);
|
||||
}
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Models;
|
||||
|
||||
use App\Domains\Catalog\Models\Item;
|
||||
use App\Domains\Metadata\Concerns\HasCustomFields;
|
||||
use App\Domains\Taxation\Models\Tax;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoiceItem extends Model
|
||||
{
|
||||
protected $table = 'invoice_items';
|
||||
|
||||
use HasCustomFields;
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'price' => 'integer',
|
||||
'total' => 'integer',
|
||||
'discount' => 'float',
|
||||
'quantity' => 'float',
|
||||
'discount_val' => 'integer',
|
||||
'tax' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function invoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class);
|
||||
}
|
||||
|
||||
public function taxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Tax::class);
|
||||
}
|
||||
|
||||
public function recurringInvoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RecurringInvoice::class);
|
||||
}
|
||||
|
||||
public function scopeWhereCompany(Builder $query, int $company_id): void
|
||||
{
|
||||
$query->where('company_id', $company_id);
|
||||
}
|
||||
|
||||
public function scopeInvoicesBetween(Builder $query, Carbon $start, Carbon $end): void
|
||||
{
|
||||
$query->whereHas('invoice', function ($query) use ($start, $end) {
|
||||
$query->whereBetween(
|
||||
'invoice_date',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeApplyInvoiceFilters(Builder $query, array $filters): void
|
||||
{
|
||||
$filters = collect($filters);
|
||||
|
||||
if ($filters->get('from_date') && $filters->get('to_date')) {
|
||||
$start = Carbon::createFromFormat('Y-m-d', $filters->get('from_date'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $filters->get('to_date'));
|
||||
$query->invoicesBetween($start, $end);
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeItemAttributes(Builder $query): void
|
||||
{
|
||||
$query->select(
|
||||
DB::raw('sum(quantity) as total_quantity, sum(base_total) as total_amount, invoice_items.name')
|
||||
)->groupBy('invoice_items.name');
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Models;
|
||||
|
||||
use App\Domains\Accounts\Models\Company;
|
||||
use App\Domains\Accounts\Models\CompanySetting;
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Domains\Contacts\Models\Customer;
|
||||
use App\Domains\Metadata\Concerns\HasCustomFields;
|
||||
use App\Domains\Money\Models\Currency;
|
||||
use App\Domains\Taxation\Models\Tax;
|
||||
use App\Support\SafeOrderBy;
|
||||
use Carbon\Carbon;
|
||||
use Cron;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class RecurringInvoice extends Model
|
||||
{
|
||||
protected $table = 'recurring_invoices';
|
||||
|
||||
use HasCustomFields;
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'starts_at',
|
||||
];
|
||||
|
||||
public const NONE = 'NONE';
|
||||
|
||||
public const COUNT = 'COUNT';
|
||||
|
||||
public const DATE = 'DATE';
|
||||
|
||||
public const COMPLETED = 'COMPLETED';
|
||||
|
||||
public const ON_HOLD = 'ON_HOLD';
|
||||
|
||||
public const ACTIVE = 'ACTIVE';
|
||||
|
||||
protected $appends = [
|
||||
'formattedCreatedAt',
|
||||
'formattedStartsAt',
|
||||
'formattedNextInvoiceAt',
|
||||
'formattedLimitDate',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'exchange_rate' => 'float',
|
||||
'send_automatically' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormattedStartsAtAttribute()
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->starts_at)->translatedFormat($dateFormat);
|
||||
}
|
||||
|
||||
public function getFormattedNextInvoiceAtAttribute()
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->next_invoice_at)->translatedFormat($dateFormat);
|
||||
}
|
||||
|
||||
public function getFormattedLimitDateAttribute()
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->limit_date)->format($dateFormat);
|
||||
}
|
||||
|
||||
public function getFormattedCreatedAtAttribute()
|
||||
{
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
|
||||
|
||||
return Carbon::parse($this->created_at)->format($dateFormat);
|
||||
}
|
||||
|
||||
public function invoices(): HasMany
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
|
||||
public function taxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Tax::class);
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(InvoiceItem::class);
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function company(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function currency(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Currency::class);
|
||||
}
|
||||
|
||||
public function scopeWhereCompany($query)
|
||||
{
|
||||
$query->where('recurring_invoices.company_id', request()->header('company'));
|
||||
}
|
||||
|
||||
public function scopePaginateData($query, $limit)
|
||||
{
|
||||
if ($limit == 'all') {
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
return $query->paginate($limit);
|
||||
}
|
||||
|
||||
public function scopeWhereOrder($query, $orderByField, $orderBy)
|
||||
{
|
||||
SafeOrderBy::apply($query, $orderByField, $orderBy);
|
||||
}
|
||||
|
||||
public function scopeWhereStatus($query, $status)
|
||||
{
|
||||
return $query->where('recurring_invoices.status', $status);
|
||||
}
|
||||
|
||||
public function scopeWhereCustomer($query, $customer_id)
|
||||
{
|
||||
$query->where('customer_id', $customer_id);
|
||||
}
|
||||
|
||||
public function scopeRecurringInvoicesStartBetween($query, $start, $end)
|
||||
{
|
||||
return $query->whereBetween(
|
||||
'starts_at',
|
||||
[$start->format('Y-m-d'), $end->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeWhereSearch($query, $search)
|
||||
{
|
||||
foreach (explode(' ', $search) as $term) {
|
||||
$query->whereHas('customer', function ($query) use ($term) {
|
||||
$query->where('name', 'LIKE', '%'.$term.'%')
|
||||
->orWhere('contact_name', 'LIKE', '%'.$term.'%')
|
||||
->orWhere('company_name', 'LIKE', '%'.$term.'%');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function scopeApplyFilters($query, array $filters)
|
||||
{
|
||||
$filters = collect($filters);
|
||||
|
||||
if ($filters->get('status') && $filters->get('status') !== 'ALL') {
|
||||
$query->whereStatus($filters->get('status'));
|
||||
}
|
||||
|
||||
if ($filters->get('search')) {
|
||||
$query->whereSearch($filters->get('search'));
|
||||
}
|
||||
|
||||
if ($filters->get('from_date') && $filters->get('to_date')) {
|
||||
$start = Carbon::createFromFormat('Y-m-d', $filters->get('from_date'));
|
||||
$end = Carbon::createFromFormat('Y-m-d', $filters->get('to_date'));
|
||||
$query->recurringInvoicesStartBetween($start, $end);
|
||||
}
|
||||
|
||||
if ($filters->get('customer_id')) {
|
||||
$query->whereCustomer($filters->get('customer_id'));
|
||||
}
|
||||
|
||||
if ($filters->get('orderByField') || $filters->get('orderBy')) {
|
||||
$field = $filters->get('orderByField') ? $filters->get('orderByField') : 'created_at';
|
||||
$orderBy = $filters->get('orderBy') ? $filters->get('orderBy') : 'asc';
|
||||
$query->whereOrder($field, $orderBy);
|
||||
}
|
||||
}
|
||||
|
||||
public function markStatusAsCompleted(): void
|
||||
{
|
||||
if ($this->status == $this->status) {
|
||||
$this->status = self::COMPLETED;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getNextInvoiceDate(string $frequency, string $starts_at): string
|
||||
{
|
||||
$cron = new Cron\CronExpression($frequency);
|
||||
$timezone = config('app.timezone', 'UTC');
|
||||
|
||||
return $cron->getNextRunDate($starts_at, 0, false, $timezone)->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function updateNextInvoiceDate(): void
|
||||
{
|
||||
$nextInvoiceAt = self::getNextInvoiceDate($this->frequency, $this->starts_at);
|
||||
|
||||
$this->next_invoice_at = $nextInvoiceAt;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Policies;
|
||||
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Domains\Sales\Models\Estimate;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Silber\Bouncer\BouncerFacade;
|
||||
|
||||
class EstimatePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
if (BouncerFacade::can('view-estimate', Estimate::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function view(User $user, Estimate $estimate): bool
|
||||
{
|
||||
if (BouncerFacade::can('view-estimate', $estimate) && $user->hasCompany($estimate->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
if (BouncerFacade::can('create-estimate', Estimate::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function update(User $user, Estimate $estimate): bool
|
||||
{
|
||||
if (BouncerFacade::can('edit-estimate', $estimate) && $user->hasCompany($estimate->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete(User $user, Estimate $estimate): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-estimate', $estimate) && $user->hasCompany($estimate->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function restore(User $user, Estimate $estimate): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-estimate', $estimate) && $user->hasCompany($estimate->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function forceDelete(User $user, Estimate $estimate): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-estimate', $estimate) && $user->hasCompany($estimate->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can send email of the model.
|
||||
*
|
||||
* @param Estimate $payment
|
||||
* @return mixed
|
||||
*/
|
||||
public function send(User $user, Estimate $estimate)
|
||||
{
|
||||
if (BouncerFacade::can('send-estimate', $estimate) && $user->hasCompany($estimate->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteMultiple(User $user)
|
||||
{
|
||||
if (BouncerFacade::can('delete-estimate', Estimate::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Policies;
|
||||
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Domains\Receivables\Models\Payment;
|
||||
use App\Domains\Sales\Models\Invoice;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Silber\Bouncer\BouncerFacade;
|
||||
|
||||
class InvoicePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
if (BouncerFacade::can('view-invoice', Invoice::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function view(User $user, Invoice $invoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('view-invoice', $invoice) && $user->hasCompany($invoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
if (BouncerFacade::can('create-invoice', Invoice::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function update(User $user, Invoice $invoice): bool
|
||||
{
|
||||
// A credit note is a reversal document: it is immutable once minted,
|
||||
// because saving it back through the invoice form would recompute its
|
||||
// totals positive.
|
||||
if ($invoice->isCreditNote()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BouncerFacade::can('edit-invoice', $invoice) && $user->hasCompany($invoice->company_id)) {
|
||||
return $invoice->allow_edit;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete(User $user, Invoice $invoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-invoice', $invoice) && $user->hasCompany($invoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function restore(User $user, Invoice $invoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-invoice', $invoice) && $user->hasCompany($invoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function forceDelete(User $user, Invoice $invoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-invoice', $invoice) && $user->hasCompany($invoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can send email of the model.
|
||||
*
|
||||
* @param Payment $payment
|
||||
* @return mixed
|
||||
*/
|
||||
public function send(User $user, Invoice $invoice)
|
||||
{
|
||||
if (BouncerFacade::can('send-invoice', $invoice) && $user->hasCompany($invoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteMultiple(User $user)
|
||||
{
|
||||
if (BouncerFacade::can('delete-invoice', Invoice::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Sales\Policies;
|
||||
|
||||
use App\Domains\Accounts\Models\User;
|
||||
use App\Domains\Sales\Models\RecurringInvoice;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
use Silber\Bouncer\BouncerFacade;
|
||||
|
||||
class RecurringInvoicePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
if (BouncerFacade::can('view-recurring-invoice', RecurringInvoice::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function view(User $user, RecurringInvoice $recurringInvoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('view-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
if (BouncerFacade::can('create-recurring-invoice', RecurringInvoice::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function update(User $user, RecurringInvoice $recurringInvoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('edit-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function delete(User $user, RecurringInvoice $recurringInvoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function restore(User $user, RecurringInvoice $recurringInvoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @return Response|bool
|
||||
*/
|
||||
public function forceDelete(User $user, RecurringInvoice $recurringInvoice): bool
|
||||
{
|
||||
if (BouncerFacade::can('delete-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteMultiple(User $user)
|
||||
{
|
||||
if (BouncerFacade::can('delete-recurring-invoice', RecurringInvoice::class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user