TaxReports
This commit is contained in:
parent
e204862488
commit
b78ba2b1fc
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Reports;
|
||||||
|
|
||||||
|
use App\Http\Controllers\BaseController;
|
||||||
|
use App\Http\Requests\Report\GenericReportRequest;
|
||||||
|
use App\Jobs\Report\PreviewReport;
|
||||||
|
use App\Jobs\Report\SendToAdmin;
|
||||||
|
use App\Services\Report\TaxPeriodReport;
|
||||||
|
use App\Utils\Traits\MakesHash;
|
||||||
|
|
||||||
|
class TaxPeriodReportController extends BaseController
|
||||||
|
{
|
||||||
|
use MakesHash;
|
||||||
|
|
||||||
|
private string $filename = 'tax_period.xlsx';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/reports/tasks",
|
||||||
|
* operationId="getTaskReport",
|
||||||
|
* tags={"reports"},
|
||||||
|
* summary="Task reports",
|
||||||
|
* description="Export task reports",
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/GenericReportSchema")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="success",
|
||||||
|
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||||
|
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="default",
|
||||||
|
* description="Unexpected Error",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function __invoke(GenericReportRequest $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($request->has('send_email') && $request->get('send_email') && $request->missing('output')) {
|
||||||
|
SendToAdmin::dispatch($user->company(), $request->all(), TaxPeriodReport::class, $this->filename);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'working...'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = \Illuminate\Support\Str::uuid();
|
||||||
|
|
||||||
|
PreviewReport::dispatch($user->company(), $request->all(), TaxPeriodReport::class, $hash);
|
||||||
|
|
||||||
|
return response()->json(['message' => $hash], 200);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
||||||
use App\Listeners\Invoice\InvoiceTransactionEventEntryAccrual;
|
use App\Listeners\Invoice\InvoiceTransactionEventEntryCash;
|
||||||
|
|
||||||
class InvoiceTaxSummary implements ShouldQueue
|
class InvoiceTaxSummary implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -165,7 +165,7 @@ class InvoiceTaxSummary implements ShouldQueue
|
||||||
Invoice::withTrashed()
|
Invoice::withTrashed()
|
||||||
->with('payments')
|
->with('payments')
|
||||||
->where('company_id', $company->id)
|
->where('company_id', $company->id)
|
||||||
->whereIn('status_id', [3,4,5]) // Paid statuses
|
->whereIn('status_id', [3,4]) // Paid statuses
|
||||||
->where('is_deleted', 0)
|
->where('is_deleted', 0)
|
||||||
->whereColumn('amount', '!=', 'balance')
|
->whereColumn('amount', '!=', 'balance')
|
||||||
->whereHas('client', function ($query) {
|
->whereHas('client', function ($query) {
|
||||||
|
|
@ -190,7 +190,7 @@ class InvoiceTaxSummary implements ShouldQueue
|
||||||
})
|
})
|
||||||
->cursor()
|
->cursor()
|
||||||
->each(function (Invoice $invoice) use ($startDate, $endDate) {
|
->each(function (Invoice $invoice) use ($startDate, $endDate) {
|
||||||
(new InvoiceTransactionEventEntryAccrual())->run($invoice, $startDate, $endDate);
|
(new InvoiceTransactionEventEntryCash())->run($invoice, $startDate, $endDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,12 @@ class InvoiceTransactionEventEntry
|
||||||
* @param Invoice $invoice
|
* @param Invoice $invoice
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function run($invoice)
|
public function run(Invoice $invoice, ?string $force_period = null)
|
||||||
{
|
{
|
||||||
$this->setPaidRatio($invoice);
|
$this->setPaidRatio($invoice);
|
||||||
|
|
||||||
|
$period = $force_period ?? now()->endOfMonth()->format('Y-m-d');
|
||||||
|
|
||||||
$this->payments = $invoice->payments->flatMap(function ($payment) {
|
$this->payments = $invoice->payments->flatMap(function ($payment) {
|
||||||
return $payment->invoices()->get()->map(function ($invoice) use ($payment) {
|
return $payment->invoices()->get()->map(function ($invoice) use ($payment) {
|
||||||
return [
|
return [
|
||||||
|
|
@ -66,7 +68,7 @@ class InvoiceTransactionEventEntry
|
||||||
'event_id' => TransactionEvent::INVOICE_UPDATED,
|
'event_id' => TransactionEvent::INVOICE_UPDATED,
|
||||||
'timestamp' => now()->timestamp,
|
'timestamp' => now()->timestamp,
|
||||||
'metadata' => $this->getMetadata($invoice),
|
'metadata' => $this->getMetadata($invoice),
|
||||||
'period' => now()->endOfMonth()->format('Y-m-d'),
|
'period' => $period,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoice Ninja (https://invoiceninja.com).
|
|
||||||
*
|
|
||||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
|
||||||
*
|
|
||||||
* @license https://www.elastic.co/licensing/elastic-license
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Listeners\Invoice;
|
|
||||||
|
|
||||||
use App\Models\Invoice;
|
|
||||||
use App\Models\Activity;
|
|
||||||
use App\Models\TransactionEvent;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use App\DataMapper\TaxReport\TaxDetail;
|
|
||||||
use App\DataMapper\TaxReport\TaxReport;
|
|
||||||
use App\DataMapper\TaxReport\TaxSummary;
|
|
||||||
use App\Repositories\ActivityRepository;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use App\DataMapper\TransactionEventMetadata;
|
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
|
|
||||||
class InvoiceTransactionEventEntryAccrual
|
|
||||||
{
|
|
||||||
private Collection $payments;
|
|
||||||
|
|
||||||
private float $paid_ratio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the event.
|
|
||||||
*
|
|
||||||
* @param Invoice $invoice
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function run($invoice, $start_date, $end_date)
|
|
||||||
{
|
|
||||||
|
|
||||||
$this->setPaidRatio($invoice);
|
|
||||||
|
|
||||||
$this->payments = $invoice->payments->flatMap(function ($payment) use ($start_date, $end_date) {
|
|
||||||
return $payment->invoices()->get()->map(function ($invoice) use ($payment) {
|
|
||||||
return [
|
|
||||||
'number' => $payment->number,
|
|
||||||
'amount' => $invoice->pivot->amount,
|
|
||||||
'refunded' => $invoice->pivot->refunded,
|
|
||||||
'date' => $invoice->pivot->created_at->format('Y-m-d'),
|
|
||||||
];
|
|
||||||
})->filter(function ($payment) use ($start_date, $end_date) {
|
|
||||||
// Filter payments where the pivot created_at is within the date boundaries
|
|
||||||
return \Carbon\Carbon::parse($payment['date'])->isBetween($start_date, $end_date);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
TransactionEvent::create([
|
|
||||||
'invoice_id' => $invoice->id,
|
|
||||||
'client_id' => $invoice->client_id,
|
|
||||||
'client_balance' => $invoice->client->balance,
|
|
||||||
'client_paid_to_date' => $invoice->client->paid_to_date,
|
|
||||||
'client_credit_balance' => $invoice->client->credit_balance,
|
|
||||||
'invoice_balance' => $invoice->balance ?? 0,
|
|
||||||
'invoice_amount' => $invoice->amount ?? 0 ,
|
|
||||||
'invoice_partial' => $invoice->partial ?? 0,
|
|
||||||
'invoice_paid_to_date' => $invoice->paid_to_date ?? 0,
|
|
||||||
'invoice_status' => $invoice->is_deleted ? 7 : $invoice->status_id,
|
|
||||||
'event_id' => TransactionEvent::PAYMENT_CASH,
|
|
||||||
'timestamp' => now()->timestamp,
|
|
||||||
'metadata' => $this->getMetadata($invoice),
|
|
||||||
'period' => now()->endOfMonth()->format('Y-m-d'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function setPaidRatio(Invoice $invoice): self
|
|
||||||
{
|
|
||||||
if ($invoice->amount == 0) {
|
|
||||||
$this->paid_ratio = 0;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->paid_ratio = $invoice->paid_to_date / $invoice->amount;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calculateRatio(float $amount): float
|
|
||||||
{
|
|
||||||
return round($amount * $this->paid_ratio, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Existing tax details are not deleted, but pending taxes are set to 0
|
|
||||||
*
|
|
||||||
* @param mixed $invoice
|
|
||||||
*/
|
|
||||||
private function getCancelledMetaData($invoice)
|
|
||||||
{
|
|
||||||
|
|
||||||
$calc = $invoice->calc();
|
|
||||||
|
|
||||||
$details = [];
|
|
||||||
|
|
||||||
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
|
|
||||||
|
|
||||||
foreach ($taxes as $tax) {
|
|
||||||
$tax_detail = [
|
|
||||||
'tax_name' => $tax['name'],
|
|
||||||
'tax_rate' => $tax['tax_rate'],
|
|
||||||
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(),
|
|
||||||
'tax_amount' => $this->calculateRatio($tax['total']),
|
|
||||||
'tax_amount_paid' => $this->calculateRatio($tax['total']),
|
|
||||||
'tax_amount_remaining' => 0,
|
|
||||||
];
|
|
||||||
$details[] = $tax_detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TransactionEventMetadata([
|
|
||||||
'tax_report' => [
|
|
||||||
'tax_details' => $details,
|
|
||||||
'payment_history' => $this->payments->toArray(),
|
|
||||||
'tax_summary' => [
|
|
||||||
'total_taxes' => $invoice->total_taxes,
|
|
||||||
'total_paid' => $this->getTotalTaxPaid($invoice),
|
|
||||||
'status' => 'cancelled',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set all tax details to 0
|
|
||||||
*
|
|
||||||
* @param mixed $invoice
|
|
||||||
*/
|
|
||||||
private function getDeletedMetaData($invoice)
|
|
||||||
{
|
|
||||||
|
|
||||||
$calc = $invoice->calc();
|
|
||||||
|
|
||||||
$details = [];
|
|
||||||
|
|
||||||
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
|
|
||||||
|
|
||||||
foreach ($taxes as $tax) {
|
|
||||||
$tax_detail = [
|
|
||||||
'tax_name' => $tax['name'],
|
|
||||||
'tax_rate' => $tax['tax_rate'],
|
|
||||||
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(),
|
|
||||||
'tax_amount' => $tax['total'],
|
|
||||||
'tax_amount_paid' => $this->calculateRatio($tax['total']),
|
|
||||||
'tax_amount_remaining' => 0,
|
|
||||||
];
|
|
||||||
$details[] = $tax_detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TransactionEventMetadata([
|
|
||||||
'tax_report' => [
|
|
||||||
'tax_details' => $details,
|
|
||||||
'payment_history' => $this->payments->toArray(),
|
|
||||||
'tax_summary' => [
|
|
||||||
'total_taxes' => $invoice->total_taxes,
|
|
||||||
'total_paid' => $this->getTotalTaxPaid($invoice),0,
|
|
||||||
'status' => 'deleted',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getMetadata($invoice)
|
|
||||||
{
|
|
||||||
|
|
||||||
// if ($invoice->status_id == Invoice::STATUS_CANCELLED) {
|
|
||||||
// return $this->getCancelledMetaData($invoice);
|
|
||||||
// } elseif ($invoice->is_deleted) {
|
|
||||||
// return $this->getDeletedMetaData($invoice);
|
|
||||||
// }
|
|
||||||
|
|
||||||
$calc = $invoice->calc();
|
|
||||||
|
|
||||||
$details = [];
|
|
||||||
|
|
||||||
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
|
|
||||||
|
|
||||||
foreach ($taxes as $tax) {
|
|
||||||
$tax_detail = [
|
|
||||||
'tax_name' => $tax['name'],
|
|
||||||
'tax_rate' => $tax['tax_rate'],
|
|
||||||
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(),
|
|
||||||
'tax_amount' => $tax['total'],
|
|
||||||
'tax_amount_paid' => $this->calculateRatio($tax['total']),
|
|
||||||
'tax_amount_remaining' => $tax['total'] - $this->calculateRatio($tax['total']),
|
|
||||||
];
|
|
||||||
$details[] = $tax_detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TransactionEventMetadata([
|
|
||||||
'tax_report' => [
|
|
||||||
'tax_details' => $details,
|
|
||||||
'payment_history' => $this->payments->toArray(),
|
|
||||||
'tax_summary' => [
|
|
||||||
'total_taxes' => $invoice->total_taxes,
|
|
||||||
'total_paid' => $this->getTotalTaxPaid($invoice),
|
|
||||||
'status' => 'updated',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTotalTaxPaid($invoice)
|
|
||||||
{
|
|
||||||
if ($invoice->amount == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$total_paid = $this->payments->sum('amount') - $this->payments->sum('refunded');
|
|
||||||
|
|
||||||
return round($invoice->total_taxes * ($total_paid / $invoice->amount), 2);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,41 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Report\XLS;
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
use Carbon\Carbon;
|
namespace App\Services\Report;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
|
use App\Utils\Number;
|
||||||
|
use App\Models\Client;
|
||||||
|
use League\Csv\Writer;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Models\Invoice;
|
||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
|
use App\Export\CSV\BaseExport;
|
||||||
|
use App\Utils\Traits\MakesDates;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use App\Services\Report\TaxSummaryReport;
|
use App\Services\Template\TemplateService;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
||||||
use App\Models\TransactionEvent;
|
use App\Models\TransactionEvent;
|
||||||
|
|
||||||
class TaxReport
|
|
||||||
|
class TaxPeriodReport extends BaseExport
|
||||||
{
|
{
|
||||||
|
use MakesDates;
|
||||||
|
|
||||||
private Spreadsheet $spreadsheet;
|
private Spreadsheet $spreadsheet;
|
||||||
|
|
||||||
private array $data = [];
|
private array $data = [];
|
||||||
|
|
@ -24,36 +44,178 @@ class TaxReport
|
||||||
|
|
||||||
private string $number_format;
|
private string $number_format;
|
||||||
|
|
||||||
public function __construct(public Company $company, private string $start_date, private string $end_date)
|
//is_income_billed = accrual
|
||||||
|
//!is_invoice_billed = cash
|
||||||
|
/**
|
||||||
|
@param array $input
|
||||||
|
[
|
||||||
|
'date_range',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'client_id',
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
public function __construct(public Company $company, public array $input)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
|
|
||||||
$this->start_date = Carbon::parse($this->start_date);
|
|
||||||
$this->end_date = Carbon::parse($this->end_date);
|
|
||||||
|
|
||||||
MultiDB::setDb($this->company->db);
|
MultiDB::setDb($this->company->db);
|
||||||
App::forgetInstance('translator');
|
App::forgetInstance('translator');
|
||||||
App::setLocale($this->company->locale());
|
App::setLocale($this->company->locale());
|
||||||
$t = app('translator');
|
$t = app('translator');
|
||||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||||
|
|
||||||
$this->spreadsheet = new Spreadsheet();
|
$this->calculateDateRange()
|
||||||
|
->initializeData()
|
||||||
$this->buildData()
|
->buildData();
|
||||||
->setCurrencyFormat()
|
|
||||||
->createSummarySheet()
|
|
||||||
->createInvoiceSummarySheetAccrual()
|
|
||||||
->createInvoiceSummarySheetCash()
|
|
||||||
->createInvoiceItemSummarySheetAccrual()
|
|
||||||
->createInvoiceItemSummarySheetCash();
|
|
||||||
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initializeData
|
||||||
|
*
|
||||||
|
* Ensure our dataset has the appropriate transaction events.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
private function initializeData(): self
|
||||||
|
{
|
||||||
|
Invoice::withTrashed()
|
||||||
|
->where('company_id', $this->company->id)
|
||||||
|
->where('is_deleted', 0)
|
||||||
|
->whereIn('status_id', [2,3,4,5])
|
||||||
|
->whereBetween('date', ['1970-01-01', now()->subMonth()->endOfMonth()->format('Y-m-d')])
|
||||||
|
->whereDoesntHave('transaction_events')
|
||||||
|
->cursor()
|
||||||
|
->each(function($invoice){
|
||||||
|
|
||||||
|
if($invoice->status_id == Invoice::STATUS_SENT){
|
||||||
|
(new InvoiceTransactionEventEntry())->run($invoice, \Carbon\Carbon::parse($invoice->date)->endOfMonth()->format('Y-m-d'));
|
||||||
|
}
|
||||||
|
elseif(in_array($invoice->status_id, [Invoice::STATUS_PAID, Invoice::STATUS_PARTIAL])){
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveQuery()
|
||||||
|
{
|
||||||
|
$query = Invoice::query()
|
||||||
|
->withTrashed()
|
||||||
|
->with('transaction_events')
|
||||||
|
->where('company_id', $this->company->id)
|
||||||
|
->where('is_deleted', 0);
|
||||||
|
|
||||||
|
if($this->input['is_income_billed']) //acrrual
|
||||||
|
{
|
||||||
|
$query->whereIn('status_id', [2,3,4])
|
||||||
|
->whereHas('transaction_events', function($query){
|
||||||
|
$query->where('event_id', TransactionEvent::INVOICE_UPDATED)
|
||||||
|
->whereBetween('period', [$this->start_date, $this->end_date]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else //cash
|
||||||
|
{
|
||||||
|
$query->whereIn('status_id', [3,4])
|
||||||
|
->whereHas('transaction_events', function($query){
|
||||||
|
$query->where('event_id', TransactionEvent::PAYMENT_CASH)
|
||||||
|
->whereBetween('period', [$this->start_date, $this->end_date]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->orderBy('balance', 'desc');
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calculateDateRange
|
||||||
|
*
|
||||||
|
* We only support dates as of the end of the last month.
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
private function calculateDateRange(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
switch ($date_range) {
|
||||||
|
case 'last7':
|
||||||
|
case 'last30':
|
||||||
|
case 'this_month':
|
||||||
|
case 'last_month':
|
||||||
|
$this->start_date = now()->startOfMonth()->subMonth()->format('Y-m-d');
|
||||||
|
$this->end_date = now()->startOfMonth()->subMonth()->endOfMonth()->format('Y-m-d');
|
||||||
|
case 'this_quarter':
|
||||||
|
$this->start_date = (new \Carbon\Carbon('0 months'))->startOfQuarter()->format('Y-m-d');
|
||||||
|
$this->end_date = (new \Carbon\Carbon('0 months'))->endOfQuarter()->format('Y-m-d');
|
||||||
|
case 'last_quarter':
|
||||||
|
$this->start_date = (new \Carbon\Carbon('-3 months'))->startOfQuarter()->format('Y-m-d');
|
||||||
|
$this->end_date = (new \Carbon\Carbon('-3 months'))->endOfQuarter()->format('Y-m-d');
|
||||||
|
case 'last365_days':
|
||||||
|
$this->start_date = now()->startOfDay()->subDays(365)->format('Y-m-d');
|
||||||
|
$this->end_date = now()->startOfDay()->format('Y-m-d');
|
||||||
|
case 'this_year':
|
||||||
|
|
||||||
|
$first_month_of_year = $this->company->first_month_of_year ?? 1;
|
||||||
|
$fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1);
|
||||||
|
|
||||||
|
if (now()->lt($fin_year_start)) {
|
||||||
|
$fin_year_start->subYearNoOverflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->start_date = $fin_year_start->format('Y-m-d');
|
||||||
|
$this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d');
|
||||||
|
case 'last_year':
|
||||||
|
|
||||||
|
$first_month_of_year = $this->company->first_month_of_year ?? 1;
|
||||||
|
$fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1);
|
||||||
|
$fin_year_start->subYearNoOverflow();
|
||||||
|
|
||||||
|
if (now()->subYear()->lt($fin_year_start)) {
|
||||||
|
$fin_year_start->subYearNoOverflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->start_date = $fin_year_start->format('Y-m-d');
|
||||||
|
$this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d');
|
||||||
|
case 'custom':
|
||||||
|
|
||||||
|
try {
|
||||||
|
$custom_start_date = Carbon::parse($this->input['start_date']);
|
||||||
|
$custom_end_date = Carbon::parse($this->input['end_date']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$custom_start_date = now()->startOfYear();
|
||||||
|
$custom_end_date = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->start_date = $custom_start_date->format('Y-m-d');
|
||||||
|
$this->end_date = $custom_end_date->format('Y-m-d');
|
||||||
|
case 'all':
|
||||||
|
default:
|
||||||
|
$this->start_date = now()->startOfYear()->format('Y-m-d');
|
||||||
|
$this->end_date = now()->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accrual()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cash()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getXlsFile()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function setCurrencyFormat()
|
public function setCurrencyFormat()
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +244,7 @@ class TaxReport
|
||||||
}
|
}
|
||||||
|
|
||||||
// All invoices within a time period - regardless if they are paid or not!
|
// All invoices within a time period - regardless if they are paid or not!
|
||||||
public function createInvoiceSummarySheetAccrual()
|
public function createInvoiceSummarySheet()
|
||||||
{
|
{
|
||||||
|
|
||||||
$worksheet = $this->spreadsheet->createSheet();
|
$worksheet = $this->spreadsheet->createSheet();
|
||||||
|
|
@ -98,24 +260,7 @@ class TaxReport
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All paid invoices within a time period
|
public function createInvoiceItemSummarySheet()
|
||||||
public function createInvoiceSummarySheetCash()
|
|
||||||
{
|
|
||||||
|
|
||||||
$worksheet = $this->spreadsheet->createSheet();
|
|
||||||
$worksheet->setTitle(ctrans('texts.invoice')." ".ctrans('texts.cash_accounting'));
|
|
||||||
|
|
||||||
$worksheet->fromArray($this->data['cash']['invoices'], null, 'A1');
|
|
||||||
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode($this->company->date_format()); // Invoice date column
|
|
||||||
$worksheet->getStyle('C:C')->getNumberFormat()->setFormatCode($this->currency_format); // Invoice total column
|
|
||||||
$worksheet->getStyle('D:D')->getNumberFormat()->setFormatCode($this->currency_format); // Paid amount column
|
|
||||||
$worksheet->getStyle('E:E')->getNumberFormat()->setFormatCode($this->currency_format); // Total taxes column
|
|
||||||
$worksheet->getStyle('F:F')->getNumberFormat()->setFormatCode($this->currency_format); // Tax paid column
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createInvoiceItemSummarySheetAccrual()
|
|
||||||
{
|
{
|
||||||
|
|
||||||
$worksheet = $this->spreadsheet->createSheet();
|
$worksheet = $this->spreadsheet->createSheet();
|
||||||
|
|
@ -134,30 +279,11 @@ class TaxReport
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createInvoiceItemSummarySheetCash()
|
|
||||||
{
|
|
||||||
|
|
||||||
$worksheet = $this->spreadsheet->createSheet();
|
|
||||||
$worksheet->setTitle(ctrans('texts.invoice_item')." ".ctrans('texts.cash_accounting'));
|
|
||||||
$worksheet->fromArray($this->data['cash']['invoice_items'], null, 'A1');
|
|
||||||
|
|
||||||
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode($this->company->date_format()); // Invoice date column
|
|
||||||
$worksheet->getStyle('C:C')->getNumberFormat()->setFormatCode($this->currency_format); // Invoice total column
|
|
||||||
$worksheet->getStyle('D:D')->getNumberFormat()->setFormatCode($this->currency_format); // Paid amount column
|
|
||||||
$worksheet->getStyle('F:F')->getNumberFormat()->setFormatCode($this->number_format."%"); // Tax rate column
|
|
||||||
$worksheet->getStyle('G:G')->getNumberFormat()->setFormatCode($this->currency_format); // Tax amount column
|
|
||||||
$worksheet->getStyle('H:H')->getNumberFormat()->setFormatCode($this->currency_format); // Tax paid column
|
|
||||||
$worksheet->getStyle('I:I')->getNumberFormat()->setFormatCode($this->currency_format); // Taxable amount column
|
|
||||||
// Column J (tax_nexus) is text, so no special formatting needed
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildData()
|
private function buildData()
|
||||||
{
|
{
|
||||||
|
|
||||||
$start_date_instance = $this->start_date;
|
$query = $this->resolveQuery();
|
||||||
$end_date_instance = $this->end_date;
|
|
||||||
|
|
||||||
$this->data['invoices'] = [];
|
$this->data['invoices'] = [];
|
||||||
$this->data['invoices'][] =
|
$this->data['invoices'][] =
|
||||||
|
|
@ -192,20 +318,9 @@ class TaxReport
|
||||||
$this->data['cash']['invoice_items'] = [$invoice_item_headers];
|
$this->data['cash']['invoice_items'] = [$invoice_item_headers];
|
||||||
|
|
||||||
|
|
||||||
Invoice::withTrashed()
|
$query->cursor()
|
||||||
->with('transaction_events')
|
|
||||||
->where('company_id', $this->company->id)
|
|
||||||
->whereHas('transaction_events', function ($query){
|
|
||||||
return $query->where('period', now()->endOfMonth()->format('Y-m-d'));
|
|
||||||
})
|
|
||||||
->cursor()
|
|
||||||
->each(function($invoice){
|
->each(function($invoice){
|
||||||
|
|
||||||
if($invoice->transaction_events->count() == 0){
|
|
||||||
(new InvoiceTransactionEventEntry())->run($invoice);
|
|
||||||
$invoice->load('transaction_events');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var TransactionEvent $invoice_state */
|
/** @var TransactionEvent $invoice_state */
|
||||||
$invoice_state = $invoice->transaction_events()->where('event_id', TransactionEvent::INVOICE_UPDATED)->where('period', now()->endOfMonth()->format('Y-m-d'))->orderBy('timestamp', 'desc')->first();
|
$invoice_state = $invoice->transaction_events()->where('event_id', TransactionEvent::INVOICE_UPDATED)->where('period', now()->endOfMonth()->format('Y-m-d'))->orderBy('timestamp', 'desc')->first();
|
||||||
$payment_state = $invoice->transaction_events()->where('event_id', TransactionEvent::PAYMENT_CASH)->where('period', now()->endOfMonth()->format('Y-m-d'))->orderBy('timestamp', 'desc')->first();
|
$payment_state = $invoice->transaction_events()->where('event_id', TransactionEvent::PAYMENT_CASH)->where('period', now()->endOfMonth()->format('Y-m-d'))->orderBy('timestamp', 'desc')->first();
|
||||||
|
|
@ -281,4 +396,5 @@ class TaxReport
|
||||||
return $fileContent;
|
return $fileContent;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -5613,6 +5613,7 @@ $lang = array(
|
||||||
'include_project_tasks' => 'Include Project Tasks',
|
'include_project_tasks' => 'Include Project Tasks',
|
||||||
'include_project_tasks_help' => 'Also invoice tasks that are part of a project',
|
'include_project_tasks_help' => 'Also invoice tasks that are part of a project',
|
||||||
'tax_nexus' => 'Tax Nexus',
|
'tax_nexus' => 'Tax Nexus',
|
||||||
|
'tax_period_report' => 'Tax Period Report',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $lang;
|
return $lang;
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ use App\Http\Controllers\Reports\ARDetailReportController;
|
||||||
use App\Http\Controllers\Reports\DocumentReportController;
|
use App\Http\Controllers\Reports\DocumentReportController;
|
||||||
use App\Http\Controllers\Reports\ARSummaryReportController;
|
use App\Http\Controllers\Reports\ARSummaryReportController;
|
||||||
use App\Http\Controllers\Reports\QuoteItemReportController;
|
use App\Http\Controllers\Reports\QuoteItemReportController;
|
||||||
|
use App\Http\Controllers\Reports\TaxPeriodReportController;
|
||||||
use App\Http\Controllers\Reports\UserSalesReportController;
|
use App\Http\Controllers\Reports\UserSalesReportController;
|
||||||
use App\Http\Controllers\Reports\TaxSummaryReportController;
|
use App\Http\Controllers\Reports\TaxSummaryReportController;
|
||||||
use App\Http\Controllers\Support\Messages\SendingController;
|
use App\Http\Controllers\Support\Messages\SendingController;
|
||||||
|
|
@ -372,6 +373,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
|
||||||
Route::post('reports/client_balance_report', ClientBalanceReportController::class);
|
Route::post('reports/client_balance_report', ClientBalanceReportController::class);
|
||||||
Route::post('reports/client_sales_report', ClientSalesReportController::class);
|
Route::post('reports/client_sales_report', ClientSalesReportController::class);
|
||||||
Route::post('reports/tax_summary_report', TaxSummaryReportController::class);
|
Route::post('reports/tax_summary_report', TaxSummaryReportController::class);
|
||||||
|
Route::post('reports/tax_period_report', TaxPeriodReportController::class);
|
||||||
Route::post('reports/user_sales_report', UserSalesReportController::class);
|
Route::post('reports/user_sales_report', UserSalesReportController::class);
|
||||||
Route::post('reports/projects', ProjectReportController::class);
|
Route::post('reports/projects', ProjectReportController::class);
|
||||||
Route::post('reports/preview/{hash}', ReportPreviewController::class);
|
Route::post('reports/preview/{hash}', ReportPreviewController::class);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ use App\Factory\InvoiceItemFactory;
|
||||||
use App\Services\Report\TaxSummaryReport;
|
use App\Services\Report\TaxSummaryReport;
|
||||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
|
||||||
use App\Listeners\Invoice\InvoiceTransactionEventEntryAccrual;
|
use App\Listeners\Invoice\InvoiceTransactionEventEntryCash;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
@ -226,7 +226,7 @@ class TaxSummaryReportTest extends TestCase
|
||||||
|
|
||||||
$this->assertEquals($i3->amount, $i3->paid_to_date);
|
$this->assertEquals($i3->amount, $i3->paid_to_date);
|
||||||
|
|
||||||
(new InvoiceTransactionEventEntryAccrual())->run($i3, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
|
(new InvoiceTransactionEventEntryCash())->run($i3, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,7 +268,7 @@ class TaxSummaryReportTest extends TestCase
|
||||||
|
|
||||||
$i2 = $i2->fresh();
|
$i2 = $i2->fresh();
|
||||||
|
|
||||||
(new InvoiceTransactionEventEntryAccrual())->run($i2, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
|
(new InvoiceTransactionEventEntryCash())->run($i2, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
|
||||||
|
|
||||||
$payment = $i2->payments()->first();
|
$payment = $i2->payments()->first();
|
||||||
|
|
||||||
|
|
@ -294,18 +294,18 @@ class TaxSummaryReportTest extends TestCase
|
||||||
|
|
||||||
$payment->refund($data);
|
$payment->refund($data);
|
||||||
|
|
||||||
$pl = new \App\Services\Report\XLS\TaxReport($this->company, '2025-01-01', '2025-12-31');
|
// $pl = new \App\Services\Report\XLS\TaxReport($this->company, '2025-01-01', '2025-12-31');
|
||||||
|
|
||||||
$response = $pl->run()->getXlsFile();
|
// $response = $pl->run()->getXlsFile();
|
||||||
|
|
||||||
$this->assertIsString($response);
|
// $this->assertIsString($response);
|
||||||
|
|
||||||
try{
|
// try{
|
||||||
file_put_contents('/home/david/ttx.xlsx', $response);
|
// file_put_contents('/home/david/ttx.xlsx', $response);
|
||||||
}
|
// }
|
||||||
catch(\Throwable $e){
|
// catch(\Throwable $e){
|
||||||
nlog($e->getMessage());
|
// nlog($e->getMessage());
|
||||||
}
|
// }
|
||||||
|
|
||||||
config(['queue.default' => 'redis']);
|
config(['queue.default' => 'redis']);
|
||||||
|
|
||||||
|
|
@ -376,7 +376,7 @@ class TaxSummaryReportTest extends TestCase
|
||||||
$i2 = $i2->calc()->getInvoice();
|
$i2 = $i2->calc()->getInvoice();
|
||||||
$i2->service()->markPaid();
|
$i2->service()->markPaid();
|
||||||
|
|
||||||
(new InvoiceTransactionEventEntryAccrual())->run($i2, now()->subDays(3000)->format('Y-m-d'), now()->addDays(3000)->format('Y-m-d'));
|
(new InvoiceTransactionEventEntryCash())->run($i2, now()->subDays(3000)->format('Y-m-d'), now()->addDays(3000)->format('Y-m-d'));
|
||||||
|
|
||||||
$pl = new TaxSummaryReport($this->company, $this->payload);
|
$pl = new TaxSummaryReport($this->company, $this->payload);
|
||||||
$response = $pl->run();
|
$response = $pl->run();
|
||||||
|
|
@ -449,12 +449,12 @@ class TaxSummaryReportTest extends TestCase
|
||||||
$i2 = $i2->calc()->getInvoice();
|
$i2 = $i2->calc()->getInvoice();
|
||||||
$i2->service()->markPaid()->save();
|
$i2->service()->markPaid()->save();
|
||||||
|
|
||||||
(new InvoiceTransactionEventEntryAccrual())->run($i2, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
|
(new InvoiceTransactionEventEntryCash())->run($i2, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
|
||||||
|
|
||||||
$tr = new \App\Services\Report\XLS\TaxReport($this->company, '2025-01-01', '2025-12-31');
|
// $tr = new \App\Services\Report\XLS\TaxReport($this->company, '2025-01-01', '2025-12-31');
|
||||||
$response = $tr->run()->getXlsFile();
|
// $response = $tr->run()->getXlsFile();
|
||||||
|
|
||||||
$this->assertNotEmpty($response);
|
// $this->assertNotEmpty($response);
|
||||||
|
|
||||||
$this->assertNotNull(TransactionEvent::where('invoice_id', $i->id)->first());
|
$this->assertNotNull(TransactionEvent::where('invoice_id', $i->id)->first());
|
||||||
$this->assertNotNull(TransactionEvent::where('invoice_id', $i2->id)->first());
|
$this->assertNotNull(TransactionEvent::where('invoice_id', $i2->id)->first());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue