Chore: Changed the account_type and voucher_type enum.

The account type enum is not stored in the database as an enum.
The voucher_type enum is now a table in the database.

Feature: Closing stock can now be saved and in each department.
This commit is contained in:
2021-10-31 18:41:06 +05:30
parent f8de1cd3cf
commit 0574f9df14
71 changed files with 1382 additions and 497 deletions

View File

@ -65,9 +65,7 @@ export class ClosingStockDataSource extends DataSource<ClosingStockItem> {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'product':
return compare(a.product, b.product, isAsc);
case 'group':
return compare(a.group, b.group, isAsc);
return compare(`${a.group} - ${a.product}`, `${b.group} - ${b.product}`, isAsc);
case 'quantity':
return compare(+a.quantity, +b.quantity, isAsc);
case 'amount':

View File

@ -1,14 +1,24 @@
import { CostCentre } from '../core/cost-centre';
import { Product } from '../core/product';
export class ClosingStockItem {
product: string;
id: string | null;
product: Product;
group: string;
quantity: number;
amount: number;
physical: number;
variance: number;
costCentre?: CostCentre;
public constructor(init?: Partial<ClosingStockItem>) {
this.product = '';
this.id = null;
this.product = new Product();
this.group = '';
this.quantity = 0;
this.amount = 0;
this.variance = 0;
this.physical = 0;
Object.assign(this, init);
}
}

View File

@ -13,6 +13,7 @@ export class ClosingStockResolver implements Resolve<ClosingStock> {
resolve(route: ActivatedRouteSnapshot): Observable<ClosingStock> {
const date = route.paramMap.get('date');
return this.ser.list(date);
const costCentre = route.queryParamMap.get('d') || null;
return this.ser.list(date, costCentre);
}
}

View File

@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '../auth/auth-guard.service';
import { CostCentreListResolver } from '../cost-centre/cost-centre-list-resolver.service';
import { ClosingStockResolver } from './closing-stock-resolver.service';
import { ClosingStockComponent } from './closing-stock.component';
@ -17,7 +18,9 @@ const closingStockRoutes: Routes = [
},
resolve: {
info: ClosingStockResolver,
costCentres: CostCentreListResolver,
},
runGuardsAndResolvers: 'always',
},
{
path: ':date',
@ -28,7 +31,9 @@ const closingStockRoutes: Routes = [
},
resolve: {
info: ClosingStockResolver,
costCentres: CostCentreListResolver,
},
runGuardsAndResolvers: 'always',
},
];

View File

@ -2,3 +2,16 @@
display: flex;
justify-content: flex-end;
}
.first {
margin-right: 4px;
}
.middle {
margin-left: 4px;
margin-right: 4px;
}
.last {
margin-left: 4px;
}

View File

@ -14,7 +14,15 @@
fxLayoutGap.lt-md="0px"
fxLayoutAlign="space-around start"
>
<mat-form-field fxFlex>
<mat-form-field fxFlex="60">
<mat-label>Department</mat-label>
<mat-select formControlName="costCentre" name="costCentre">
<mat-option *ngFor="let at of costCentres" [value]="at.id">
{{ at.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="40">
<input
matInput
[matDatepicker]="dateInput"
@ -28,45 +36,123 @@
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()">Show</button>
</div>
<mat-table
#table
[dataSource]="dataSource"
matSort
aria-label="Elements"
formArrayName="stocks"
>
<!-- Product Column -->
<ng-container matColumnDef="product" class="first">
<mat-header-cell *matHeaderCellDef mat-sort-header class="first">Product</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.product.name }}</mat-cell>
</ng-container>
<!-- Group Column -->
<ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef class="middle">Group</mat-header-cell>
<mat-cell *matCellDef="let row" class="middle">{{ row.group }}</mat-cell>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right middle"
>Closing Stock</mat-header-cell
>
<mat-cell *matCellDef="let row" class="right middle">{{
row.quantity | number: '0.2-2'
}}</mat-cell>
</ng-container>
<!-- Physical Column -->
<ng-container matColumnDef="physical">
<mat-header-cell *matHeaderCellDef class="middle">Physical Stock</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field fxFlex="100%">
<mat-label>Physical</mat-label>
<input
matInput
type="number"
placeholder="Physical"
formControlName="physical"
(change)="updatePhysical($event, row)"
/>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Variance Column -->
<ng-container matColumnDef="variance" class="middle">
<mat-header-cell *matHeaderCellDef class="right">Variance</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{
row.quantity - row.physical | number: '0.2-2'
}}</mat-cell>
</ng-container>
<!-- Department Column -->
<ng-container matColumnDef="department">
<mat-header-cell *matHeaderCellDef class="middle">Department</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field fxFlex="100%">
<mat-label>Department</mat-label>
<mat-select
formControlName="costCentre"
name="costCentre"
(selectionChange)="updateDepartment($event.value, row)"
>
<mat-option *ngFor="let at of costCentres" [value]="at.id">
{{ at.name }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount" class="last">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right last"
>Amount</mat-header-cell
>
<mat-cell *matCellDef="let row" class="right">{{
row.amount | currency: 'INR'
}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
>
</mat-paginator>
</form>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
<!-- Product Column -->
<ng-container matColumnDef="product">
<mat-header-cell *matHeaderCellDef mat-sort-header>Product</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.product }}</mat-cell>
</ng-container>
<!-- Group Column -->
<ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.group }}</mat-cell>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{
row.quantity | number: '0.2-2'
}}</mat-cell>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
>
</mat-paginator>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">
Save
</button>
<button
mat-raised-button
(click)="post()"
*ngIf="canDelete()"
[disabled]="info.posted || !auth.allowed('post-vouchers')"
>
{{ info.posted ? 'Posted' : 'Post' }}
</button>
<button
mat-raised-button
color="warn"
(click)="confirmDelete()"
*ngIf="canDelete()"
[disabled]="!canSave()"
>
Delete
</button>
</mat-card-actions>
</mat-card>

View File

@ -1,14 +1,23 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatSort } from '@angular/material/sort';
import { ActivatedRoute, Router } from '@angular/router';
import * as moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { CostCentre } from '../core/cost-centre';
import { ToasterService } from '../core/toaster.service';
import { User } from '../core/user';
import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component';
import { ToCsvService } from '../shared/to-csv.service';
import { ClosingStock } from './closing-stock';
import { ClosingStockDataSource } from './closing-stock-datasource';
import { ClosingStockItem } from './closing-stock-item';
import { ClosingStockService } from './closing-stock.service';
@Component({
selector: 'app-closing-stock',
@ -19,37 +28,96 @@ export class ClosingStockComponent implements OnInit {
@ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator;
@ViewChild(MatSort, { static: true }) sort?: MatSort;
info: ClosingStock = new ClosingStock();
dataSource: ClosingStockDataSource = new ClosingStockDataSource(this.info.body);
dataSource: ClosingStockDataSource = new ClosingStockDataSource(this.info.items);
form: FormGroup;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'group', 'quantity', 'amount'];
displayedColumns = [
'product',
'group',
'quantity',
'physical',
'variance',
'department',
'amount',
];
costCentres: CostCentre[];
constructor(
private route: ActivatedRoute,
private router: Router,
private fb: FormBuilder,
private toCsv: ToCsvService,
private dialog: MatDialog,
private toaster: ToasterService,
public auth: AuthService,
private ser: ClosingStockService,
) {
this.costCentres = [];
this.form = this.fb.group({
date: '',
costCentre: '',
stocks: this.fb.array([]),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: ClosingStock };
const data = value as { info: ClosingStock; costCentres: CostCentre[] };
this.info = data.info;
this.form.setValue({
this.costCentres = data.costCentres;
this.form.patchValue({
date: moment(this.info.date, 'DD-MMM-YYYY').toDate(),
costCentre: this.info.costCentre.id,
});
this.form.setControl(
'stocks',
this.fb.array(
this.info.items.map((x) =>
this.fb.group({
physical: '' + x.physical,
costCentre: x.costCentre?.id,
}),
),
),
);
this.dataSource = new ClosingStockDataSource(this.info.items, this.paginator, this.sort);
});
this.dataSource = new ClosingStockDataSource(this.info.body, this.paginator, this.sort);
}
show() {
const info = this.getInfo();
this.router.navigate(['closing-stock', info.date]);
this.router.navigate(['closing-stock', info.date], {
queryParams: {
d: info.costCentre.id,
},
});
}
save() {
this.ser.save(this.getClosingStock()).subscribe(
() => {
this.toaster.show('Success', '');
},
(error) => {
this.toaster.show('Danger', error);
},
);
}
getClosingStock(): ClosingStock {
const formModel = this.form.value;
this.info.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = this.form.get('stocks') as FormArray;
this.info.items.forEach((item, index) => {
item.physical = +array.controls[index].value.physical;
item.costCentre =
array.controls[index].value.costCentre == null
? undefined
: new CostCentre({ id: array.controls[index].value.costCentre });
});
console.log('getClosingStock', this.info);
return this.info;
}
getInfo(): ClosingStock {
@ -57,6 +125,7 @@ export class ClosingStockComponent implements OnInit {
return new ClosingStock({
date: moment(formModel.date).format('DD-MMM-YYYY'),
costCentre: new CostCentre({ id: formModel.costCentre }),
});
}
@ -78,4 +147,69 @@ export class ClosingStockComponent implements OnInit {
link.click();
document.body.removeChild(link);
}
updatePhysical($event: Event, row: ClosingStockItem) {
row.physical = +($event.target as HTMLInputElement).value;
}
updateDepartment($event: MatSelectChange, row: ClosingStockItem) {
row.costCentre = new CostCentre({ id: $event.value });
}
canDelete() {
return this.info.items.find((x) => !!x.id) !== undefined;
}
canSave() {
if (this.info.items.find((x) => !!x.id) !== undefined) {
return true;
}
if (this.info.posted && this.auth.allowed('edit-posted-vouchers')) {
return true;
}
return (
this.info.user.id === (this.auth.user as User).id ||
this.auth.allowed("edit-other-user's-vouchers")
);
}
post() {
this.ser.post(this.info.date, this.info.costCentre.id as string).subscribe(
(result) => {
// this.loadVoucher(result);
this.toaster.show('Success', 'Voucher Posted');
},
(error) => {
this.toaster.show('Danger', error);
},
);
}
delete() {
this.ser.delete(this.info.date, this.info.costCentre.id as string).subscribe(
() => {
this.toaster.show('Success', '');
this.router.navigate(['/closing-stock'], { replaceUrl: true });
},
(error) => {
this.toaster.show('Danger', error);
},
);
}
confirmDelete(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '250px',
data: {
title: 'Delete Closing Stock information?',
content: 'Are you sure? This cannot be undone.',
},
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.delete();
}
});
}
}

View File

@ -19,6 +19,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
@ -59,6 +60,7 @@ export const MY_FORMATS = {
ReactiveFormsModule,
SharedModule,
ClosingStockRoutingModule,
MatSelectModule,
],
declarations: [ClosingStockComponent],
providers: [

View File

@ -1,9 +1,10 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { Voucher } from '../core/voucher';
import { ClosingStock } from './closing-stock';
@ -16,10 +17,38 @@ const serviceName = 'ClosingStockService';
export class ClosingStockService {
constructor(private http: HttpClient, private log: ErrorLoggerService) {}
list(date: string | null): Observable<ClosingStock> {
list(date: string | null, costCentre: string | null): Observable<ClosingStock> {
const listUrl = date === null ? url : `${url}/${date}`;
const options = { params: new HttpParams() };
if (costCentre !== null) {
options.params = options.params.set('d', costCentre);
}
return this.http
.get<ClosingStock>(listUrl)
.get<ClosingStock>(listUrl, options)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<ClosingStock>;
}
save(closingStock: ClosingStock): Observable<ClosingStock> {
return this.http
.post<ClosingStock>(url, closingStock)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<ClosingStock>;
}
post(date: string, costCentre: string): Observable<ClosingStock> {
const options = { params: new HttpParams().set('d', costCentre) };
return this.http
.post<ClosingStock>(`${url}/${date}`, {}, options)
.pipe(
catchError(this.log.handleError(serviceName, 'Post Voucher')),
) as Observable<ClosingStock>;
}
delete(date: string, costCentre: string): Observable<ClosingStock> {
const options = { params: new HttpParams().set('d', costCentre) };
return this.http
.delete<ClosingStock>(`${url}/${date}`, options)
.pipe(
catchError(this.log.handleError(serviceName, 'Delete Voucher')),
) as Observable<ClosingStock>;
}
}

View File

@ -1,12 +1,27 @@
import { CostCentre } from '../core/cost-centre';
import { User } from '../core/user';
import { ClosingStockItem } from './closing-stock-item';
export class ClosingStock {
date: string;
body: ClosingStockItem[];
costCentre: CostCentre;
items: ClosingStockItem[];
creationDate: string;
lastEditDate: string;
user: User;
posted: boolean;
poster: User;
public constructor(init?: Partial<ClosingStock>) {
this.date = '';
this.body = [];
this.costCentre = new CostCentre();
this.items = [];
this.creationDate = '';
this.lastEditDate = '';
this.user = new User();
this.posted = false;
this.poster = new User();
Object.assign(this, init);
}
}

View File

@ -27,8 +27,7 @@ export class Product {
name: string;
skus: StockKeepingUnit[];
price: number | undefined;
tax: number | undefined;
discount: number | undefined;
fractionUnits: string | undefined;
isActive: boolean;
isFixture: boolean;

View File

@ -71,7 +71,7 @@ export class VoucherService {
}
saveOrUpdate(voucher: Voucher): Observable<Voucher> {
const endpoint = voucher.type.replace(/ /g, '-').toLowerCase();
const endpoint = voucher.type.replace(/_/g, '-').toLowerCase();
if (!voucher.id) {
return this.save(voucher, endpoint);
}

View File

@ -71,7 +71,7 @@ export class ProductLedgerComponent implements OnInit, AfterViewInit {
debounceTime(150),
distinctUntilChanged(),
switchMap((x) =>
x === null ? observableOf([]) : this.productSer.autocomplete(x, false, false),
x === null ? observableOf([]) : this.productSer.autocomplete(x, null, false, false),
),
);
}

View File

@ -10,7 +10,7 @@ import { ProductService } from './product.service';
providedIn: 'root',
})
export class ProductResolver implements Resolve<Product> {
constructor(private ser: ProductService, private router: Router) {}
constructor(private ser: ProductService) {}
resolve(route: ActivatedRouteSnapshot): Observable<Product> {
const id = route.paramMap.get('id');

View File

@ -53,6 +53,7 @@ export class ProductService {
autocomplete(
query: string,
isPurchased: boolean | null,
extended: boolean = false,
skus: boolean = true,
date?: string,
@ -61,6 +62,9 @@ export class ProductService {
const options = {
params: new HttpParams().set('q', query).set('e', extended.toString()).set('s', skus),
};
if (isPurchased !== null) {
options.params = options.params.set('p', isPurchased.toString());
}
if (!!vendorId && !!date) {
options.params = options.params.set('v', vendorId as string).set('d', date as string);
}

View File

@ -42,7 +42,7 @@ export class PurchaseDialogComponent implements OnInit {
map((x) => (x !== null && x.length >= 1 ? x : null)),
debounceTime(150),
distinctUntilChanged(),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x))),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x, true))),
);
}

View File

@ -100,6 +100,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
? observableOf([])
: this.productSer.autocomplete(
x,
true,
false,
true,
moment(this.form.value.date).format('DD-MMM-YYYY'),

View File

@ -77,7 +77,7 @@ export class RateContractDetailComponent implements OnInit, AfterViewInit {
map((x) => (x !== null && x.length >= 1 ? x : null)),
debounceTime(150),
distinctUntilChanged(),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x))),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x, true))),
);
}