Updated next_sendd_date for recurring ivnoices that are bulk updated

This commit is contained in:
David Bomba 2025-08-03 12:08:48 +10:00
parent f24c8cafff
commit 799991f520
11 changed files with 98 additions and 30 deletions

View File

@ -16,6 +16,7 @@ use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Account;
use App\Models\Invoice;
use App\Models\Scheduler;
use App\Jobs\Cron\AutoBill;
use Illuminate\Http\Response;
use App\Factory\InvoiceFactory;
@ -1078,4 +1079,20 @@ class InvoiceController extends BaseController
return $this->itemResponse($invoice->fresh());
}
public function deletePaymentSchedule(Invoice $invoice)
{
$repo = new SchedulerRepository();
$scheduler = Scheduler::where('company_id', $invoice->company_id)
->where('template', 'payment_schedule')
->where('parameters->invoice_id', $invoice->hashed_id)
->first();
if($scheduler) {
$scheduler->forceDelete();
}
return $this->itemResponse($invoice->fresh());
}
}

View File

@ -452,8 +452,14 @@ class RecurringInvoiceController extends BaseController
$recurring_invoices->each(function ($recurring_invoice) {
if($recurring_invoice->status_id == RecurringInvoice::STATUS_COMPLETED){
$recurring_invoice->next_send_date = $recurring_invoice->last_sent_date;
$recurring_invoice->next_send_date_client = $recurring_invoice->last_sent_date;
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
$recurring_invoice->status_id = RecurringInvoice::STATUS_PAUSED;
$recurring_invoice->save();
}
});
}

View File

@ -36,7 +36,7 @@ class PaymentScheduleRequest extends Request
'frequency_id' => 'sometimes|integer|required_with:remaining_cycles',
'remaining_cycles' => 'sometimes|integer|required_with:frequency_id',
'parameters' => 'bail|array',
'parameters.schedule' => 'sometimes|array|required_without:frequency_id,remaining_cycles',
'parameters.schedule' => 'array|required_without:frequency_id,remaining_cycles',
'parameters.schedule.*.id' => 'required|integer',
'parameters.schedule.*.date' => 'required|date:Y-m-d',
'parameters.schedule.*.amount' => 'required|numeric',
@ -91,23 +91,20 @@ class PaymentScheduleRequest extends Request
}
if (isset($input['frequency_id']) && isset($input['remaining_cycles'])) {
$input['parameters']['schedule'] = $this->generateSchedule($input['frequency_id'], $input['remaining_cycles']);
$due_date = $input['next_run'] ?? $this->invoice->due_date ?? Carbon::parse($this->invoice->date)->addDays((int)$this->invoice->client->getSetting('payment_terms'));
$input['parameters']['schedule'] = $this->generateSchedule($input['frequency_id'], $input['remaining_cycles'], Carbon::parse($due_date));
}
$input['next_run'] = $input['parameters']['schedule'][0]['date'];
// $input['next_run'] = $input['parameters']['schedule'][0]['date'];
$input['remaining_cycles'] = count($input['parameters']['schedule']);
$this->replace($input);
}
private function generateSchedule($frequency_id, $remaining_cycles)
private function generateSchedule(int $frequency_id, int $remaining_cycles, Carbon $due_date)
{
if(!$this->invoice->due_date) {
$due_date = Carbon::parse($this->invoice->date)->addDays((int)$this->invoice->client->getSetting('payment_terms'));
} else {
$due_date = Carbon::parse($this->invoice->due_date);
}
$amount = round($this->invoice->amount / $remaining_cycles, 2);

View File

@ -128,8 +128,7 @@ class StoreSchedulerRequest extends Request
{
return [
'parameters.schedule.min' => 'The schedule must have at least one item.',
'parameters.schedule' => 'You must have at least one schedule entry.',
'parameters.invoice_id' => 'You must select an invoice.'
'parameters.schedule' => 'You must have at least one schedule entry.'
];
}
}

View File

@ -72,7 +72,7 @@ class UpdateSchedulerRequest extends Request
'parameters.status' => ['bail','sometimes', 'nullable', 'string'],
'parameters.include_project_tasks' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'],
'parameters.auto_send' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'],
'parameters.invoice_id' => ['bail','sometimes', 'string', 'required_if:template,payment_schedule'],
// 'parameters.invoice_id' => ['bail','sometimes', 'string', 'required_if:template,payment_schedule'],
'parameters.auto_bill' => ['bail','sometimes', 'boolean', 'required_if:template,payment_schedule'],
'parameters.schedule' => ['bail', 'array', 'required_if:template,payment_schedule','min:1'],
'parameters.schedule.*.id' => ['bail','sometimes', 'integer'],
@ -100,6 +100,10 @@ class UpdateSchedulerRequest extends Request
$input['parameters']['clients'] = [];
}
if(isset($input['parameters']['invoice_id'])) {
unset($input['parameters']['invoice_id']);
}
if (isset($input['parameters']['status'])) {

View File

@ -235,7 +235,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['credits'] = $this->company->credits()->orderBy('number', 'DESC')->cursor()->map(function ($credit) {
$credit = $this->transformBasicEntities($credit);
$credit = $this->transformArrayOfKeys($credit, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','invoice_id']);
$credit = $this->transformArrayOfKeys($credit, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','invoice_id', 'location_id']);
return $credit->makeVisible(['id']);
})->all();
@ -314,7 +314,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['invoices'] = $this->company->invoices()->orderBy('number', 'DESC')->cursor()->map(function ($invoice) {
$invoice = $this->transformBasicEntities($invoice);
$invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
$invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id', 'location_id']);
$invoice->tax_data = '';
return $invoice->makeHidden(['gateway_fee'])->makeVisible(['id',
@ -397,7 +397,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['quotes'] = $this->company->quotes()->orderBy('number', 'DESC')->cursor()->map(function ($quote) {
$quote = $this->transformBasicEntities($quote);
$quote = $this->transformArrayOfKeys($quote, ['invoice_id','recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
$quote = $this->transformArrayOfKeys($quote, ['invoice_id','recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id', 'location_id']);
return $quote->makeVisible(['id']);
})->all();
@ -436,7 +436,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['recurring_invoices'] = $this->company->recurring_invoices()->orderBy('number', 'DESC')->cursor()->map(function ($ri) {
$ri = $this->transformBasicEntities($ri);
$ri = $this->transformArrayOfKeys($ri, ['client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
$ri = $this->transformArrayOfKeys($ri, ['client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id', 'location_id']);
return $ri->makeVisible(['id']);
})->all();
@ -572,7 +572,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['purchase_orders'] = $this->company->purchase_orders()->orderBy('number', 'DESC')->cursor()->map(function ($purchase_order) {
$purchase_order = $this->transformBasicEntities($purchase_order);
$purchase_order = $this->transformArrayOfKeys($purchase_order, ['expense_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','project_id']);
$purchase_order = $this->transformArrayOfKeys($purchase_order, ['expense_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','project_id', 'location_id']);
return $purchase_order->makeVisible(['id',
'private_notes',
@ -642,6 +642,16 @@ class CompanyExport implements ShouldQueue
$x->addItems($this->export_data['e_invoicing_tokens']);
$this->export_data = null;
$this->export_data['locations'] = $this->company->locations()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($location) {
$location = $this->transformArrayOfKeys($location, ['company_id', 'user_id', 'client_id', 'vendor_id']);
return $location->makeVisible(['id','user_id','company_id']);
})->all();
$x = $this->writer->collection('locations');
$x->addItems($this->export_data['locations']);
$this->export_data = null;
//////////////////////////////////// fine ////////////////////////////////////
$this->writer->end();

View File

@ -33,6 +33,7 @@ use App\Models\Webhook;
use App\Utils\TempFile;
use App\Models\Activity;
use App\Models\Document;
use App\Models\Location;
use App\Libraries\MultiDB;
use App\Models\TaskStatus;
use App\Models\CompanyUser;
@ -137,6 +138,7 @@ class CompanyImport implements ShouldQueue
'client_contacts',
'vendors',
'vendor_contacts',
'locations',
'projects',
'products',
'company_gateways',
@ -302,6 +304,7 @@ class CompanyImport implements ShouldQueue
{
set_time_limit(0);
try {
$json = JsonMachine::fromFile($this->file_path, '/'.$key, new ExtJsonDecoder());
if ($force_array) {
@ -309,6 +312,10 @@ class CompanyImport implements ShouldQueue
}
return $json;
} catch (\Throwable $th) {
nlog("Key '{$key}' does not exist in JSON file: " . $th->getMessage());
return [];
}
}
public function handle()
@ -908,6 +915,19 @@ class CompanyImport implements ShouldQueue
return $this;
}
private function import_locations()
{
$this->genericImport(
Location::class,
['user_id', 'company_id', 'id', 'hashed_id', 'client_id', 'vendor_id'],
[['users' => 'user_id'], ['clients' => 'client_id'], ['vendors' => 'vendor_id']],
'locations',
'name'
);
return $this;
}
private function import_projects()
{
$this->genericImport(
@ -995,7 +1015,7 @@ class CompanyImport implements ShouldQueue
['clients' => 'client_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['clients' => 'client_id'],
['locations' => 'location_id'],
],
'recurring_invoices',
'number'
@ -1026,7 +1046,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(
Invoice::class,
['user_id', 'client_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status', 'sync'],
['user_id', 'client_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status', 'sync', 'location_id'],
[
['users' => 'user_id'],
['users' => 'assigned_user_id'],
@ -1035,6 +1055,7 @@ class CompanyImport implements ShouldQueue
['subscriptions' => 'subscription_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['locations' => 'location_id'],
],
'invoices',
'number'
@ -1064,13 +1085,14 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(
PurchaseOrder::class,
['user_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status', 'vendor_id', 'subscription_id','client_id'],
['user_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status', 'vendor_id', 'subscription_id','client_id', 'location_id'],
[
['users' => 'user_id'],
['users' => 'assigned_user_id'],
['recurring_invoices' => 'recurring_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['locations' => 'location_id'],
],
'purchase_orders',
'number'
@ -1101,7 +1123,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(
Quote::class,
['user_id', 'client_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status'],
['user_id', 'client_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status', 'location_id'],
[
['users' => 'user_id'],
['users' => 'assigned_user_id'],
@ -1110,6 +1132,7 @@ class CompanyImport implements ShouldQueue
['subscriptions' => 'subscription_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['locations' => 'location_id'],
],
'quotes',
'number'
@ -1140,7 +1163,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(
Credit::class,
['user_id', 'client_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status'],
['user_id', 'client_id', 'company_id', 'id', 'hashed_id', 'recurring_id','status', 'location_id'],
[
['users' => 'user_id'],
['users' => 'assigned_user_id'],
@ -1149,6 +1172,7 @@ class CompanyImport implements ShouldQueue
['subscriptions' => 'subscription_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['locations' => 'location_id'],
],
'credits',
'number'

View File

@ -846,10 +846,11 @@ class Invoice extends BaseModel
return [];
}
return collect($schedule->parameters['schedule'])->map(function ($item) {
return collect($schedule->parameters['schedule'])->map(function ($item) use ($schedule) {
return [
'date' => $item['date'],
'amount' => $item['is_amount'] ? \App\Utils\Number::formatMoney($item['amount'], $this->client) : $this->amount ." %",
'amount' => $item['is_amount'] ? \App\Utils\Number::formatMoney($item['amount'], $this->client) : $item['amount'] ." %",
'auto_bill' => $schedule->parameters['auto_bill'],
];
})->toArray();
}

View File

@ -17,7 +17,7 @@ use Illuminate\Support\Facades\App;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\Client
* App\Models\Location
*
* @property int $id
* @property int $company_id

View File

@ -5598,6 +5598,15 @@ $lang = array(
'time_zone' => 'Time Zone',
'tax_names' => 'Tax Names',
'auto_bill_help' => 'If enabled, when the schedule runs, auto bill will be attempted for the scheduled amount',
'choose_schedule_type' => 'Choose Schedule Type',
'split_payments' => 'Split Payments',
'split_payments_help' => 'Splits the invoice amount into multiple payments over a period of time. ie 4 payments over 4 months',
'custom_schedule' => 'Manually create a custom payment schedule',
'custom_schedule_help' => 'Create a custom payment schedule, allows creating exact dates and amounts for each schedule',
'schedule_frequency_help' => 'The interval time between each payment',
'first_payment_date' => 'First Payment Date',
'first_payment_date_help' => 'The date of the first payment',
);
return $lang;

View File

@ -277,6 +277,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action');
Route::put('invoices/{invoice}/upload', [InvoiceController::class, 'upload'])->name('invoices.upload');
Route::post('invoices/{invoice}/payment_schedule', [InvoiceController::class, 'paymentSchedule'])->name('invoices.payment_schedule');
Route::delete('invoices/{invoice}/payment_schedule', [InvoiceController::class, 'deletePaymentSchedule'])->name('invoices.delete_payment_schedule');
Route::get('invoice/{invitation_key}/download', [InvoiceController::class, 'downloadPdf'])->name('invoices.downloadPdf');
Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoices.downloadEInvoice');
Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk');