diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 70df6d9fc6..0eeb6e22a6 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -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()); + } } diff --git a/app/Http/Controllers/RecurringInvoiceController.php b/app/Http/Controllers/RecurringInvoiceController.php index fc5e488493..09d8065e45 100644 --- a/app/Http/Controllers/RecurringInvoiceController.php +++ b/app/Http/Controllers/RecurringInvoiceController.php @@ -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(); + } }); } diff --git a/app/Http/Requests/TaskScheduler/PaymentScheduleRequest.php b/app/Http/Requests/TaskScheduler/PaymentScheduleRequest.php index 66a6aacb90..092b366818 100644 --- a/app/Http/Requests/TaskScheduler/PaymentScheduleRequest.php +++ b/app/Http/Requests/TaskScheduler/PaymentScheduleRequest.php @@ -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); diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index ac9dd2ef75..e925632a08 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -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.' ]; } } diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index b22d2abfdd..a4b35d7ccf 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -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'])) { diff --git a/app/Jobs/Company/CompanyExport.php b/app/Jobs/Company/CompanyExport.php index 3999c7d506..4c97bbb7c9 100644 --- a/app/Jobs/Company/CompanyExport.php +++ b/app/Jobs/Company/CompanyExport.php @@ -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(); diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 1571a3814d..352d326430 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -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,13 +304,18 @@ class CompanyImport implements ShouldQueue { 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) { - return iterator_to_array($json); + if ($force_array) { + 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() @@ -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' diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 75c2311477..a5959808b0 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -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(); } diff --git a/app/Models/Location.php b/app/Models/Location.php index 6ec77c329e..3fcecd675c 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -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 diff --git a/lang/en/texts.php b/lang/en/texts.php index 337f1cebdb..cc79165496 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -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; diff --git a/routes/api.php b/routes/api.php index f883defc14..a3910c5944 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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');