diff --git a/brewman/brewman/main.py b/brewman/brewman/main.py index ff216e0b..7fa3f32f 100644 --- a/brewman/brewman/main.py +++ b/brewman/brewman/main.py @@ -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"]) diff --git a/brewman/brewman/routers/reports/mozimo_daily_register.py b/brewman/brewman/routers/reports/mozimo_daily_register.py new file mode 100644 index 00000000..a4ac892f --- /dev/null +++ b/brewman/brewman/routers/reports/mozimo_daily_register.py @@ -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), + ) diff --git a/brewman/brewman/routers/reports/mozimo_product_register.py b/brewman/brewman/routers/reports/mozimo_product_register.py index 2b4bf797..df65528f 100644 --- a/brewman/brewman/routers/reports/mozimo_product_register.py +++ b/brewman/brewman/routers/reports/mozimo_product_register.py @@ -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_ ) diff --git a/brewman/brewman/schemas/mozimo_daily_register.py b/brewman/brewman/schemas/mozimo_daily_register.py new file mode 100644 index 00000000..1cff8140 --- /dev/null +++ b/brewman/brewman/schemas/mozimo_daily_register.py @@ -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") diff --git a/overlord/src/app/app.routes.ts b/overlord/src/app/app.routes.ts index e38904bd..fe368081 100644 --- a/overlord/src/app/app.routes.ts +++ b/overlord/src/app/app.routes.ts @@ -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), diff --git a/overlord/src/app/core/nav-bar/nav-bar.component.html b/overlord/src/app/core/nav-bar/nav-bar.component.html index 66510dcb..c85510f9 100644 --- a/overlord/src/app/core/nav-bar/nav-bar.component.html +++ b/overlord/src/app/core/nav-bar/nav-bar.component.html @@ -36,6 +36,7 @@ Batch Integrity Non Contract Purchases Mozimo Product Register + Mozimo Daily Register diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register-datasource.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register-datasource.ts new file mode 100644 index 00000000..ba13ee0a --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register-datasource.ts @@ -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 { + public data: MozimoDailyRegisterItem[] = []; + public paginator?: MatPaginator; + constructor(public dataObs: Observable) { + super(); + } + + connect(): Observable { + const dataMutations: EventEmitter[] = []; + 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); + } +} diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register-item.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register-item.ts new file mode 100644 index 00000000..0b9d84c0 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register-item.ts @@ -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) { + 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); + } +} diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.css b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.css new file mode 100644 index 00000000..bfd7f853 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.css @@ -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; +} diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html new file mode 100644 index 00000000..66d8f550 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html @@ -0,0 +1,136 @@ + + + + Mozimo Product Register + @if (dataSource.data.length) { + + } + + + +
+
+ + Date + + + + + +
+ + + + Product + + {{ row.product.name }} + + + + + + Opening + {{ row.opening | number: '0.2-2' }} + + + + + Received + + + Received + + + + + + + + Sale + + + Sale + + + + + + + + Nc + + + Nc + + + + + + + + Display + + + Display + + + + + + + + Ageing + + + Ageing + + + + + + + + Variance + {{ row.variance | number: '0.2-2' }} + + + + + Closing + {{ row.closing | number: '0.2-2' }} + + + + + + + + +
+
+ + + +
diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.spec.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.spec.ts new file mode 100644 index 00000000..fb6ae558 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MozimoDailyRegisterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MozimoDailyRegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts new file mode 100644 index 00000000..e403dd1c --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts @@ -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; + @ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator; + info: MozimoDailyRegister = new MozimoDailyRegister(); + body = new BehaviorSubject([]); + dataSource: MozimoDailyRegisterDataSource = new MozimoDailyRegisterDataSource(this.body); + + form: FormGroup<{ + date: FormControl; + items: FormArray< + FormGroup<{ + received: FormControl; + sale: FormControl; + nc: FormControl; + display: FormControl; + ageing: FormControl; + }> + >; + }>; + /** 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; + sale: FormControl; + nc: FormControl; + display: FormControl; + ageing: FormControl; + }> + >([]), + }); + } + + 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); + } +} diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.resolver.spec.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.resolver.spec.ts new file mode 100644 index 00000000..0bc1b5c3 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.resolver.spec.ts @@ -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 = (...resolverParameters) => + TestBed.runInInjectionContext(() => mozimoDailyRegisterResolver(...resolverParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeResolver).toBeTruthy(); + }); +}); diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.resolver.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.resolver.ts new file mode 100644 index 00000000..bde661f5 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.resolver.ts @@ -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 = (route) => { + const date = route.paramMap.get('date'); + return inject(MozimoDailyRegisterService).list(date); +}; diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.routes.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.routes.ts new file mode 100644 index 00000000..92ca1cee --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.routes.ts @@ -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', + }, +]; diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.service.spec.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.service.spec.ts new file mode 100644 index 00000000..9e796314 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.service.spec.ts @@ -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(); + })); +}); diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.service.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.service.ts new file mode 100644 index 00000000..5bd22a80 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.service.ts @@ -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 { + const listUrl: string = date === null ? url : `${url}/${date}`; + return this.http + .get(listUrl) + .pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable; + } + + save(mozimoProductRegister: MozimoDailyRegister): Observable { + return this.http + .post(`${url}/${mozimoProductRegister.date}`, mozimoProductRegister) + .pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable; + } +} diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.ts new file mode 100644 index 00000000..5309a947 --- /dev/null +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.ts @@ -0,0 +1,12 @@ +import { MozimoDailyRegisterItem } from './mozimo-daily-register-item'; + +export class MozimoDailyRegister { + date: string; + body: MozimoDailyRegisterItem[]; + + public constructor(init?: Partial) { + this.date = ''; + this.body = []; + Object.assign(this, init); + } +} diff --git a/overlord/src/app/mozimo-product-register/mozimo-product-register.routes.ts b/overlord/src/app/mozimo-product-register/mozimo-product-register.routes.ts index d71dae18..beafe7db 100644 --- a/overlord/src/app/mozimo-product-register/mozimo-product-register.routes.ts +++ b/overlord/src/app/mozimo-product-register/mozimo-product-register.routes.ts @@ -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,