Accrual listeners

This commit is contained in:
David Bomba 2025-08-05 15:57:02 +10:00
parent 33c5b90dce
commit 6895a2c9a0
11 changed files with 480 additions and 52 deletions

View File

@ -12,19 +12,21 @@
namespace App\Jobs\Cron; namespace App\Jobs\Cron;
use Carbon\Carbon;
use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Webhook; use App\Models\Webhook;
use App\Models\Company;
use App\Models\Timezone; use App\Models\Timezone;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Models\TransactionEvent;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; 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 Carbon\Carbon;
use App\Listeners\Invoice\InvoiceTransactionEventEntry; use App\Listeners\Invoice\InvoiceTransactionEventEntry;
use App\Listeners\Invoice\InvoiceTransactionEventEntryAccrual;
class InvoiceTaxSummary implements ShouldQueue class InvoiceTaxSummary implements ShouldQueue
{ {
@ -51,7 +53,6 @@ class InvoiceTaxSummary implements ShouldQueue
$companies = $this->getCompaniesInTimezones($transitioningTimezones); $companies = $this->getCompaniesInTimezones($transitioningTimezones);
foreach ($companies as $company) { foreach ($companies as $company) {
$this->processCompanyTaxSummary($company); $this->processCompanyTaxSummary($company);
} }
} }
@ -64,6 +65,7 @@ class InvoiceTaxSummary implements ShouldQueue
// Get all timezones from the database // Get all timezones from the database
$timezones = app('timezones'); $timezones = app('timezones');
/** @var \App\Models\Timezone $timezone */
foreach ($timezones as $timezone) { foreach ($timezones as $timezone) {
// Calculate the current UTC offset for this timezone (accounting for DST) // Calculate the current UTC offset for this timezone (accounting for DST)
$currentOffset = $this->getCurrentUtcOffset($timezone->name); $currentOffset = $this->getCurrentUtcOffset($timezone->name);
@ -152,11 +154,45 @@ class InvoiceTaxSummary implements ShouldQueue
->whereBetween('date', [$startDate, $endDate]) ->whereBetween('date', [$startDate, $endDate])
->whereDoesntHave('transaction_events', function ($query) use ($todayStart, $todayEnd) { ->whereDoesntHave('transaction_events', function ($query) use ($todayStart, $todayEnd) {
$query->where('timestamp', '>=', $todayStart) $query->where('timestamp', '>=', $todayStart)
->where('timestamp', '<=', $todayEnd); ->where('timestamp', '<=', $todayEnd)
->where('event_id', TransactionEvent::INVOICE_UPDATED);
}) })
->cursor() ->cursor()
->each(function (Invoice $invoice) { ->each(function (Invoice $invoice) {
(new InvoiceTransactionEventEntry())->run($invoice); (new InvoiceTransactionEventEntry())->run($invoice);
}); });
Invoice::withTrashed()
->with('payments')
->where('company_id', $company->id)
->whereIn('status_id', [3,4,5]) // Paid statuses
->where('is_deleted', 0)
->whereColumn('amount', '!=', 'balance')
->whereHas('client', function ($query) {
$query->where('is_deleted', false);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0)
->whereHas('account', function ($q) {
$q->where('is_flagged', false);
});
})
->whereHas('payments', function ($query) use ($startDate, $endDate) {
$query->whereHas('paymentables', function ($subQuery) use ($startDate, $endDate) {
$subQuery->where('paymentable_type', Invoice::class)
->whereBetween('created_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59']);
});
})
->whereDoesntHave('transaction_events', function ($q) use ($todayStart, $todayEnd) {
$q->where('event_id', TransactionEvent::PAYMENT_CASH)
->where('timestamp', '>=', $todayStart)
->where('timestamp', '<=', $todayEnd);
})
->cursor()
->each(function (Invoice $invoice) use ($startDate, $endDate) {
(new InvoiceTransactionEventEntryAccrual())->run($invoice, $startDate, $endDate);
});
} }
} }

View File

@ -64,7 +64,7 @@ class InvoiceTransactionEventEntry
'invoice_partial' => $invoice->partial ?? 0, 'invoice_partial' => $invoice->partial ?? 0,
'invoice_paid_to_date' => $invoice->paid_to_date ?? 0, 'invoice_paid_to_date' => $invoice->paid_to_date ?? 0,
'invoice_status' => $invoice->is_deleted ? 7 : $invoice->status_id, 'invoice_status' => $invoice->is_deleted ? 7 : $invoice->status_id,
'event_id' => $invoice->is_deleted ? TransactionEvent::INVOICE_DELETED : 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' => now()->endOfMonth()->format('Y-m-d'),

View File

@ -0,0 +1,227 @@
<?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::INVOICE_UPDATED,
'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);
}
}

View File

@ -43,14 +43,15 @@ class PaymentTransactionEventEntry implements ShouldQueue
private float $paid_ratio; private float $paid_ratio;
private Collection $payments; private Collection $payments;
/** /**
*/ */
public function __construct(private Payment $payment, private array $invoice_ids, private string $db) public function __construct(private Payment $payment, private array $invoice_ids, private string $db, private float $invoice_adjustment = 0, private int $is_deleted = false)
{} {}
public function handle() public function handle()
{ {
nlog("PaymentTransactionEventEntry::handle");
//payment vs refunded //payment vs refunded
MultiDB::setDb($this->db); MultiDB::setDb($this->db);
@ -81,6 +82,12 @@ class PaymentTransactionEventEntry implements ShouldQueue
$this->setPaidRatio($invoice); $this->setPaidRatio($invoice);
//delete any other payment mutations here if this is a delete event, the refunds are redundant in this time period
$invoice->transaction_events()
->where('event_id', TransactionEvent::PAYMENT_REFUNDED)
->where('period', now()->endOfMonth()->format('Y-m-d'))
->delete();
TransactionEvent::create([ TransactionEvent::create([
'invoice_id' => $invoice->id, 'invoice_id' => $invoice->id,
'client_id' => $invoice->client_id, 'client_id' => $invoice->client_id,
@ -92,7 +99,7 @@ class PaymentTransactionEventEntry implements ShouldQueue
'invoice_partial' => $invoice->partial ?? 0, 'invoice_partial' => $invoice->partial ?? 0,
'invoice_paid_to_date' => $invoice->paid_to_date ?? 0, 'invoice_paid_to_date' => $invoice->paid_to_date ?? 0,
'invoice_status' => $invoice->is_deleted ? 7 : $invoice->status_id, 'invoice_status' => $invoice->is_deleted ? 7 : $invoice->status_id,
'event_id' => $this->payment->is_deleted ? TransactionEvent::PAYMENT_DELETED : TransactionEvent::PAYMENT_REFUNDED, 'event_id' => $this->is_deleted ? TransactionEvent::PAYMENT_DELETED : TransactionEvent::PAYMENT_REFUNDED,
'timestamp' => now()->timestamp, 'timestamp' => now()->timestamp,
'metadata' => $this->getMetadata($invoice), 'metadata' => $this->getMetadata($invoice),
'period' => now()->endOfMonth()->format('Y-m-d'), 'period' => now()->endOfMonth()->format('Y-m-d'),
@ -137,10 +144,14 @@ class PaymentTransactionEventEntry implements ShouldQueue
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); $taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
foreach ($taxes as $tax) { foreach ($taxes as $tax) {
$base_amount = $tax['base_amount'] ?? $calc->getNetSubtotal();
$tax_detail = [ $tax_detail = [
'tax_name' => $tax['name'], 'tax_name' => $tax['name'],
'tax_rate' => $tax['tax_rate'], 'tax_rate' => $tax['tax_rate'],
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(), 'taxable_amount' => $base_amount,
'tax_amount' => $tax['total'], 'tax_amount' => $tax['total'],
'tax_amount_paid' => $this->calculateRatio($tax['total']), 'tax_amount_paid' => $this->calculateRatio($tax['total']),
'tax_amount_remaining' => round($tax['total'] - $this->calculateRatio($tax['total']), 2), 'tax_amount_remaining' => round($tax['total'] - $this->calculateRatio($tax['total']), 2),
@ -178,14 +189,26 @@ class PaymentTransactionEventEntry implements ShouldQueue
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); $taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
foreach ($taxes as $tax) { foreach ($taxes as $tax) {
$base_amount = $tax['base_amount'] ?? $calc->getNetSubtotal();
if($this->invoice_adjustment > 0)
$tax_amount_paid = round(($this->invoice_adjustment / ($base_amount+$tax['total'])) * $tax['total'], 2);
else {
$tax_amount_paid = $this->calculateRatio($tax['total']);
}
$tax_detail = [ $tax_detail = [
'tax_name' => $tax['name'], 'tax_name' => $tax['name'],
'tax_rate' => $tax['tax_rate'], 'tax_rate' => $tax['tax_rate'],
'taxable_amount' => $tax['base_amount'] ?? $calc->getNetSubtotal(), 'taxable_amount' => $base_amount,
'tax_amount' => $tax['total'], 'tax_amount' => $tax['total'],
'tax_amount_paid' => $this->calculateRatio($tax['total']), 'tax_amount_paid' => $tax_amount_paid,
'tax_amount_remaining' => 0, 'tax_amount_remaining' => 0,
'tax_status' => 'payment_deleted',
]; ];
$details[] = $tax_detail; $details[] = $tax_detail;
} }

View File

@ -857,11 +857,15 @@ class Invoice extends BaseModel
$formatted_string = "<div id=\"payment-schedule\">"; $formatted_string = "<div id=\"payment-schedule\">";
foreach($schedule->parameters['schedule'] as $item){ $formatted_string .= "<p><span class=\"payment-schedule-title\"><b>".ctrans('texts.payment_schedule')."</b></span></p>";
foreach($schedule->parameters['schedule'] as $key => $item){
$amount = $item['is_amount'] ? $item['amount'] : round($this->amount * ($item['amount']/100),2); $amount = $item['is_amount'] ? $item['amount'] : round($this->amount * ($item['amount']/100),2);
$amount = \App\Utils\Number::formatMoney($amount, $this->client); $amount = \App\Utils\Number::formatMoney($amount, $this->client);
$formatted_string .= "<p><span class=\"payment-schedule-date\">".$this->formatDate($item['date'], $this->client->date_format()) . "</span> - <span class=\"payment-schedule-amount\"> " . $amount."</span></p>"; $schedule_text = ctrans('texts.payment_schedule_table', ['key' => $key+1, 'date' => $this->formatDate($item['date'], $this->client->date_format()), 'amount' => $amount]);
$formatted_string .= "<p><span class=\"payment-schedule\">".$schedule_text."</span></p>";
} }
$formatted_string .= "</div>"; $formatted_string .= "</div>";

View File

@ -38,7 +38,7 @@ use App\DataMapper\TransactionEventMetadata;
* @property int $event_id * @property int $event_id
* @property int $timestamp * @property int $timestamp
* @property array|null $payment_request * @property array|null $payment_request
* @property array|null $metadata * @property TransactionEventMetadata|null $metadata
* @property string $credit_balance * @property string $credit_balance
* @property string $credit_amount * @property string $credit_amount
* @property int|null $credit_status * @property int|null $credit_status
@ -65,4 +65,6 @@ class TransactionEvent extends StaticModel
public const PAYMENT_REFUNDED = 2; public const PAYMENT_REFUNDED = 2;
public const PAYMENT_DELETED = 3; public const PAYMENT_DELETED = 3;
public const PAYMENT_CASH = 4;
} }

View File

@ -89,7 +89,7 @@ class DeletePayment
if ($this->payment->invoices()->exists()) { if ($this->payment->invoices()->exists()) {
$invoice_ids = $this->payment->invoices()->pluck('id'); $invoice_ids = $this->payment->invoices()->pluck('invoices.id')->toArray();
$this->payment->invoices()->each(function ($paymentable_invoice) { $this->payment->invoices()->each(function ($paymentable_invoice) {
$net_deletable = $paymentable_invoice->pivot->amount - $paymentable_invoice->pivot->refunded; $net_deletable = $paymentable_invoice->pivot->amount - $paymentable_invoice->pivot->refunded;
@ -161,10 +161,10 @@ class DeletePayment
} }
PaymentTransactionEventEntry::dispatch($this->payment, [$paymentable_invoice->id], $this->payment->company->db, $net_deletable, true);
}); });
PaymentTransactionEventEntry::dispatch($this->payment, $invoice_ids, $this->payment->company->db);
} }
//sometimes the payment is NOT created properly, this catches the payment and prevents the paid to date reducing inappropriately. //sometimes the payment is NOT created properly, this catches the payment and prevents the paid to date reducing inappropriately.

View File

@ -313,7 +313,7 @@ class RefundPayment
} }
PaymentTransactionEventEntry::dispatch($this->payment, array_column($this->refund_data['invoices'], 'invoice_id'), $this->payment->company->db); PaymentTransactionEventEntry::dispatch($this->payment, array_column($this->refund_data['invoices'], 'invoice_id'), $this->payment->company->db, 0, false);
} else { } else {
//if we are refunding and no payments have been tagged, then we need to decrement the client->paid_to_date by the total refund amount. //if we are refunding and no payments have been tagged, then we need to decrement the client->paid_to_date by the total refund amount.

View File

@ -12,6 +12,7 @@ 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;
class TaxReport class TaxReport
{ {
@ -83,7 +84,7 @@ class TaxReport
$worksheet = $this->spreadsheet->createSheet(); $worksheet = $this->spreadsheet->createSheet();
$worksheet->setTitle(ctrans('texts.invoice')." ".ctrans('texts.cash_vs_accrual')); $worksheet->setTitle(ctrans('texts.invoice')." ".ctrans('texts.cash_vs_accrual'));
$worksheet->fromArray($this->data['invoices'], null, 'A1'); $worksheet->fromArray($this->data['accrual']['invoices'], null, 'A1');
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode($this->company->date_format()); // Invoice date column $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('C:C')->getNumberFormat()->setFormatCode($this->currency_format); // Invoice total column
@ -97,13 +98,11 @@ class TaxReport
// All paid invoices within a time period // All paid invoices within a time period
public function createInvoiceSummarySheetCash() public function createInvoiceSummarySheetCash()
{ {
$cash_invoices = collect($this->data['invoices'])->filter(function($invoice){
return $invoice[3] != 0;
})->toArray();
$worksheet = $this->spreadsheet->createSheet(); $worksheet = $this->spreadsheet->createSheet();
$worksheet->setTitle(ctrans('texts.invoice')." ".ctrans('texts.cash_accounting')); $worksheet->setTitle(ctrans('texts.invoice')." ".ctrans('texts.cash_accounting'));
$worksheet->fromArray($cash_invoices, null, 'A1');
$worksheet->fromArray($this->data['cash']['invoices'], null, 'A1');
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode($this->company->date_format()); // Invoice date column $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('C:C')->getNumberFormat()->setFormatCode($this->currency_format); // Invoice total column
$worksheet->getStyle('D:D')->getNumberFormat()->setFormatCode($this->currency_format); // Paid amount column $worksheet->getStyle('D:D')->getNumberFormat()->setFormatCode($this->currency_format); // Paid amount column
@ -118,7 +117,7 @@ class TaxReport
$worksheet = $this->spreadsheet->createSheet(); $worksheet = $this->spreadsheet->createSheet();
$worksheet->setTitle(ctrans('texts.invoice_item')." ".ctrans('texts.cash_vs_accrual')); $worksheet->setTitle(ctrans('texts.invoice_item')." ".ctrans('texts.cash_vs_accrual'));
$worksheet->fromArray($this->data['invoice_items'], null, 'A1'); $worksheet->fromArray($this->data['accrual']['invoice_items'], null, 'A1');
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode($this->company->date_format()); // Invoice date column $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('C:C')->getNumberFormat()->setFormatCode($this->currency_format); // Invoice total column
@ -135,13 +134,9 @@ class TaxReport
public function createInvoiceItemSummarySheetCash() public function createInvoiceItemSummarySheetCash()
{ {
$cash_invoice_items = collect($this->data['invoice_items'])->filter(function($invoice_item){
return $invoice_item[3] != 0;
})->toArray();
$worksheet = $this->spreadsheet->createSheet(); $worksheet = $this->spreadsheet->createSheet();
$worksheet->setTitle(ctrans('texts.invoice_item')." ".ctrans('texts.cash_accounting')); $worksheet->setTitle(ctrans('texts.invoice_item')." ".ctrans('texts.cash_accounting'));
$worksheet->fromArray($cash_invoice_items, null, 'A1'); $worksheet->fromArray($this->data['cash']['invoice_items'], null, 'A1');
$worksheet->getStyle('B:B')->getNumberFormat()->setFormatCode($this->company->date_format()); // Invoice date column $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('C:C')->getNumberFormat()->setFormatCode($this->currency_format); // Invoice total column
@ -162,17 +157,19 @@ class TaxReport
$end_date_instance = Carbon::parse($this->tsr->end_date); $end_date_instance = Carbon::parse($this->tsr->end_date);
$this->data['invoices'] = []; $this->data['invoices'] = [];
$this->data['invoices'][] = [ $this->data['invoices'][] =
$invoice_headers = [
ctrans('texts.invoice_number'), ctrans('texts.invoice_number'),
ctrans('texts.invoice_date'), ctrans('texts.invoice_date'),
ctrans('texts.invoice_total'), ctrans('texts.invoice_total'),
ctrans('texts.paid'), ctrans('texts.paid'),
ctrans('texts.total_taxes'), ctrans('texts.total_taxes'),
ctrans('texts.tax_paid') ctrans('texts.tax_paid'),
ctrans('texts.notes')
]; ];
$this->data['invoice_items'] = []; $invoice_item_headers = [
$this->data['invoice_items'][] = [
ctrans('texts.invoice_number'), ctrans('texts.invoice_number'),
ctrans('texts.invoice_date'), ctrans('texts.invoice_date'),
ctrans('texts.invoice_total'), ctrans('texts.invoice_total'),
@ -185,26 +182,106 @@ class TaxReport
ctrans('texts.tax_nexus'), ctrans('texts.tax_nexus'),
]; ];
$offset = $this->company->timezone_offset();
/** @var Invoice $invoice */ $this->data['accrual']['invoices'] = [$invoice_headers];
foreach($this->query->cursor() as $invoice){ $this->data['cash']['invoices'] = [$invoice_headers];
$this->data['accrual']['invoice_items'] = [$invoice_item_headers];
$this->data['cash']['invoice_items'] = [$invoice_item_headers];
Invoice::withTrashed()
->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){
if($invoice->transaction_events->count() == 0){ if($invoice->transaction_events->count() == 0){
(new InvoiceTransactionEventEntry())->run($invoice); (new InvoiceTransactionEventEntry())->run($invoice);
$invoice->load('transaction_events');
} }
//get the invoice state as at the end of the current period. /** @var TransactionEvent $invoice_state */
$invoice->transaction_events->each(function($event){ $invoice_state = $invoice->transaction_events->where('event_id', TransactionEvent::INVOICE_UPDATED)->sortByDesc('timestamp')->first();
$adjustments = $invoice->transaction_events->whereIn('event_id',[TransactionEvent::PAYMENT_REFUNDED, TransactionEvent::PAYMENT_DELETED]);
if($invoice_state->event_id == TransactionEvent::INVOICE_UPDATED){
$this->data['accrual']['invoices'][] = [
$invoice->number,
$invoice->date,
$invoice->amount,
$invoice_state->invoice_paid_to_date,
$invoice_state->metadata->tax_report->tax_summary->total_taxes,
$invoice_state->metadata->tax_report->tax_summary->total_paid,
'payable',
];
}
elseif($invoice_state->event_id == TransactionEvent::PAYMENT_CASH){
$this->data['cash']['invoices'][] = [
$invoice->number,
$invoice->date,
$invoice->amount,
$invoice_state->invoice_paid_to_date,
$invoice_state->metadata->tax_report->tax_summary->total_taxes,
$invoice_state->metadata->tax_report->tax_summary->total_paid,
'payable',
];
}
$_adjustments = [];
foreach($adjustments as $adjustment){
$_adjustments[] = [
$invoice->number,
$invoice->date,
$invoice->amount,
$invoice_state->invoice_paid_to_date,
$invoice_state->metadata->tax_report->tax_summary->total_taxes,
$invoice_state->metadata->tax_report->tax_summary->adjustment,
'adjustment',
];
}
$this->data['accrual']['invoices'] = array_merge($this->data['accrual']['invoices'], $_adjustments);
$this->data['cash']['invoices'] = array_merge($this->data['cash']['invoices'], $_adjustments);
}); });
//anything period the reporting period is considered an ADJUSTMENT
}
return $this; return $this;
} }
// $offset = $this->company->timezone_offset();
// /** @var Invoice $invoice */
// foreach($this->query->cursor() as $invoice){
// if($invoice->transaction_events->count() == 0){
// (new InvoiceTransactionEventEntry())->run($invoice);
// }
// //get the invoice state as at the end of the current period.
// $invoice_state =$invoice->transaction_events()
// ->where('period', Carbon::parse($invoice->date)->endOfMonth()->format('Y-m-d'))
// ->where('event_id', TransactionEvent::INVOICE_UPDATED)
// ->latest()
// ->first();
// //anything period the reporting period is considered an ADJUSTMENT
// }
// Invoice::withTrashed()
// ->where('company_id', $this->company->id)
// ->whereHas('transaction_events', function ($query){
// return $query->where('period', Carbon::parse($invoice->date)->endOfMonth()->format('Y-m-d'))
// ->whereIn('event_id',[TransactionEvent::PAYMENT_REFUNDED, TransactionEvent::PAYMENT_DELETED]);
// });
// return $this;
// }
public function getXlsFile() public function getXlsFile()
{ {
@ -214,7 +291,7 @@ class TaxReport
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($this->spreadsheet); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($this->spreadsheet);
$writer->save($tempFile); $writer->save($tempFile);
// $writer->save('/home/david/ttx.xslx'); $writer->save('/home/david/ttx.xlsx');
// Read file content // Read file content
$fileContent = file_get_contents($tempFile); $fileContent = file_get_contents($tempFile);

View File

@ -5607,6 +5607,7 @@ $lang = array(
'first_payment_date' => 'First Payment Date', 'first_payment_date' => 'First Payment Date',
'first_payment_date_help' => 'The date of the first payment', 'first_payment_date_help' => 'The date of the first payment',
'payment_schedule_interval' => 'Payment :index of :total for :amount', 'payment_schedule_interval' => 'Payment :index of :total for :amount',
'payment_schedule_table' => 'Payment :key on :date for :amount',
'auto_send' => 'Auto Send', 'auto_send' => 'Auto Send',
'auto_send_help' => 'Automatically emails the invoice to the client', 'auto_send_help' => 'Automatically emails the invoice to the client',
'include_project_tasks' => 'Include Project Tasks', 'include_project_tasks' => 'Include Project Tasks',

View File

@ -11,17 +11,19 @@
namespace Tests\Feature\Export; namespace Tests\Feature\Export;
use App\DataMapper\CompanySettings; use Tests\TestCase;
use App\Factory\InvoiceItemFactory; use App\Models\User;
use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\Account;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\User;
use App\Services\Report\TaxSummaryReport;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\DataMapper\CompanySettings;
use App\Factory\InvoiceItemFactory;
use App\Services\Report\TaxSummaryReport;
use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase; use App\Listeners\Invoice\InvoiceTransactionEventEntry;
use App\Listeners\Invoice\InvoiceTransactionEventEntryAccrual;
/** /**
* *
@ -162,6 +164,34 @@ class TaxSummaryReportTest extends TestCase
$i = $i->calc()->getInvoice(); $i = $i->calc()->getInvoice();
(new InvoiceTransactionEventEntry())->run($i);
$i2 = Invoice::factory()->create([
'client_id' => $this->client->id,
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'amount' => 0,
'balance' => 0,
'status_id' => 2,
'total_taxes' => 1,
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 10,
'tax_rate2' => 17.5,
'tax_rate3' => 5,
'tax_name1' => 'GST',
'tax_name2' => 'VAT',
'tax_name3' => 'CA Sales Tax',
'uses_inclusive_taxes' => false,
'line_items' => $this->buildLineItems(),
]);
$i2 = $i2->calc()->getInvoice();
$i2->service()->markPaid();
(new InvoiceTransactionEventEntryAccrual())->run($i2);
$pl = new TaxSummaryReport($this->company, $this->payload); $pl = new TaxSummaryReport($this->company, $this->payload);
$response = $pl->run(); $response = $pl->run();
@ -206,6 +236,34 @@ class TaxSummaryReportTest extends TestCase
$i = $i->calc()->getInvoice(); $i = $i->calc()->getInvoice();
(new InvoiceTransactionEventEntry())->run($i);
$i2 = Invoice::factory()->create([
'client_id' => $this->client->id,
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'amount' => 0,
'balance' => 0,
'status_id' => 2,
'total_taxes' => 1,
'date' => now()->format('Y-m-d'),
'terms' => 'nada',
'discount' => 0,
'tax_rate1' => 10,
'tax_rate2' => 17.5,
'tax_rate3' => 5,
'tax_name1' => 'GST',
'tax_name2' => 'VAT',
'tax_name3' => 'CA Sales Tax',
'uses_inclusive_taxes' => false,
'line_items' => $this->buildLineItems(),
]);
$i2 = $i2->calc()->getInvoice();
$i2->service()->markPaid();
(new InvoiceTransactionEventEntryAccrual())->run($i2, now()->subDays(30)->format('Y-m-d'), now()->addDays(30)->format('Y-m-d'));
$pl = new TaxSummaryReport($this->company, $this->payload); $pl = new TaxSummaryReport($this->company, $this->payload);
$query = Invoice::query() $query = Invoice::query()