Feature: Added a Mozimo Daily Register which shows products from Menu Items category

This commit is contained in:
2024-08-19 17:01:04 +05:30
parent 860c7d39ef
commit 18524a5f47
19 changed files with 915 additions and 21 deletions

View File

@ -53,6 +53,7 @@ from .routers.reports import (
daybook,
entries,
ledger,
mozimo_daily_register,
mozimo_product_register,
net_transactions,
non_contract_purchase,
@ -115,6 +116,7 @@ app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["rep
app.include_router(entries.router, prefix="/api/entries", tags=["reports"])
app.include_router(batch_integrity.router, prefix="/api/batch-integrity", tags=["reports"])
app.include_router(non_contract_purchase.router, prefix="/api/non-contract-purchase", tags=["reports"])
app.include_router(mozimo_daily_register.router, prefix="/api/mozimo-daily-register", tags=["mozimo"])
app.include_router(mozimo_product_register.router, prefix="/api/mozimo-product-register", tags=["mozimo"])

View File

@ -0,0 +1,219 @@
import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import desc, func, or_
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
import brewman.schemas.mozimo_daily_register as schemas
from ...core.security import get_current_active_user as get_user
from ...core.session import get_date, set_date
from ...db.session import SessionFuture
from ...models.mozimo_stock_register import MozimoStockRegister
from ...models.product import Product
from ...models.stock_keeping_unit import StockKeepingUnit
from ...schemas.user import UserToken
router = APIRouter()
@router.get("", response_model=schemas.MozimoDailyRegister)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
) -> schemas.MozimoDailyRegister:
return schemas.MozimoDailyRegister(date_=get_date(request.session), body=[])
@router.get("/{date_}", response_model=schemas.MozimoDailyRegister)
def show_data(
date_: str,
request: Request,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
) -> schemas.MozimoDailyRegister:
with SessionFuture() as db:
d = datetime.strptime(date_, "%d-%b-%Y").date()
body = build_report(d, db)
set_date(date_, request.session)
return schemas.MozimoDailyRegister(
date=d,
body=body,
)
def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterItem]:
body = []
products = (
db.execute(
select(StockKeepingUnit)
.join(StockKeepingUnit.product)
.where(Product.product_group_id == uuid.UUID("dad46805-f577-4e5b-8073-9b788e0173fc")) # Menu items
.order_by(Product.name, StockKeepingUnit.units)
)
.scalars()
.all()
)
for sku in products:
ob = opening_balance(sku.id, date_, db)
item = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == sku.id,
MozimoStockRegister.date_ == date_,
)
)
.scalars()
.one_or_none()
)
body.append(
schemas.MozimoDailyRegisterItem(
id_=None if item is None else item.id_,
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.name} ({sku.units})"),
opening=ob,
received=Decimal(0) if item is None else item.received,
sale=Decimal(0) if item is None else item.sale,
nc=Decimal(0) if item is None else item.nc,
display=None if item is None else item.display,
ageing=None if item is None else item.ageing,
last_edit_date=None if item is None else item.last_edit_date,
)
)
return body
def opening_balance(product_id: uuid.UUID, start_date: date, db: Session) -> Decimal:
opening_physical = (
db.execute(
select(MozimoStockRegister)
.order_by(desc(MozimoStockRegister.date_))
.where(
MozimoStockRegister.sku_id == product_id,
MozimoStockRegister.date_ < start_date,
or_(
MozimoStockRegister.display != None, # noqa: E711
MozimoStockRegister.ageing != None, # noqa: E711
),
)
.order_by(desc(MozimoStockRegister.date_))
.limit(1)
)
.scalars()
.one_or_none()
)
physical = ((opening_physical.display or 0) + (opening_physical.ageing or 0)) if opening_physical is not None else 0
query_ = select(func.sum(MozimoStockRegister.received - MozimoStockRegister.sale - MozimoStockRegister.nc)).where(
MozimoStockRegister.sku_id == product_id,
MozimoStockRegister.date_ < start_date,
)
if opening_physical is not None:
query_ = query_.where(MozimoStockRegister.date_ > opening_physical.date_)
calculated = db.execute(query_).scalar() or 0
return physical + calculated
@router.post("/{date_}", response_model=schemas.MozimoDailyRegister)
def save_route(
date_: str,
request: Request,
data: schemas.MozimoDailyRegister,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
) -> schemas.MozimoDailyRegister:
d = datetime.strptime(date_, "%d-%b-%Y").date()
try:
if any(i for i in data.body if i.received < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Received cannot be less than 0.",
)
if any(i for i in data.body if i.sale < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Sale cannot be less than 0.",
)
if any(i for i in data.body if i.nc < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="No Charge cannot be less than 0.",
)
if any(i for i in data.body if i.display is not None and i.display < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity on display can be Blank, but cannot be less than 0.",
)
if any(i for i in data.body if i.ageing is not None and i.ageing < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity in ageing room can be Blank, but cannot be less than 0.",
)
with SessionFuture() as db:
now = datetime.now(UTC).replace(tzinfo=None)
for item in data.body:
ob = opening_balance(item.product.id_, d, db)
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
else:
closing = ob + item.received - item.sale - item.nc
if closing < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Closing Stock cannot be less than 0.",
)
for item in data.body:
is_blank = (
item.received == 0
and item.sale == 0
and item.nc == 0
and item.display is None
and item.ageing is None
)
old = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == item.product.id_,
MozimoStockRegister.date_ == d,
)
)
.scalars()
.one_or_none()
)
if old is not None:
if is_blank:
db.delete(old)
elif (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
or old.display != item.display
or old.ageing != item.ageing
):
old.received = item.received
old.sale = item.sale
old.nc = item.nc
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
elif not is_blank:
entry = MozimoStockRegister(
d, item.received, item.sale, item.nc, item.display, item.ageing, item.product.id_
)
db.add(entry)
body = build_report(d, db)
db.commit()
return schemas.MozimoDailyRegister(
date_=d,
body=body,
)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)

View File

@ -4,7 +4,6 @@ from datetime import UTC, date, datetime
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import desc
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
@ -18,6 +17,7 @@ from ...models.mozimo_stock_register import MozimoStockRegister
from ...models.stock_keeping_unit import StockKeepingUnit
from ...schemas.user import UserToken
from ..attendance import date_range
from .mozimo_daily_register import opening_balance
router = APIRouter()
@ -26,7 +26,7 @@ router = APIRouter()
@router.get("", response_model=schemas.MozimoProductRegister)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["ledger"]),
user: UserToken = Security(get_user, scopes=["product-ledger"]),
) -> schemas.MozimoProductRegister:
return schemas.MozimoProductRegister(
start_date=get_start_date(request.session),
@ -42,7 +42,7 @@ def show_data(
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["ledger"]),
user: UserToken = Security(get_user, scopes=["product-ledger"]),
) -> schemas.MozimoProductRegister:
with SessionFuture() as db:
sku = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == id_)).scalar_one()
@ -98,23 +98,11 @@ def build_report(
return body
def opening_balance(product_id: uuid.UUID, start_date: date, db: Session) -> Decimal:
opening = db.execute(
select(MozimoStockRegister.display + MozimoStockRegister.ageing)
.order_by(desc(MozimoStockRegister.date_))
.where(
MozimoStockRegister.sku_id == product_id,
MozimoStockRegister.date_ < start_date,
)
).scalar()
return Decimal(0) if opening is None else opening
@router.post("", response_model=schemas.MozimoProductRegister)
def save_route(
request: Request,
data: schemas.MozimoProductRegister,
user: UserToken = Security(get_user, scopes=["ledger"]),
user: UserToken = Security(get_user, scopes=["product-ledger"]),
) -> schemas.MozimoProductRegister:
try:
if any(i for i in data.body if i.received < 0):
@ -158,6 +146,13 @@ def save_route(
)
for item in data.body:
is_blank = (
item.received == 0
and item.sale == 0
and item.nc == 0
and item.display is None
and item.ageing is None
)
old = (
db.execute(
select(MozimoStockRegister).where(
@ -169,7 +164,9 @@ def save_route(
.one_or_none()
)
if old is not None:
if (
if is_blank:
db.delete(old)
elif (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
@ -182,7 +179,7 @@ def save_route(
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
else:
elif not is_blank:
entry = MozimoStockRegister(
item.date_, item.received, item.sale, item.nc, item.display, item.ageing, data.product.id_
)

View File

@ -0,0 +1,57 @@
import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from . import Daf, to_camel
from .product import ProductLink
class MozimoDailyRegisterItem(BaseModel):
id_: uuid.UUID | None = None
product: ProductLink
opening: Daf
received: Daf
sale: Daf
nc: Daf
display: Daf | None
ageing: Daf | None
last_edit_date: datetime | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: None | datetime | str) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
class MozimoDailyRegister(BaseModel):
date_: date
body: list[MozimoDailyRegisterItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")

View File

@ -89,6 +89,10 @@ export const routes: Routes = [
path: 'ledger',
loadChildren: () => import('./ledger/ledger.routes').then((mod) => mod.routes),
},
{
path: 'mozimo-daily-register',
loadChildren: () => import('./mozimo-daily-register/mozimo-daily-register.routes').then((mod) => mod.routes),
},
{
path: 'mozimo-product-register',
loadChildren: () => import('./mozimo-product-register/mozimo-product-register.routes').then((mod) => mod.routes),

View File

@ -36,6 +36,7 @@
<a mat-menu-item routerLink="/batch-integrity-report">Batch Integrity</a>
<a mat-menu-item routerLink="/non-contract-purchase">Non Contract Purchases</a>
<a mat-menu-item routerLink="/mozimo-product-register">Mozimo Product Register</a>
<a mat-menu-item routerLink="/mozimo-daily-register">Mozimo Daily Register</a>
</mat-menu>
<button mat-button [matMenuTriggerFor]="productReportMenu">Product Reports</button>

View File

@ -0,0 +1,63 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { merge, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
export class MozimoDailyRegisterDataSource extends DataSource<MozimoDailyRegisterItem> {
public data: MozimoDailyRegisterItem[] = [];
public paginator?: MatPaginator;
constructor(public dataObs: Observable<MozimoDailyRegisterItem[]>) {
super();
}
connect(): Observable<MozimoDailyRegisterItem[]> {
const dataMutations: EventEmitter<PageEvent>[] = [];
const d = this.dataObs.pipe(
tap((x) => {
this.data = x;
}),
);
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
return merge(d, ...dataMutations).pipe(
map(() => this.calculate([...this.data])),
tap(() => {
if (this.paginator) {
this.paginator.length = this.data.length;
}
}),
map((x: MozimoDailyRegisterItem[]) => this.getPagedData(x)),
);
}
disconnect() {}
private calculate(data: MozimoDailyRegisterItem[]): MozimoDailyRegisterItem[] {
if (data.length === 0) {
return data;
}
data.forEach((item) => {
if (item.ageing !== null || item.display !== null) {
item.closing = (item.ageing ?? 0) + (item.display ?? 0);
item.variance = item.opening + item.received - item.sale - item.nc - item.closing;
} else {
item.closing = item.opening + item.received - item.sale - item.nc;
item.variance = 0;
}
});
return data;
}
private getPagedData(data: MozimoDailyRegisterItem[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
}

View File

@ -0,0 +1,30 @@
import { Product } from '../core/product';
export class MozimoDailyRegisterItem {
id: string | null;
product: Product;
opening: number;
received: number;
sale: number;
nc: number;
display: number | null;
ageing: number | null;
variance: number | null;
closing: number;
lastEditDate: string | null;
public constructor(init?: Partial<MozimoDailyRegisterItem>) {
this.id = null;
this.product = new Product();
this.opening = 0;
this.received = 0;
this.sale = 0;
this.nc = 0;
this.display = null;
this.ageing = null;
this.variance = null;
this.closing = 0;
this.lastEditDate = null;
Object.assign(this, init);
}
}

View File

@ -0,0 +1,17 @@
.right {
display: flex;
justify-content: flex-end;
}
.first {
margin-right: 4px;
}
.middle {
margin-left: 4px;
margin-right: 4px;
}
.last {
margin-left: 4px;
}

View File

@ -0,0 +1,136 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>Mozimo Product Register</mat-card-title>
@if (dataSource.data.length) {
<button mat-icon-button (click)="exportCsv()">
<mat-icon>save_alt</mat-icon>
</button>
}
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form" class="flex flex-col">
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-4/5 mr-5">
<mat-label>Date</mat-label>
<input
matInput
#dateElement
[matDatepicker]="date"
(focus)="dateElement.select()"
formControlName="date"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1/5" color="primary" (click)="show()">Show</button>
</div>
<mat-table #table [dataSource]="dataSource" aria-label="Elements" formArrayName="items">
<!-- Product Column -->
<ng-container matColumnDef="product">
<mat-header-cell *matHeaderCellDef class="center first">Product</mat-header-cell>
<mat-cell *matCellDef="let row" class="center first">
<span
matBadge="1"
matBadgeSize="small"
[matBadgeHidden]="!row.lastEditDate"
matTooltip="{{ row.lastEditDate | localTime }}"
[matTooltipDisabled]="!row.lastEditDate"
>{{ row.product.name }}</span
>
</mat-cell>
</ng-container>
<!-- Opening Column -->
<ng-container matColumnDef="opening">
<mat-header-cell *matHeaderCellDef class="right middle">Opening</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.opening | number: '0.2-2' }}</mat-cell>
</ng-container>
<!-- Received Column -->
<ng-container matColumnDef="received">
<mat-header-cell *matHeaderCellDef class="middle">Received</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Received</mat-label>
<input matInput type="number" formControlName="received" (change)="updateReceived($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Sale Column -->
<ng-container matColumnDef="sale">
<mat-header-cell *matHeaderCellDef class="middle">Sale</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Sale</mat-label>
<input matInput type="number" formControlName="sale" (change)="updateSale($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Nc Column -->
<ng-container matColumnDef="nc">
<mat-header-cell *matHeaderCellDef class="middle">Nc</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Nc</mat-label>
<input matInput type="number" formControlName="nc" (change)="updateNc($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Display Column -->
<ng-container matColumnDef="display">
<mat-header-cell *matHeaderCellDef class="middle">Display</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Display</mat-label>
<input matInput type="number" formControlName="display" (change)="updateDisplay($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Ageing Column -->
<ng-container matColumnDef="ageing">
<mat-header-cell *matHeaderCellDef class="middle">Ageing</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Ageing</mat-label>
<input matInput type="number" formControlName="ageing" (change)="updateAgeing($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Variance Column -->
<ng-container matColumnDef="variance">
<mat-header-cell *matHeaderCellDef class="right middle">Variance</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.variance | number: '0.2-2' }}</mat-cell>
</ng-container>
<!-- Closing Column -->
<ng-container matColumnDef="closing">
<mat-header-cell *matHeaderCellDef class="right last">Closing</mat-header-cell>
<mat-cell *matCellDef="let row" class="right last">{{ row.closing | number: '0.2-2' }}</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-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MozimoDailyRegisterComponent } from './mozimo-daily-register.component';
describe('MozimoProductRegisterComponent', () => {
let component: MozimoDailyRegisterComponent;
let fixture: ComponentFixture<MozimoDailyRegisterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MozimoDailyRegisterComponent],
}).compileComponents();
fixture = TestBed.createComponent(MozimoDailyRegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,223 @@
import { DecimalPipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatBadgeModule } from '@angular/material/badge';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToCsvService } from '../shared/to-csv.service';
import { MozimoDailyRegister } from './mozimo-daily-register';
import { MozimoDailyRegisterDataSource } from './mozimo-daily-register-datasource';
import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { BehaviorSubject } from 'rxjs';
import { LocalTimePipe } from '../shared/local-time.pipe';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-mozimo-daily-register',
templateUrl: './mozimo-daily-register.component.html',
styleUrls: ['./mozimo-daily-register.component.css'],
standalone: true,
imports: [
AsyncPipe,
CurrencyPipe,
DecimalPipe,
LocalTimePipe,
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatCardModule,
MatDatepickerModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatPaginatorModule,
MatTableModule,
MatTooltipModule,
ReactiveFormsModule,
],
providers: [LocalTimePipe],
})
export class MozimoDailyRegisterComponent implements OnInit {
@ViewChild('dateElement', { static: false }) dateElement!: ElementRef<HTMLInputElement>;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
info: MozimoDailyRegister = new MozimoDailyRegister();
body = new BehaviorSubject<MozimoDailyRegisterItem[]>([]);
dataSource: MozimoDailyRegisterDataSource = new MozimoDailyRegisterDataSource(this.body);
form: FormGroup<{
date: FormControl<Date>;
items: FormArray<
FormGroup<{
received: FormControl<number>;
sale: FormControl<number>;
nc: FormControl<number>;
display: FormControl<number | null>;
ageing: FormControl<number | null>;
}>
>;
}>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'opening', 'received', 'sale', 'nc', 'display', 'ageing', 'variance', 'closing'];
constructor(
private route: ActivatedRoute,
private router: Router,
private toCsv: ToCsvService,
private localTimePipe: LocalTimePipe,
private snackBar: MatSnackBar,
public auth: AuthService,
private ser: MozimoDailyRegisterService,
) {
this.form = new FormGroup({
date: new FormControl(new Date(), { nonNullable: true }),
items: new FormArray<
FormGroup<{
received: FormControl<number>;
sale: FormControl<number>;
nc: FormControl<number>;
display: FormControl<number | null>;
ageing: FormControl<number | null>;
}>
>([]),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: MozimoDailyRegister };
this.info = data.info;
this.form.patchValue({
date: moment(this.info.date, 'DD-MMM-YYYY').toDate(),
});
this.form.controls.items.clear();
this.info.body.forEach((x) =>
this.form.controls.items.push(
new FormGroup({
received: new FormControl(x.received, { nonNullable: true, validators: [Validators.min(0)] }),
sale: new FormControl(x.sale, { nonNullable: true, validators: [Validators.min(0)] }),
nc: new FormControl(x.nc, { nonNullable: true, validators: [Validators.min(0)] }),
display: new FormControl(x.display, { nonNullable: false, validators: [Validators.min(0)] }),
ageing: new FormControl(x.ageing, { nonNullable: false, validators: [Validators.min(0)] }),
}),
),
);
if (!this.dataSource.paginator) {
this.dataSource.paginator = this.paginator;
}
this.body.next(this.info.body);
});
}
show() {
const date = moment(this.form.value.date).format('DD-MMM-YYYY');
this.router.navigate(['/mozimo-daily-register', date]);
}
save() {
this.ser.save(this.getMozimoDailyRegister()).subscribe({
next: () => {
this.snackBar.open('', 'Success');
},
error: (error) => {
this.snackBar.open(error, 'Danger');
},
});
}
getMozimoDailyRegister(): MozimoDailyRegister {
const formModel = this.form.value;
this.info.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = this.form.controls.items;
this.info.body.forEach((item, index) => {
item.received = +(array.controls[index].value.received ?? 0);
item.sale = +(array.controls[index].value.sale ?? 0);
item.nc = +(array.controls[index].value.nc ?? 0);
const display = array.controls[index].value.display ?? null;
const ageing = array.controls[index].value.ageing ?? null;
item.display = display == null ? null : +display;
item.ageing = ageing == null ? null : +ageing;
});
return this.info;
}
exportCsv() {
const headers = {
Product: 'product',
Opening: 'opening',
Received: 'received',
Sale: 'sale',
Nc: 'nc',
Display: 'display',
Ageing: 'ageing',
Variance: 'variance',
Closing: 'closing',
'Last Edit Date': 'lastEditDate',
};
const d = JSON.parse(JSON.stringify(this.dataSource.data)).map((x: MozimoDailyRegisterItem) => ({
id: x.id,
product: x.product.name,
opening: x.opening,
received: x.received,
sale: x.sale,
nc: x.nc,
display: x.display,
ageing: x.ageing,
variance: x.variance,
closing: x.closing,
lastEditDate: x.lastEditDate ? this.localTimePipe.transform(x.lastEditDate ?? '') : 'Unsaved',
}));
const csvData = new Blob([this.toCsv.toCsv(headers, d)], {
type: 'text/csv;charset=utf-8;',
});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(csvData);
link.setAttribute('download', 'mozimo-product-register.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
updateReceived($event: Event, row: MozimoDailyRegisterItem) {
row.received = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateSale($event: Event, row: MozimoDailyRegisterItem) {
row.sale = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateNc($event: Event, row: MozimoDailyRegisterItem) {
row.nc = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateDisplay($event: Event, row: MozimoDailyRegisterItem) {
const val = ($event.target as HTMLInputElement).value;
row.display = val === '' ? null : +val;
this.body.next(this.info.body);
}
updateAgeing($event: Event, row: MozimoDailyRegisterItem) {
const val = ($event.target as HTMLInputElement).value;
row.ageing = val === '' ? null : +val;
this.body.next(this.info.body);
}
}

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { MozimoDailyRegister } from './mozimo-daily-register';
import { mozimoDailyRegisterResolver } from './mozimo-daily-register.resolver';
describe('mozimoProductRegisterResolver', () => {
const executeResolver: ResolveFn<MozimoDailyRegister> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => mozimoDailyRegisterResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});

View File

@ -0,0 +1,10 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { MozimoDailyRegister } from './mozimo-daily-register';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
export const mozimoDailyRegisterResolver: ResolveFn<MozimoDailyRegister> = (route) => {
const date = route.paramMap.get('date');
return inject(MozimoDailyRegisterService).list(date);
};

View File

@ -0,0 +1,33 @@
import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { MozimoDailyRegisterComponent } from './mozimo-daily-register.component';
import { mozimoDailyRegisterResolver } from './mozimo-daily-register.resolver';
export const routes: Routes = [
{
path: '',
component: MozimoDailyRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Product Ledger',
},
resolve: {
info: mozimoDailyRegisterResolver,
},
runGuardsAndResolvers: 'always',
},
{
path: ':date',
component: MozimoDailyRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Product Ledger',
},
resolve: {
info: mozimoDailyRegisterResolver,
},
runGuardsAndResolvers: 'always',
},
];

View File

@ -0,0 +1,17 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
describe('MozimoProductRegisterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [MozimoDailyRegisterService, provideHttpClient(withInterceptorsFromDi())],
});
});
it('should be created', inject([MozimoDailyRegisterService], (service: MozimoDailyRegisterService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,34 @@
import { HttpClient } 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 { MozimoDailyRegister } from './mozimo-daily-register';
const url = '/api/mozimo-daily-register';
const serviceName = 'MozimoDailyRegisterService';
@Injectable({
providedIn: 'root',
})
export class MozimoDailyRegisterService {
constructor(
private http: HttpClient,
private log: ErrorLoggerService,
) {}
list(date: string | null): Observable<MozimoDailyRegister> {
const listUrl: string = date === null ? url : `${url}/${date}`;
return this.http
.get<MozimoDailyRegister>(listUrl)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<MozimoDailyRegister>;
}
save(mozimoProductRegister: MozimoDailyRegister): Observable<MozimoDailyRegister> {
return this.http
.post<MozimoDailyRegister>(`${url}/${mozimoProductRegister.date}`, mozimoProductRegister)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<MozimoDailyRegister>;
}
}

View File

@ -0,0 +1,12 @@
import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
export class MozimoDailyRegister {
date: string;
body: MozimoDailyRegisterItem[];
public constructor(init?: Partial<MozimoDailyRegister>) {
this.date = '';
this.body = [];
Object.assign(this, init);
}
}

View File

@ -11,7 +11,7 @@ export const routes: Routes = [
component: MozimoProductRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Ledger',
permission: 'Product Ledger',
},
resolve: {
info: mozimoProductRegisterResolver,
@ -23,8 +23,7 @@ export const routes: Routes = [
component: MozimoProductRegisterComponent,
canActivate: [authGuard],
data: {
// permission: 'Mozimo Product Register',
permission: 'Ledger',
permission: 'Product Ledger',
},
resolve: {
info: mozimoProductRegisterResolver,