Merge pull request #198 from mchev/invoice_cancellation

Support for Zero and Negative Item Quantities on Invoices
This commit is contained in:
mchev
2024-11-02 11:24:09 +01:00
committed by Darko Gjorgjijoski
parent e1a0a2d8e4
commit 967c225df9
7 changed files with 148 additions and 23 deletions

View File

@@ -49,11 +49,10 @@ class InvoicesRequest extends FormRequest
'required', 'required',
], ],
'sub_total' => [ 'sub_total' => [
'integer', 'numeric',
'required', 'required',
], ],
'total' => [ 'total' => [
'integer',
'numeric', 'numeric',
'max:999999999999', 'max:999999999999',
'required', 'required',
@@ -83,7 +82,7 @@ class InvoicesRequest extends FormRequest
'required', 'required',
], ],
'items.*.price' => [ 'items.*.price' => [
'integer', 'numeric',
'required', 'required',
], ],
]; ];

View File

@@ -374,7 +374,7 @@ class Invoice extends Model implements HasMedia
return 'customer_cannot_be_changed_after_payment_is_added'; 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'; return 'total_invoice_amount_must_be_more_than_paid_amount';
} }

View File

@@ -0,0 +1,65 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->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();
});
}
};

View File

@@ -42,7 +42,6 @@
:content-loading="loading" :content-loading="loading"
type="number" type="number"
small small
min="0"
step="any" step="any"
@change="syncItemToStore()" @change="syncItemToStore()"
@input="v$.quantity.$touch()" @input="v$.quantity.$touch()"
@@ -325,10 +324,6 @@ const rules = {
}, },
quantity: { quantity: {
required: helpers.withMessage(t('validation.required'), required), required: helpers.withMessage(t('validation.required'), required),
minValue: helpers.withMessage(
t('validation.qty_must_greater_than_zero'),
minValue(0)
),
maxLength: helpers.withMessage( maxLength: helpers.withMessage(
t('validation.amount_maxlength'), t('validation.amount_maxlength'),
maxLength(20) maxLength(20)
@@ -336,10 +331,6 @@ const rules = {
}, },
price: { price: {
required: helpers.withMessage(t('validation.required'), required), required: helpers.withMessage(t('validation.required'), required),
minValue: helpers.withMessage(
t('validation.number_length_minvalue'),
minValue(1)
),
maxLength: helpers.withMessage( maxLength: helpers.withMessage(
t('validation.price_maxlength'), t('validation.price_maxlength'),
maxLength(20) maxLength(20)
@@ -350,7 +341,7 @@ const rules = {
t('validation.discount_maxlength'), t('validation.discount_maxlength'),
between( between(
0, 0,
computed(() => subtotal.value) computed(() => Math.abs(subtotal.value))
) )
), ),
}, },
@@ -403,11 +394,12 @@ function updateTax(data) {
function setDiscount() { function setDiscount() {
const newValue = props.store[props.storeProp].items[props.index].discount const newValue = props.store[props.storeProp].items[props.index].discount
const absoluteSubtotal = Math.abs(subtotal.value)
if (props.itemData.discount_type === 'percentage'){ if (props.itemData.discount_type === 'percentage'){
updateItemAttribute('discount_val', Math.round((subtotal.value * newValue) / 100)) updateItemAttribute('discount_val', Math.round((absoluteSubtotal * newValue) / 100))
}else{ } else {
updateItemAttribute('discount_val', Math.round(newValue * 100)) updateItemAttribute('discount_val', Math.min(Math.round(newValue * 100), absoluteSubtotal))
} }
} }

View File

@@ -254,6 +254,7 @@ async function submitForm() {
v$.value.$touch() v$.value.$touch()
if (v$.value.$invalid) { if (v$.value.$invalid) {
console.log('Form is invalid:', v$.value.$errors)
return false return false
} }

View File

@@ -231,13 +231,20 @@ test('estimate mark as rejected', function () {
}); });
test('create invoice from estimate', 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") $estimate = Estimate::factory()
->assertStatus(200); ->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 () { test('delete multiple estimates using a form request', function () {

View File

@@ -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 () { test('create invoice as sent', function () {
$invoice = Invoice::factory() $invoice = Invoice::factory()
->raw([ ->raw([