Feature: Added product Stock Keeping Units to prevent duplicate products. A lot of refactoring because of this.

Removed: Reset Stock as it was never used and don't think it is even needed with this new batch system.
Fix: Incentive update was not working
This commit is contained in:
2021-09-27 09:31:58 +05:30
parent 4f907e965b
commit 1647d356c9
71 changed files with 1272 additions and 904 deletions

View File

@ -0,0 +1,16 @@
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs';
import { StockKeepingUnit } from '../../core/product';
export class ProductDetailDatasource extends DataSource<StockKeepingUnit> {
constructor(private data: Observable<StockKeepingUnit[]>) {
super();
}
connect(): Observable<StockKeepingUnit[]> {
return this.data;
}
disconnect() {}
}

View File

@ -0,0 +1,46 @@
<h1 mat-dialog-title>Edit Journal Entry</h1>
<div mat-dialog-content>
<form [formGroup]="form">
<div
fxLayout="row wrap"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>Units</mat-label>
<input matInput placeholder="Units" formControlName="units" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Fraction</mat-label>
<input matInput type="number" placeholder="Fraction" formControlName="fraction" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Fraction Units</mat-label>
<input matInput placeholder="Fraction Units" formControlName="fractionUnits" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Yield</mat-label>
<input matInput type="number" placeholder="Yield" formControlName="productYield" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ data.isPurchased ? 'Purchase Price' : 'Cost Price' }}</mat-label>
<input
matInput
type="number"
placeholder="{{ data.isPurchased ? 'Purchase Price' : 'Cost Price' }}"
formControlName="price"
/>
</mat-form-field>
<mat-form-field fxFlex [hidden]="!data.isSold">
<mat-label>Sale Price</mat-label>
<input matInput type="number" placeholder="Sale Price" formControlName="salePrice" />
</mat-form-field>
</div>
</form>
</div>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="false" cdkFocusInitial>Cancel</button>
<button mat-button (click)="accept()" color="primary">Ok</button>
</div>

View File

@ -0,0 +1,26 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProductDetailDialogComponent } from './product-detail-dialog.component';
describe('ProductDetailDialogComponent', () => {
let component: ProductDetailDialogComponent;
let fixture: ComponentFixture<ProductDetailDialogComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProductDetailDialogComponent],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(ProductDetailDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,68 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { StockKeepingUnit } from '../../core/product';
@Component({
selector: 'app-journal-dialog',
templateUrl: './product-detail-dialog.component.html',
styleUrls: ['./product-detail-dialog.component.css'],
})
export class ProductDetailDialogComponent implements OnInit {
form: FormGroup;
constructor(
public dialogRef: MatDialogRef<ProductDetailDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { item: StockKeepingUnit; isSold: boolean; isPurchased: boolean },
private fb: FormBuilder,
) {
this.form = this.fb.group({
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
salePrice: '',
});
}
ngOnInit() {
this.form.setValue({
units: this.data.item.units,
fraction: '' + this.data.item.fraction,
fractionUnits: this.data.item.fractionUnits,
productYield: '' + this.data.item.productYield,
price: '' + this.data.item.price,
salePrice: '' + this.data.item.salePrice,
});
}
accept(): void {
const formValue = this.form.value;
const fraction = +formValue.fraction;
if (fraction < 1) {
return;
}
const productYield = +formValue.productYield;
if (productYield < 0 || productYield > 1) {
return;
}
const price = +formValue.price;
if (price < 0) {
return;
}
const salePrice = +formValue.salePrice;
if (salePrice < 0) {
return;
}
this.data.item.units = formValue.units;
this.data.item.fraction = fraction;
this.data.item.fractionUnits = formValue.fractionUnits;
this.data.item.productYield = productYield;
this.data.item.price = price;
this.data.item.salePrice = salePrice;
this.dialogRef.close(this.data.item);
}
}

View File

@ -24,55 +24,10 @@
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex="75">
<mat-form-field fxFlex>
<mat-label>Name</mat-label>
<input matInput #nameElement placeholder="Name" formControlName="name" />
</mat-form-field>
<mat-form-field fxFlex="25">
<mat-label>Units</mat-label>
<input matInput placeholder="Units" formControlName="units" />
</mat-form-field>
</div>
<div
fxLayout="row"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>Fraction</mat-label>
<input matInput type="number" placeholder="Fraction" formControlName="fraction" />
</mat-form-field>
<mat-form-field cdk-overlay-origin="">
<mat-label>Fraction Units</mat-label>
<input matInput placeholder="Fraction Units" formControlName="fractionUnits" />
</mat-form-field>
<mat-form-field cdk-overlay-origin="">
<mat-label>Yield</mat-label>
<input matInput type="number" placeholder="Yield" formControlName="productYield" />
</mat-form-field>
</div>
<div
fxLayout="row"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>{{ item.isPurchased ? 'Purchase Price' : 'Cost Price' }}</mat-label>
<input
matInput
type="number"
placeholder="{{ item.isPurchased ? 'Purchase Price' : 'Cost Price' }}"
formControlName="price"
/>
</mat-form-field>
<mat-form-field fxFlex [hidden]="!item.isSold">
<mat-label>Sale Price</mat-label>
<input matInput type="number" placeholder="Sale Price" formControlName="salePrice" />
</mat-form-field>
</div>
<div
fxLayout="row"
@ -101,7 +56,114 @@
</mat-select>
</mat-form-field>
</div>
<h2>Stock Keeping Units</h2>
<div
formGroupName="addRow"
fxLayout="row wrap"
fxLayoutAlign="space-around start"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-label>Units</mat-label>
<input matInput placeholder="Units" formControlName="units" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Fraction</mat-label>
<input matInput type="number" placeholder="Fraction" formControlName="fraction" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Fraction Units</mat-label>
<input matInput placeholder="Fraction Units" formControlName="fractionUnits" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Yield</mat-label>
<input matInput type="number" placeholder="Yield" formControlName="productYield" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ item.isPurchased ? 'Purchase Price' : 'Cost Price' }}</mat-label>
<input
matInput
type="number"
placeholder="{{ item.isPurchased ? 'Purchase Price' : 'Cost Price' }}"
formControlName="price"
/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Sale Price</mat-label>
<input matInput type="number" placeholder="Sale Price" formControlName="salePrice" />
</mat-form-field>
<button mat-raised-button color="primary" (click)="addRow()" fxFlex="15">Add</button>
</div>
</form>
<mat-table [dataSource]="dataSource" aria-label="Elements">
<!-- Checkbox Column -->
<ng-container matColumnDef="isDefault">
<mat-header-cell *matHeaderCellDef>Default</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="changeDefault($event, row)"
[checked]="row.isDefault"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<!-- Units Column -->
<ng-container matColumnDef="units">
<mat-header-cell *matHeaderCellDef>Units</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.units }}</mat-cell>
</ng-container>
<!-- Fraction Column -->
<ng-container matColumnDef="fraction">
<mat-header-cell *matHeaderCellDef>Fraction</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.fraction }}</mat-cell>
</ng-container>
<!-- Fraction Units Column -->
<ng-container matColumnDef="fractionUnits">
<mat-header-cell *matHeaderCellDef>Fraction Units</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.fractionUnits }}</mat-cell>
</ng-container>
<!-- Yield Column -->
<ng-container matColumnDef="yield">
<mat-header-cell *matHeaderCellDef class="right">Yield</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.productYield }}</mat-cell>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<mat-header-cell *matHeaderCellDef class="right">Price</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.price | currency: 'INR' }}</mat-cell>
</ng-container>
<!-- Sale Price Column -->
<ng-container matColumnDef="salePrice">
<mat-header-cell *matHeaderCellDef class="right">Sale Price</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{
row.salePrice | currency: 'INR'
}}</mat-cell>
</ng-container>
<!-- Action Column -->
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>
<mat-cell *matCellDef="let row" class="center">
<button mat-icon-button tabindex="-1" (click)="editRow(row)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button tabindex="-1" color="warn" (click)="deleteRow(row)">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="save()">Save</button>

View File

@ -1,14 +1,19 @@
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { Product } from '../../core/product';
import { Product, StockKeepingUnit } from '../../core/product';
import { ProductGroup } from '../../core/product-group';
import { ToasterService } from '../../core/toaster.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ProductService } from '../product.service';
import { ProductDetailDatasource } from './product-detail-datasource';
import { ProductDetailDialogComponent } from './product-detail-dialog.component';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
@ -18,8 +23,21 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
form: FormGroup;
productGroups: ProductGroup[] = [];
public skus = new BehaviorSubject<StockKeepingUnit[]>([]);
dataSource: ProductDetailDatasource = new ProductDetailDatasource(this.skus);
item: Product = new Product();
displayedColumns = [
'isDefault',
'units',
'fraction',
'fractionUnits',
'yield',
'price',
'salePrice',
'action',
];
constructor(
private route: ActivatedRoute,
private router: Router,
@ -31,12 +49,14 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.form = this.fb.group({
code: { value: '', disabled: true },
name: '',
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
salePrice: '',
addRow: this.fb.group({
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
salePrice: '',
}),
isPurchased: '',
isSold: '',
isActive: '',
@ -51,6 +71,8 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.productGroups = data.productGroups;
this.showItem(data.item);
});
this.dataSource = new ProductDetailDatasource(this.skus);
this.skus.next(this.item.skus);
}
showItem(item: Product) {
@ -58,12 +80,14 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.form.setValue({
code: this.item.code || '(Auto)',
name: this.item.name || '',
units: this.item.units || '',
fraction: this.item.fraction || '',
fractionUnits: this.item.fractionUnits || '',
productYield: this.item.productYield || '',
price: this.item.price || '',
salePrice: this.item.salePrice || '',
addRow: {
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
salePrice: '',
},
isPurchased: this.item.isPurchased,
isSold: this.item.isSold,
isActive: this.item.isActive,
@ -79,6 +103,79 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
}, 0);
}
addRow() {
const formValue = (this.form.get('addRow') as FormControl).value;
const fraction = +formValue.fraction;
if (fraction < 1) {
this.toaster.show('Danger', 'Fraction has to be >= 1');
return;
}
const productYield = +formValue.productYield;
if (productYield < 0 || productYield > 1) {
this.toaster.show('Danger', 'Product Yield has to be > 0 and <= 1');
return;
}
const price = +formValue.price;
if (price < 0) {
this.toaster.show('Danger', 'Price has to be >= 0');
return;
}
const salePrice = +formValue.salePrice;
if (salePrice < 0) {
this.toaster.show('Danger', 'Sale Price has to be >= 0');
return;
}
this.item.skus.push(
new StockKeepingUnit({
units: formValue.units,
fraction,
fractionUnits: formValue.fractionUnits,
productYield,
price,
salePrice,
}),
);
this.skus.next(this.item.skus);
this.resetAddRow();
}
resetAddRow() {
(this.form.get('addRow') as FormControl).reset({
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
salePrice: '',
});
}
editRow(row: StockKeepingUnit) {
const dialogRef = this.dialog.open(ProductDetailDialogComponent, {
width: '750px',
data: {
item: { ...row },
isSold: this.item.isSold,
isPurchased: this.item.isPurchased,
},
});
dialogRef.afterClosed().subscribe((result: boolean | StockKeepingUnit) => {
if (!result) {
return;
}
const j = result as StockKeepingUnit;
Object.assign(row, j);
this.skus.next(this.item.skus);
this.resetAddRow();
});
}
deleteRow(row: StockKeepingUnit) {
this.item.skus.splice(this.item.skus.indexOf(row), 1);
this.skus.next(this.item.skus);
}
save() {
this.ser.saveOrUpdate(this.getItem()).subscribe(
() => {
@ -119,12 +216,6 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
getItem(): Product {
const formModel = this.form.value;
this.item.name = formModel.name;
this.item.units = formModel.units;
this.item.fraction = +formModel.fraction;
this.item.fractionUnits = formModel.fractionUnits;
this.item.productYield = +formModel.productYield;
this.item.price = +formModel.price;
this.item.salePrice = +formModel.salePrice;
this.item.isPurchased = formModel.isPurchased;
this.item.isSold = formModel.isSold;
this.item.isActive = formModel.isActive;
@ -134,4 +225,9 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.item.productGroup.id = formModel.productGroup;
return this.item;
}
changeDefault($event: MatCheckboxChange, row: StockKeepingUnit) {
this.item.skus.forEach((x) => (x.isDefault = false));
row.isDefault = true;
}
}