mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-07 13:41:23 +00:00
Merge pull request #198 from mchev/invoice_cancellation
Support for Zero and Negative Item Quantities on Invoices
This commit is contained in:
committed by
Darko Gjorgjijoski
parent
e1a0a2d8e4
commit
967c225df9
@@ -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',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user