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\Quote;
use App\Models\Account; use App\Models\Account;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Scheduler;
use App\Jobs\Cron\AutoBill; use App\Jobs\Cron\AutoBill;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
@ -1078,4 +1079,20 @@ class InvoiceController extends BaseController
return $this->itemResponse($invoice->fresh()); 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) { $recurring_invoices->each(function ($recurring_invoice) {
if($recurring_invoice->status_id == RecurringInvoice::STATUS_COMPLETED){ 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->status_id = RecurringInvoice::STATUS_PAUSED;
$recurring_invoice->save(); $recurring_invoice->save();
} }
}); });
} }

View File

@ -36,7 +36,7 @@ class PaymentScheduleRequest extends Request
'frequency_id' => 'sometimes|integer|required_with:remaining_cycles', 'frequency_id' => 'sometimes|integer|required_with:remaining_cycles',
'remaining_cycles' => 'sometimes|integer|required_with:frequency_id', 'remaining_cycles' => 'sometimes|integer|required_with:frequency_id',
'parameters' => 'bail|array', '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.*.id' => 'required|integer',
'parameters.schedule.*.date' => 'required|date:Y-m-d', 'parameters.schedule.*.date' => 'required|date:Y-m-d',
'parameters.schedule.*.amount' => 'required|numeric', 'parameters.schedule.*.amount' => 'required|numeric',
@ -91,23 +91,20 @@ class PaymentScheduleRequest extends Request
} }
if (isset($input['frequency_id']) && isset($input['remaining_cycles'])) { 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']); $input['remaining_cycles'] = count($input['parameters']['schedule']);
$this->replace($input); $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); $amount = round($this->invoice->amount / $remaining_cycles, 2);

View File

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

View File

@ -72,7 +72,7 @@ class UpdateSchedulerRequest extends Request
'parameters.status' => ['bail','sometimes', 'nullable', 'string'], 'parameters.status' => ['bail','sometimes', 'nullable', 'string'],
'parameters.include_project_tasks' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'], '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.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.auto_bill' => ['bail','sometimes', 'boolean', 'required_if:template,payment_schedule'],
'parameters.schedule' => ['bail', 'array', 'required_if:template,payment_schedule','min:1'], 'parameters.schedule' => ['bail', 'array', 'required_if:template,payment_schedule','min:1'],
'parameters.schedule.*.id' => ['bail','sometimes', 'integer'], 'parameters.schedule.*.id' => ['bail','sometimes', 'integer'],
@ -100,6 +100,10 @@ class UpdateSchedulerRequest extends Request
$input['parameters']['clients'] = []; $input['parameters']['clients'] = [];
} }
if(isset($input['parameters']['invoice_id'])) {
unset($input['parameters']['invoice_id']);
}
if (isset($input['parameters']['status'])) { 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) { $this->export_data['credits'] = $this->company->credits()->orderBy('number', 'DESC')->cursor()->map(function ($credit) {
$credit = $this->transformBasicEntities($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']); return $credit->makeVisible(['id']);
})->all(); })->all();
@ -314,7 +314,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['invoices'] = $this->company->invoices()->orderBy('number', 'DESC')->cursor()->map(function ($invoice) { $this->export_data['invoices'] = $this->company->invoices()->orderBy('number', 'DESC')->cursor()->map(function ($invoice) {
$invoice = $this->transformBasicEntities($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 = ''; $invoice->tax_data = '';
return $invoice->makeHidden(['gateway_fee'])->makeVisible(['id', 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) { $this->export_data['quotes'] = $this->company->quotes()->orderBy('number', 'DESC')->cursor()->map(function ($quote) {
$quote = $this->transformBasicEntities($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']); return $quote->makeVisible(['id']);
})->all(); })->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) { $this->export_data['recurring_invoices'] = $this->company->recurring_invoices()->orderBy('number', 'DESC')->cursor()->map(function ($ri) {
$ri = $this->transformBasicEntities($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']); return $ri->makeVisible(['id']);
})->all(); })->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) { $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->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', return $purchase_order->makeVisible(['id',
'private_notes', 'private_notes',
@ -642,6 +642,16 @@ class CompanyExport implements ShouldQueue
$x->addItems($this->export_data['e_invoicing_tokens']); $x->addItems($this->export_data['e_invoicing_tokens']);
$this->export_data = null; $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 //////////////////////////////////// //////////////////////////////////// fine ////////////////////////////////////
$this->writer->end(); $this->writer->end();

View File

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

View File

@ -846,10 +846,11 @@ class Invoice extends BaseModel
return []; return [];
} }
return collect($schedule->parameters['schedule'])->map(function ($item) { return collect($schedule->parameters['schedule'])->map(function ($item) use ($schedule) {
return [ return [
'date' => $item['date'], '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(); })->toArray();
} }

View File

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

View File

@ -5598,6 +5598,15 @@ $lang = array(
'time_zone' => 'Time Zone', 'time_zone' => 'Time Zone',
'tax_names' => 'Tax Names', 'tax_names' => 'Tax Names',
'auto_bill_help' => 'If enabled, when the schedule runs, auto bill will be attempted for the scheduled amount', '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; 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::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action');
Route::put('invoices/{invoice}/upload', [InvoiceController::class, 'upload'])->name('invoices.upload'); 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::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', [InvoiceController::class, 'downloadPdf'])->name('invoices.downloadPdf');
Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoices.downloadEInvoice'); Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoices.downloadEInvoice');
Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk'); Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk');