Feature: Tax Regimes are added so that different bills with different series can be printed for Different regimes such as VAT and GST

Chore: Model relationships updated to make them simpler
Chore: Bill printing majorly refactored for it

Due to the sheer depth of the changes. There can be showstoppers. Please test it carefully
This commit is contained in:
2023-03-05 23:50:41 +05:30
parent 802eded568
commit e46fe7f90e
141 changed files with 2197 additions and 892 deletions

View File

@ -2,12 +2,11 @@
<form [formGroup]="form">
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-[15%] mr-5">
<mat-select formControlName="billType">
<mat-option value="0">KOT</mat-option>
<mat-option value="1">Regular Bill</mat-option>
<mat-option value="4">Staff</mat-option>
<mat-option value="2">No Charge</mat-option>
<mat-option value="8">Void</mat-option>
<mat-label>Regime</mat-label>
<mat-select formControlName="regime">
<mat-option *ngFor="let r of regimes" [value]="r">
{{ r.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto">

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import { Component, Inject, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Regime } from 'src/app/core/regime';
@Component({
selector: 'app-bill-number',
@ -8,50 +9,40 @@ import { MatDialogRef } from '@angular/material/dialog';
styleUrls: ['./bill-number.component.css'],
})
export class BillNumberComponent implements OnInit {
form: UntypedFormGroup;
form: FormGroup<{
regime: FormControl<Regime | null>;
billNumber: FormControl<number>;
}>;
constructor(public dialogRef: MatDialogRef<BillNumberComponent>, private fb: UntypedFormBuilder) {
regimes: Regime[] = [];
constructor(
public dialogRef: MatDialogRef<BillNumberComponent>,
@Inject(MAT_DIALOG_DATA)
public data: Regime[],
) {
this.regimes = data;
// Create form
this.form = this.fb.group({
billType: '',
billNumber: '',
this.form = new FormGroup({
regime: new FormControl<Regime | null>(null),
billNumber: new FormControl<number>(0, { nonNullable: true }),
});
}
ngOnInit() {
this.form.setValue({
billType: '1',
billNumber: '',
regime: null,
billNumber: 0,
});
}
accept(): void {
const formValue = this.form.value;
const billNumber = parseInt(formValue.billNumber.replace('-', ''), 10);
if (isNaN(billNumber)) {
const billNumber = formValue.billNumber ?? 0;
const regime = formValue.regime ?? null;
if (regime == null) {
this.dialogRef.close(undefined);
} else {
let billId: string;
switch (formValue.billType) {
case '0': // KOT
billId = 'K-' + billNumber;
break;
case '1': // Regular Bill
billId = Math.floor(billNumber / 10000) + '-' + (billNumber % 10000);
break;
case '4': // Staff
billId = 'ST-' + billNumber;
break;
case '2': // No Charge
billId = 'NC-' + billNumber;
break;
case '8': // Void
billId = 'V-' + billNumber;
break;
default:
throw new Error('Unknown Bill Type');
}
this.dialogRef.close(billId);
}
this.dialogRef.close(`${regime?.prefix}-${billNumber}`);
}
}

View File

@ -46,7 +46,7 @@
><button>Table: {{ bs.bill.table.name }}</button> /
<button mat-button (click)="choosePax()">{{ bs.bill.pax }} Pax</button> /
<button mat-button (click)="chooseCustomer()">
{{ bs.bill.customer?.name || 'Customer' }}
{{ bs.bill.customer?.name ?? 'Customer' }}
</button></mat-header-cell
>
</ng-container>

View File

@ -1,10 +1,5 @@
import { Component, Inject } from '@angular/core';
import {
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
} from '@angular/forms';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { round } from 'mathjs';
@ -20,26 +15,36 @@ import { CustomerService } from '../../customers/customer.service';
styleUrls: ['./choose-customer.component.css'],
})
export class ChooseCustomerComponent {
form: UntypedFormGroup;
form: FormGroup<{
name: FormControl<string>;
phone: FormControl<string>;
address: FormControl<string | null>;
printInBill: FormControl<boolean>;
discounts: FormArray<
FormGroup<{
discount: FormControl<number | null>;
}>
>;
}>;
item: Customer = new Customer();
customers: Observable<Customer[]>;
constructor(
public dialogRef: MatDialogRef<ChooseCustomerComponent>,
@Inject(MAT_DIALOG_DATA) public data: string | undefined,
private fb: UntypedFormBuilder,
private ser: CustomerService,
) {
// Create form
this.form = this.fb.group({
name: '',
phone: '',
address: '',
printInBill: false,
discounts: this.fb.array([]),
this.form = new FormGroup({
name: new FormControl<string>('', { nonNullable: true }),
phone: new FormControl<string>('', { nonNullable: true }),
address: new FormControl<string | null>(null),
printInBill: new FormControl<boolean>(false, { nonNullable: true }),
discounts: new FormArray<FormGroup<{ discount: FormControl<number | null> }>>([]),
});
// Setup Account Autocomplete
this.customers = (this.form.get('phone') as UntypedFormControl).valueChanges.pipe(
this.customers = this.form.controls.phone.valueChanges.pipe(
startWith(null),
map((x) => (x === this.item.phone ? '' : x)),
map((x) => (x !== null && x.length >= 1 ? x : null)),
@ -54,20 +59,16 @@ export class ChooseCustomerComponent {
showItem(item: Customer) {
this.item = item;
this.form.patchValue({
name: item.name,
phone: item.phone,
address: item.address,
printInBill: item.printInBill,
});
this.form.setControl(
'discounts',
this.fb.array(
item.discounts.map((x) =>
this.fb.group({
discount: '' + x.discount * 100,
}),
),
this.form.controls.name.setValue(item.name);
this.form.controls.phone.setValue(item.phone);
this.form.controls.address.setValue(item.address);
this.form.controls.printInBill.setValue(item.printInBill);
this.form.controls.discounts.clear();
this.item.discounts.forEach((x) =>
this.form.controls.discounts.push(
new FormGroup({
discount: new FormControl<number | null>(x.discount ? x.discount * 100 : null),
}),
),
);
this.form.markAsPristine();
@ -93,18 +94,22 @@ export class ChooseCustomerComponent {
getItem(): Customer {
const formModel = this.form.value;
this.item.id = this.item.phone.trim() !== formModel.phone.trim() ? '' : this.item.id;
this.item.name = formModel.name;
this.item.phone = formModel.phone;
this.item.address = formModel.address;
this.item.printInBill = formModel.printInBill;
const array = this.form.get('discounts') as UntypedFormArray;
this.item.discounts.forEach((item, index) => {
item.discount = Math.max(
Math.min(round(array.controls[index].value.discount / 100, 5), 100),
0,
);
});
this.item.id = this.item.phone.trim() !== formModel.phone?.trim() ? '' : this.item.id;
this.item.name = formModel.name ?? '';
this.item.phone = formModel.phone ?? '';
this.item.address = formModel.address ?? '';
this.item.printInBill = formModel.printInBill ?? false;
const array = formModel.discounts;
if (array) {
this.item.discounts.forEach((item, index) => {
const array_item = array.at(index);
if (array_item && array_item?.discount) {
item.discount = Math.max(Math.min(round(array_item.discount / 100, 5), 100), 0);
} else {
item.discount = null;
}
});
}
return this.item;
}
}

View File

@ -1,11 +1,5 @@
import { Component, Inject } from '@angular/core';
import {
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { round } from 'mathjs';
import { Observable } from 'rxjs';
@ -20,52 +14,58 @@ import { DiscountItem } from './discount-item';
})
export class DiscountComponent {
list: DiscountItem[] = [];
form: UntypedFormGroup;
form: FormGroup<{
discounts: FormArray<
FormGroup<{
name: FormControl<string>;
discount: FormControl<number>;
}>
>;
}>;
dataSource: DiscountDataSource = new DiscountDataSource([]);
displayedColumns = ['name', 'discount'];
constructor(
public dialogRef: MatDialogRef<DiscountComponent>,
private fb: UntypedFormBuilder,
@Inject(MAT_DIALOG_DATA)
public data: Observable<DiscountItem[]>,
) {
this.form = this.fb.group({
discounts: '',
this.form = new FormGroup({
discounts: new FormArray<
FormGroup<{
name: FormControl<string>;
discount: FormControl<number>;
}>
>([]),
});
this.data.subscribe((list: DiscountItem[]) => {
this.list = list;
this.form.setControl(
'discounts',
this.fb.array(
this.list.map((x) =>
this.fb.group({
name: [x.name],
discount: [
'' + (x.discount !== 0 ? x.discount * 100 : ''),
[Validators.min(0), Validators.max(x.discountLimit * 100)],
],
this.form.controls.discounts.clear();
this.list.forEach((x) =>
this.form.controls.discounts.push(
new FormGroup({
name: new FormControl(x.name, { nonNullable: true }),
discount: new FormControl(x.discount * 100, {
validators: [Validators.min(0), Validators.max(x.discountLimit * 100)],
nonNullable: true,
}),
),
}),
),
);
this.dataSource = new DiscountDataSource(this.list);
),
(this.dataSource = new DiscountDataSource(this.list));
});
}
accept(): void {
const array = this.form.get('discounts') as UntypedFormArray;
const array = this.form.controls.discounts;
for (let i = this.list.length - 1; i >= 0; i--) {
const item = this.list[i];
const control = (array.controls[i] as UntypedFormGroup).controls[
'discount'
] as UntypedFormControl;
if (
control.value === null ||
control.value === '' ||
(control.pristine && control.value === '0')
) {
const control = array.controls[i].controls.discount;
if (control.pristine && control.value === 0) {
this.list.splice(i, 1);
} else {
item.discount = Math.max(Math.min(round(control.value / 100, 5), item.discountLimit), 0);

View File

@ -1,5 +1,5 @@
import { Component, Inject, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { FormControl, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
@ -8,16 +8,17 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
styleUrls: ['./pax.component.css'],
})
export class PaxComponent implements OnInit {
form: UntypedFormGroup;
form: FormGroup<{
pax: FormControl<number>;
}>;
constructor(
public dialogRef: MatDialogRef<PaxComponent>,
@Inject(MAT_DIALOG_DATA) public data: number,
private fb: UntypedFormBuilder,
) {
// Create form
this.form = this.fb.group({
pax: '',
this.form = new FormGroup({
pax: new FormControl<number>(0, { nonNullable: true }),
});
}
@ -28,7 +29,7 @@ export class PaxComponent implements OnInit {
}
accept(): void {
const pax = +this.form.value.pax;
const pax = this.form.value.pax ?? 0;
this.dialogRef.close(pax);
}
}

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, Inject, ViewChild } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { FormControl, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ReasonDatasource } from './reason-datasource';
@ -11,7 +11,10 @@ import { ReasonDatasource } from './reason-datasource';
})
export class ReasonComponent {
@ViewChild('son', { static: true }) son?: ElementRef;
form: UntypedFormGroup;
form: FormGroup<{
son: FormControl<string>;
}>;
dataSource: ReasonDatasource;
title: string;
selected = '';
@ -21,12 +24,11 @@ export class ReasonComponent {
constructor(
public dialogRef: MatDialogRef<ReasonComponent>,
@Inject(MAT_DIALOG_DATA) private data: { title: string; reasons: string[] },
private fb: UntypedFormBuilder,
) {
this.reasons = data.reasons || [];
this.reasons = data.reasons ?? [];
this.title = data.title;
this.form = this.fb.group({
son: '',
this.form = new FormGroup({
son: new FormControl<string>('', { nonNullable: true }),
});
this.dataSource = new ReasonDatasource(this.reasons);
}

View File

@ -1,10 +1,5 @@
import { Component, ElementRef, Inject, ViewChild } from '@angular/core';
import {
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
} from '@angular/forms';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@ -30,20 +25,33 @@ export class ReceivePaymentComponent {
reason = '';
displayReason: boolean;
displayTable: boolean;
form: UntypedFormGroup;
form: FormGroup<{
amounts: FormArray<
FormGroup<{
name: FormControl<string>;
amount: FormControl<number>;
}>
>;
son: FormControl<string>;
}>;
dataSource: ReceivePaymentDatasource;
displayedColumns = ['name', 'amount'];
constructor(
public dialogRef: MatDialogRef<ReceivePaymentComponent>,
private fb: UntypedFormBuilder,
private ser: SettleOptionService,
@Inject(MAT_DIALOG_DATA) public data: { type: VoucherType; amount: number },
) {
this.form = this.fb.group({
amounts: '',
son: '',
this.form = new FormGroup({
amounts: new FormArray<
FormGroup<{
name: FormControl<string>;
amount: FormControl<number>;
}>
>([]),
son: new FormControl<string>('', { nonNullable: true }),
});
this.type = data.type;
this.amount = data.amount;
@ -55,7 +63,7 @@ export class ReceivePaymentComponent {
.pipe(
tap(
(x: SettleOption[]) =>
(this.displayReason = x.reduce((o, n) => o || n.hasReason, this.displayReason)),
(this.displayReason = x.reduce((o, n) => o ?? n.hasReason, this.displayReason)),
),
tap((x: SettleOption[]) => (this.displayTable = x.length > 1)),
map((x: SettleOption[]) =>
@ -66,15 +74,15 @@ export class ReceivePaymentComponent {
)
.subscribe((x) => {
this.choices = x;
this.form.setControl(
'amounts',
this.fb.array(
this.choices.map((y: ReceivePaymentItem) =>
this.fb.group({
name: [y.name],
amount: [y.amount === 0 ? '' : '' + this.amount],
this.form.controls.amounts.clear();
this.choices.forEach((y: ReceivePaymentItem) =>
this.form.controls.amounts.push(
new FormGroup({
name: new FormControl<string>(y.name, { nonNullable: true }),
amount: new FormControl<number>(y.amount === 0 ? 0 : this.amount, {
nonNullable: true,
}),
),
}),
),
);
this.dataSource = new ReceivePaymentDatasource(this.choices);
@ -86,11 +94,10 @@ export class ReceivePaymentComponent {
}
listenToAmountChange() {
const array = this.form.get('amounts') as UntypedFormArray;
const array = this.form.controls.amounts;
this.choices.forEach((z, i) =>
array.controls[i].valueChanges.pipe(distinctUntilChanged()).subscribe((x) => {
(this.choices.find((s) => s.name === x.name) as ReceivePaymentItem).amount =
x.amount === '' ? 0 : parseInt(x.amount, 10);
(this.choices.find((s) => s.name === x.name) as ReceivePaymentItem).amount = x.amount ?? 0;
this.balance = this.amount - this.choices.reduce((a, c) => a + c.amount, 0);
}),
);
@ -106,8 +113,8 @@ export class ReceivePaymentComponent {
}
maxAmount(row: ReceivePaymentItem, index: number) {
const array = this.form.get('amounts') as UntypedFormArray;
const ctrl = array.controls[index].get('amount') as UntypedFormControl;
ctrl.setValue('' + (row.amount + this.balance));
const array = this.form.controls.amounts;
const ctrl = array.controls[index].controls.amount;
ctrl.setValue(row.amount + this.balance);
}
}

View File

@ -17,7 +17,7 @@
table.guest
}}</mat-card-subtitle>
<span class="center"
>{{ table.pax || '-' }} / {{ table.seats }} / {{ table.section?.name }}</span
>{{ table.pax ?? '-' }} / {{ table.seats }} / {{ table.section?.name }}</span
>
<span class="center" *ngIf="table.date">{{ table.date }}</span>
<span class="center" *ngIf="table.amount">{{ table.amount | currency : 'INR' }}</span>

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { map } from 'rxjs/operators';
import { Regime } from 'src/app/core/regime';
import { Table } from '../../core/table';
import { ToasterService } from '../../core/toaster.service';
@ -13,6 +14,7 @@ import { BillNumberComponent } from '../bill-number/bill-number.component';
styleUrls: ['./running-tables.component.css'],
})
export class RunningTablesComponent implements OnInit {
regimes: Regime[] = [];
list: Table[] = [];
constructor(
@ -24,7 +26,8 @@ export class RunningTablesComponent implements OnInit {
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { list: Table[] };
const data = value as { list: Table[]; regimes: Regime[] };
this.regimes = data.regimes;
this.list = data.list;
});
}
@ -45,7 +48,7 @@ export class RunningTablesComponent implements OnInit {
openBill() {
return this.dialog
.open(BillNumberComponent)
.open(BillNumberComponent, { data: this.regimes })
.afterClosed()
.pipe(
map((x) => {

View File

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from '../auth/auth-guard.service';
import { RegimeListResolver } from '../regimes/regime-list-resolver.service';
import { BillResolver } from './bills/bill-resolver.service';
import { BillsComponent } from './bills/bills.component';
@ -25,6 +26,7 @@ const routes: Routes = [
},
resolve: {
list: RunningTablesResolver,
regimes: RegimeListResolver,
},
},
{
@ -36,6 +38,7 @@ const routes: Routes = [
},
resolve: {
item: RunningTablesResolver,
regimes: RegimeListResolver,
},
},
{

View File

@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core';
import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Observable } from 'rxjs';
@ -10,36 +10,44 @@ import { Observable } from 'rxjs';
})
export class SplitBillComponent {
list: { id: string; name: string; selected: boolean }[] = [];
form: UntypedFormGroup;
form: FormGroup<{
saleCategories: FormArray<
FormGroup<{
saleCategory: FormControl<boolean>;
}>
>;
}>;
constructor(
public dialogRef: MatDialogRef<SplitBillComponent>,
private fb: UntypedFormBuilder,
@Inject(MAT_DIALOG_DATA)
public data: Observable<{ id: string; name: string; selected: boolean }[]>,
) {
this.form = this.fb.group({
saleCategories: this.fb.array([]),
this.form = new FormGroup({
saleCategories: new FormArray<
FormGroup<{
saleCategory: FormControl<boolean>;
}>
>([]),
});
this.data.subscribe((list: { id: string; name: string; selected: boolean }[]) => {
this.list = list;
this.form.setControl(
'saleCategories',
this.fb.array(
this.list.map((x) =>
this.fb.group({
saleCategory: x.selected,
}),
),
this.form.controls.saleCategories.clear();
this.list.forEach((x) =>
this.form.controls.saleCategories.push(
new FormGroup({
saleCategory: new FormControl(x.selected, { nonNullable: true }),
}),
),
);
});
}
accept(): void {
const array = this.form.get('saleCategories') as UntypedFormArray;
const array = this.form.controls.saleCategories;
this.list.forEach((item, index) => {
item.selected = array.controls[index].value.saleCategory;
item.selected = array.controls[index].value.saleCategory ?? false;
});
this.dialogRef.close(this.list);
}

View File

@ -11,7 +11,7 @@
>
<h3 class="item-name">{{ table.name }}</h3>
<mat-card-subtitle class="center">{{ table.guest }}</mat-card-subtitle>
<span class="center">{{ table.pax || 0 }} / {{ table.seats }} Seats</span>
<span class="center">{{ table.pax ?? 0 }} / {{ table.seats }} Seats</span>
<span class="center" *ngIf="table.date">{{ table.date }}</span>
<span class="center" *ngIf="table.amount">{{ table.amount | currency : 'INR' }}</span>
</mat-card>