Updated translations for payment schedules and invoice outstanding tasks

This commit is contained in:
David Bomba 2025-08-04 06:41:59 +10:00
parent e50ef1d373
commit 0d9651f95c
6 changed files with 163 additions and 28 deletions

View File

@ -95,10 +95,11 @@ class PaymentScheduleRequest extends Request
$input['parameters']['schedule'] = $this->generateSchedule($input['frequency_id'], $input['remaining_cycles'], Carbon::parse($due_date)); $input['parameters']['schedule'] = $this->generateSchedule($input['frequency_id'], $input['remaining_cycles'], Carbon::parse($due_date));
} }
// $input['next_run'] = $input['parameters']['schedule'][0]['date'];
$input['remaining_cycles'] = count($input['parameters']['schedule']); $input['remaining_cycles'] = count($input['parameters']['schedule']);
$input['next_run_client'] = $input['next_run'];
$input['next_run'] = Carbon::parse($input['next_run'])->addSeconds($this->invoice->company->timezone_offset())->format('Y-m-d');
$this->replace($input); $this->replace($input);
} }
@ -107,10 +108,10 @@ class PaymentScheduleRequest extends Request
$amount = round($this->invoice->amount / $remaining_cycles, 2); $amount = round($this->invoice->amount / $remaining_cycles, 2);
$delta = round($amount * $remaining_cycles, 2); $delta = round($amount * $remaining_cycles, 2);
$adjustment = 0; $adjustment = 0;
if(floatval($delta) != floatval($this->invoice->amount)) { if(floatval($delta) != floatval($this->invoice->amount)) {
$adjustment = round(floatval($this->invoice->amount) - floatval($delta), 2); //adjustment to make the total amount equal to the invoice amount $adjustment = round(floatval($this->invoice->amount) - floatval($delta), 2); //adjustment to make the total amount equal to the invoice amount
} }
@ -126,7 +127,7 @@ class PaymentScheduleRequest extends Request
]; ];
} }
if($adjustment > 0) { if($adjustment != 0) {
$schedule[$remaining_cycles-1]['amount'] += $adjustment; $schedule[$remaining_cycles-1]['amount'] += $adjustment;
} }

View File

@ -834,7 +834,7 @@ class Invoice extends BaseModel
return $reminder_schedule; return $reminder_schedule;
} }
public function paymentSchedule(): array public function paymentSchedule(bool $formatted = false): mixed
{ {
$schedule = \App\Models\Scheduler::where('company_id', $this->company_id) $schedule = \App\Models\Scheduler::where('company_id', $this->company_id)
@ -843,15 +843,63 @@ class Invoice extends BaseModel
->first(); ->first();
if (! $schedule) { if (! $schedule) {
return [];
if($formatted){
return '';
}
else{
return [];
}
} }
return collect($schedule->parameters['schedule'])->map(function ($item) use ($schedule) { if(!$formatted){
return [ return collect($schedule->parameters['schedule'])->map(function ($item) use ($schedule) {
'date' => $item['date'], return [
'amount' => $item['is_amount'] ? \App\Utils\Number::formatMoney($item['amount'], $this->client) : $item['amount'] ." %", 'date' => $this->formatDate($item['date'], $this->client->date_format()),
'auto_bill' => $schedule->parameters['auto_bill'], 'amount' => $item['is_amount'] ? \App\Utils\Number::formatMoney($item['amount'], $this->client) : $item['amount'] ." %",
]; 'auto_bill' => $schedule->parameters['auto_bill'],
})->toArray(); ];
})->toArray();
}
$formatted_string = "<div id=\"payment-schedule\">";
foreach($schedule->parameters['schedule'] as $item){
$amount = $item['is_amount'] ? $item['amount'] : round($this->amount * ($item['amount']/100),2);
$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>";
}
$formatted_string .= "</div>";
return htmlspecialchars($formatted_string, ENT_QUOTES, 'UTF-8');
}
public function paymentScheduleInterval(): string
{
$schedule = \App\Models\Scheduler::where('company_id', $this->company_id)
->where('template', 'payment_schedule')
->where('parameters->invoice_id', $this->hashed_id)
->first();
if(!$schedule)
return '';
$schedule_array = $schedule->parameters['schedule'] ?? [];
$index = 0;
foreach($schedule_array as $key => $item){
if($date = Carbon::parse($item['date'])->eq(Carbon::parse($schedule->next_run_client))){
$index = $key;
}
}
$amount = $schedule_array[$index]['is_amount'] ? \App\Utils\Number::formatMoney($schedule_array[$index]['amount'], $this->client) : \App\Utils\Number::formatMoney(($schedule_array[$index]['amount']/100)*$this->amount, $this->client);
return ctrans('texts.payment_schedule_interval', ['index' => $index+1, 'total' => count($schedule_array), 'amount' => $amount]);
} }
} }

View File

@ -54,15 +54,8 @@ class PaymentSchedule
return; return;
} }
nlog($next_schedule);
if($next_schedule['is_amount']){ $amount = $next_schedule['is_amount'] ? $next_schedule['amount'] : round(($next_schedule['amount']/100)*$invoice->amount, 2);
nlog("is an amount");
$amount = $next_schedule['amount'];
}
else{
$amount = round(($next_schedule['amount']/100)*$invoice->amount, 2);
}
$amount = min($amount, $invoice->amount); $amount = min($amount, $invoice->amount);
@ -70,17 +63,12 @@ class PaymentSchedule
$amount = $invoice->balance; $amount = $invoice->balance;
} }
nlog("amount to add: {$amount}");
nlog("invoice partial before: {$invoice->partial}");
$invoice->partial += $amount; $invoice->partial += $amount;
$invoice->partial_due_date = $next_schedule['date']; $invoice->partial_due_date = $next_schedule['date'];
$invoice->due_date = Carbon::parse($next_schedule['date'])->addDay()->format('Y-m-d'); $invoice->due_date = Carbon::parse($next_schedule['date'])->addDay()->format('Y-m-d');
$invoice->save(); $invoice->save();
nlog("invoice partial after: {$invoice->partial}");
if($this->scheduler->parameters['auto_bill']){ if($this->scheduler->parameters['auto_bill']){
try{ try{

View File

@ -190,7 +190,14 @@ class HtmlEngine
$data['$tax_info'] = ['value' => $this->taxLabel(), 'label' => '']; $data['$tax_info'] = ['value' => $this->taxLabel(), 'label' => ''];
$data['$net'] = ['value' => '', 'label' => ctrans('texts.net')]; $data['$net'] = ['value' => '', 'label' => ctrans('texts.net')];
$data['$payment_schedule'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
$data['$payment_schedule_interval'] = ['value' => '', 'label' => ctrans('texts.payment_schedule')];
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
$data['$payment_schedule'] = ['value' => $this->entity->paymentSchedule(true), 'label' => ctrans('texts.payment_schedule')];
$data['$payment_schedule_interval'] = ['value' => $this->entity->paymentScheduleInterval(), 'label' => ctrans('texts.payment_schedule')];
$data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')]; $data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')];
$data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')];
$data['$invoice'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')]; $data['$invoice'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')];

View File

@ -5606,7 +5606,11 @@ $lang = array(
'schedule_frequency_help' => 'The interval time between each payment', 'schedule_frequency_help' => 'The interval time between each payment',
'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',
'auto_send' => 'Auto Send',
'auto_send_help' => 'Automatically emails the invoice to the client',
'include_project_tasks' => 'Include Project Tasks',
'include_project_tasks_help' => 'Also invoice tasks that are part of a project',
); );
return $lang; return $lang;

View File

@ -62,6 +62,93 @@ class SchedulerTest extends TestCase
} }
public function testPaymentScheduleCalculationsIsPercentageWithAutoBill()
{
$settings = $this->company->settings;
$settings->use_credits_payment = 'off';
$settings->use_unapplied_payment = 'off';
$this->company->settings = $settings;
$this->company->save();
\App\Models\Credit::where('client_id', $this->client->id)->delete();
$invoice = Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'date' => now()->format('Y-m-d'),
'due_date' => now()->addDays(30)->format('Y-m-d'),
'partial' => 0,
'partial_due_date' => null,
'amount' => 300.00,
'balance' => 300.00,
'status_id' => Invoice::STATUS_SENT,
]);
$data = [
'name' => 'A test payment schedule scheduler',
'frequency_id' => 0,
'next_run' => now()->format('Y-m-d'),
'template' => 'payment_schedule',
'parameters' => [
'invoice_id' => $invoice->hashed_id,
'auto_bill' => true,
'schedule' => [
[
'id' => 1,
'date' => now()->format('Y-m-d'),
'amount' => 10,
'is_amount' => false,
],
[
'id' => 2,
'date' => now()->addDays(30)->format('Y-m-d'),
'amount' => 90,
'is_amount' => false,
]
],
],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/task_schedulers', $data);
$response->assertStatus(200);
$arr = $response->json();
$scheduler = Scheduler::find($this->decodePrimaryKey($arr['data']['id']));
$this->assertNotNull($scheduler);
$scheduler->service()->runTask();
$invoice = $invoice->fresh();
$this->assertEquals(30, $invoice->partial);
$this->assertEquals(now()->format('Y-m-d'), $invoice->partial_due_date->format('Y-m-d'));
$scheduler = $scheduler->fresh();
$this->assertEquals(now()->addDays(30)->format('Y-m-d'), $scheduler->next_run->format('Y-m-d'));
$this->travelTo(now()->addDays(30));
$scheduler->service()->runTask();
$invoice = $invoice->fresh();
$this->assertEquals(300, $invoice->partial);
$this->assertEquals(now()->format('Y-m-d'), $invoice->partial_due_date->format('Y-m-d'));
$this->travelBack();
}
public function testPaymentScheduleCalculationsIsAmountWithAutoBill() public function testPaymentScheduleCalculationsIsAmountWithAutoBill()
{ {
$settings = $this->company->settings; $settings = $this->company->settings;