diff --git a/app/Http/Requests/InvoicesRequest.php b/app/Http/Requests/InvoicesRequest.php index f2e65814..3e401638 100644 --- a/app/Http/Requests/InvoicesRequest.php +++ b/app/Http/Requests/InvoicesRequest.php @@ -49,11 +49,10 @@ class InvoicesRequest extends FormRequest 'required', ], 'sub_total' => [ - 'integer', + 'numeric', 'required', ], 'total' => [ - 'integer', 'numeric', 'max:999999999999', 'required', @@ -83,7 +82,7 @@ class InvoicesRequest extends FormRequest 'required', ], 'items.*.price' => [ - 'integer', + 'numeric', 'required', ], ]; diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 1d9657d8..f76dd5b5 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -374,7 +374,7 @@ class Invoice extends Model implements HasMedia return 'customer_cannot_be_changed_after_payment_is_added'; } - if ($request->total < $total_paid_amount) { + if ($request->total >= 0 && $request->total < $total_paid_amount) { return 'total_invoice_amount_must_be_more_than_paid_amount'; } diff --git a/database/migrations/2024_10_09_103306_modify_invoices_to_allow_negative_values.php b/database/migrations/2024_10_09_103306_modify_invoices_to_allow_negative_values.php new file mode 100644 index 00000000..87d5cbf4 --- /dev/null +++ b/database/migrations/2024_10_09_103306_modify_invoices_to_allow_negative_values.php @@ -0,0 +1,65 @@ +bigInteger('discount_val')->nullable()->change(); + $table->bigInteger('sub_total')->change(); + $table->bigInteger('total')->change(); + $table->bigInteger('tax')->change(); + $table->bigInteger('due_amount')->change(); + $table->bigInteger('base_discount_val')->nullable()->change(); + $table->bigInteger('base_sub_total')->nullable()->change(); + $table->bigInteger('base_total')->nullable()->change(); + $table->bigInteger('base_tax')->nullable()->change(); + $table->bigInteger('base_due_amount')->nullable()->change(); + }); + + Schema::table('invoice_items', function (Blueprint $table) { + $table->bigInteger('discount_val')->change(); + $table->bigInteger('tax')->change(); + $table->bigInteger('total')->change(); + $table->bigInteger('base_discount_val')->nullable()->change(); + $table->bigInteger('base_tax')->nullable()->change(); + $table->bigInteger('base_total')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + + Schema::table('invoices', function (Blueprint $table) { + $table->unsignedBigInteger('discount_val')->nullable()->change(); + $table->unsignedBigInteger('sub_total')->change(); + $table->unsignedBigInteger('total')->change(); + $table->unsignedBigInteger('due_amount')->change(); + $table->unsignedBigInteger('base_discount_val')->nullable()->change(); + $table->unsignedBigInteger('base_sub_total')->nullable()->change(); + $table->unsignedBigInteger('base_total')->nullable()->change(); + $table->unsignedBigInteger('base_tax')->nullable()->change(); + $table->unsignedBigInteger('base_due_amount')->nullable()->change(); + }); + + Schema::table('invoice_items', function (Blueprint $table) { + $table->unsignedBigInteger('discount_val')->change(); + $table->unsignedBigInteger('tax')->change(); + $table->unsignedBigInteger('total')->change(); + $table->unsignedBigInteger('base_discount_val')->nullable()->change(); + $table->unsignedBigInteger('base_tax')->nullable()->change(); + $table->unsignedBigInteger('base_total')->nullable()->change(); + }); + } +}; diff --git a/resources/scripts/admin/components/estimate-invoice-common/CreateItemRow.vue b/resources/scripts/admin/components/estimate-invoice-common/CreateItemRow.vue index 705df7f4..80d6dbf5 100644 --- a/resources/scripts/admin/components/estimate-invoice-common/CreateItemRow.vue +++ b/resources/scripts/admin/components/estimate-invoice-common/CreateItemRow.vue @@ -42,7 +42,6 @@ :content-loading="loading" type="number" small - min="0" step="any" @change="syncItemToStore()" @input="v$.quantity.$touch()" @@ -325,10 +324,6 @@ const rules = { }, quantity: { required: helpers.withMessage(t('validation.required'), required), - minValue: helpers.withMessage( - t('validation.qty_must_greater_than_zero'), - minValue(0) - ), maxLength: helpers.withMessage( t('validation.amount_maxlength'), maxLength(20) @@ -336,10 +331,6 @@ const rules = { }, price: { required: helpers.withMessage(t('validation.required'), required), - minValue: helpers.withMessage( - t('validation.number_length_minvalue'), - minValue(1) - ), maxLength: helpers.withMessage( t('validation.price_maxlength'), maxLength(20) @@ -350,7 +341,7 @@ const rules = { t('validation.discount_maxlength'), between( 0, - computed(() => subtotal.value) + computed(() => Math.abs(subtotal.value)) ) ), }, @@ -403,11 +394,12 @@ function updateTax(data) { function setDiscount() { const newValue = props.store[props.storeProp].items[props.index].discount + const absoluteSubtotal = Math.abs(subtotal.value) if (props.itemData.discount_type === 'percentage'){ - updateItemAttribute('discount_val', Math.round((subtotal.value * newValue) / 100)) - }else{ - updateItemAttribute('discount_val', Math.round(newValue * 100)) + updateItemAttribute('discount_val', Math.round((absoluteSubtotal * newValue) / 100)) + } else { + updateItemAttribute('discount_val', Math.min(Math.round(newValue * 100), absoluteSubtotal)) } } diff --git a/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue b/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue index 57a7e1c6..6bf9d212 100644 --- a/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue +++ b/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue @@ -254,6 +254,7 @@ async function submitForm() { v$.value.$touch() if (v$.value.$invalid) { + console.log('Form is invalid:', v$.value.$errors) return false } diff --git a/tests/Feature/Admin/EstimateTest.php b/tests/Feature/Admin/EstimateTest.php index 9d1d504a..4eb535a5 100644 --- a/tests/Feature/Admin/EstimateTest.php +++ b/tests/Feature/Admin/EstimateTest.php @@ -231,13 +231,20 @@ test('estimate mark as rejected', function () { }); test('create invoice from estimate', function () { - $estimate = Estimate::factory()->create([ - 'estimate_date' => '1988-07-18', - 'expiry_date' => '1988-08-18', - ]); - $response = postJson("api/v1/estimates/{$estimate->id}/convert-to-invoice") - ->assertStatus(200); + $estimate = Estimate::factory() + ->create([ + 'estimate_date' => now(), + 'expiry_date' => now()->addMonth(), + ]); + + $response = postJson("api/v1/estimates/{$estimate->id}/convert-to-invoice"); + + if ($response->status() !== 200) { + $this->fail('Response status is not 200. Response body: '.json_encode($response->json())); + } + + $response->assertStatus(200); }); test('delete multiple estimates using a form request', function () { diff --git a/tests/Feature/Admin/InvoiceTest.php b/tests/Feature/Admin/InvoiceTest.php index 3adcdf36..bd27652c 100644 --- a/tests/Feature/Admin/InvoiceTest.php +++ b/tests/Feature/Admin/InvoiceTest.php @@ -61,6 +61,67 @@ test('create invoice', function () { ]); }); +test('create invoice with negative and zero item quantities', function () { + $invoice = Invoice::factory()->raw([ + 'items' => [ + InvoiceItem::factory()->raw([ + 'quantity' => -2, + 'price' => 100, + ]), + InvoiceItem::factory()->raw([ + 'quantity' => 1, + 'price' => 50, + ]), + InvoiceItem::factory()->raw([ + 'quantity' => 0, + 'price' => 75, + ]), + ], + 'sub_total' => -150, + 'total' => -150, + ]); + + $response = postJson('api/v1/invoices', $invoice); + + $response->assertOk(); + + $this->assertDatabaseHas('invoices', [ + 'total' => -150, + 'sub_total' => -150, + ]); + + $this->assertDatabaseHas('invoice_items', [ + 'quantity' => -2, + 'total' => -200, + ]); + + $this->assertDatabaseHas('invoice_items', [ + 'quantity' => 1, + 'total' => 50, + ]); + + $this->assertDatabaseHas('invoice_items', [ + 'quantity' => 0, + 'total' => 0, + ]); + + $createdInvoice = Invoice::where('total', -150)->first(); + $this->assertNotNull($createdInvoice); + $this->assertEquals(3, $createdInvoice->items()->count()); + + $negativeItem = $createdInvoice->items()->where('quantity', -2)->first(); + $this->assertNotNull($negativeItem); + $this->assertEquals(-200, $negativeItem->total); + + $positiveItem = $createdInvoice->items()->where('quantity', 1)->first(); + $this->assertNotNull($positiveItem); + $this->assertEquals(50, $positiveItem->total); + + $zeroItem = $createdInvoice->items()->where('quantity', 0)->first(); + $this->assertNotNull($zeroItem); + $this->assertEquals(0, $zeroItem->total); +}); + test('create invoice as sent', function () { $invoice = Invoice::factory() ->raw([