mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-18 02:34:08 +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',
|
||||
],
|
||||
'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',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
if (v$.value.$invalid) {
|
||||
console.log('Form is invalid:', v$.value.$errors)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user