diff --git a/barker/routes.py b/barker/routes.py index c241364..77bf382 100644 --- a/barker/routes.py +++ b/barker/routes.py @@ -362,6 +362,7 @@ def includeme(config): config.add_route("v1_sa_tax", "/v1/sale-analysis/tax") config.add_route("v1_sale_analysis", "/v1/sale-analysis") + config.add_route("v1_product_sale_report", "/v1/product-sale-report") # Done till here config.add_route("customer", "/Customer.json") diff --git a/barker/views/reports/product_sale_report.py b/barker/views/reports/product_sale_report.py new file mode 100644 index 0000000..e1f3158 --- /dev/null +++ b/barker/views/reports/product_sale_report.py @@ -0,0 +1,88 @@ +from datetime import datetime, timedelta +from pyramid.view import view_config +from sqlalchemy import func + +from barker.models import ( + Inventory, + Kot, + Product, + Voucher, + SaleCategory, + VoucherType, + MenuCategory, +) +from barker.models.validation_exception import ValidationError +from barker.views.reports import get_start_date, get_finish_date + + +@view_config( + request_method="GET", + route_name="v1_product_sale_report", + renderer="json", + permission="Sales Detail", +) +def product_sale_report_view(request): + start_date = get_start_date(request.GET.get("s", None)) + finish_date = get_finish_date(request.GET.get("f", None)) + + if ( + datetime.today() - start_date.replace(hour=0) + ).days > 5 and "Accounts Audit" not in request.effective_principals: + raise ValidationError("Accounts Audit") + + return { + "startDate": start_date.date().strftime("%d-%b-%Y"), + "finishDate": (finish_date - timedelta(days=1)).date().strftime("%d-%b-%Y"), + "amounts": product_sale_report(start_date, finish_date, request.dbsession), + } + + +def product_sale_report(start_date, finish_date, dbsession): + list_ = ( + dbsession.query( + Product.id, + Product.full_name, + Voucher.voucher_type, + Inventory.is_happy_hour, + func.sum(Inventory.quantity), + ) + .join(Inventory.kot) + .join(Kot.voucher) + .join(Inventory.product) + .join(Product.sale_category) + .join(Product.menu_category) + .filter(Voucher.date >= start_date, Voucher.date <= finish_date) + .group_by( + SaleCategory.name, + MenuCategory.name, + Product.id, + Product.full_name, + Voucher.voucher_type, + Inventory.is_happy_hour, + ) + .order_by(SaleCategory.name, MenuCategory.name) + .all() + ) + info = [] + for id_, name, type_, hh, quantity in list_: + type_ = to_camel_case(VoucherType(type_).name) + old = [i for i in info if i["id"] == id_ and i["isHappyHour"] == hh] + if len(old): + old[0][type_] = quantity + else: + info.append( + { + "id": id_, + "name": "H H " + name if hh else name, + "isHappyHour": hh, + type_: quantity, + } + ) + return info + + +def to_camel_case(snake_str): + components = snake_str.split("_") + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0].lower() + "".join(x.title() for x in components[1:]) diff --git a/bookie/src/app/app-routing.module.ts b/bookie/src/app/app-routing.module.ts index f564a6f..f5b6e04 100644 --- a/bookie/src/app/app-routing.module.ts +++ b/bookie/src/app/app-routing.module.ts @@ -33,6 +33,10 @@ const routes: Routes = [ path: 'products', loadChildren: () => import('./product/products.module').then(mod => mod.ProductsModule) }, + { + path: 'product-sale-report', + loadChildren: () => import('./product-sale-report/product-sale-report.module').then(mod => mod.ProductSaleReportModule) + }, { path: 'menu-categories', loadChildren: () => import('./menu-category/menu-categories.module').then(mod => mod.MenuCategoriesModule) diff --git a/bookie/src/app/home/home.component.html b/bookie/src/app/home/home.component.html index 477036d..be095ae 100644 --- a/bookie/src/app/home/home.component.html +++ b/bookie/src/app/home/home.component.html @@ -20,6 +20,9 @@

Tax Analysis

+ +

Product Sale Report

+

Tables

diff --git a/bookie/src/app/product-sale-report/product-sale-report-datasource.ts b/bookie/src/app/product-sale-report/product-sale-report-datasource.ts new file mode 100644 index 0000000..0333094 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report-datasource.ts @@ -0,0 +1,18 @@ +import { DataSource } from '@angular/cdk/collections'; +import { Observable, of as observableOf } from 'rxjs'; +import { ProductSaleReportItem } from './product-sale-report'; + + +export class ProductSaleReportDataSource extends DataSource { + + constructor(public data: ProductSaleReportItem[]) { + super(); + } + + connect(): Observable { + return observableOf(this.data); + } + + disconnect() { + } +} diff --git a/bookie/src/app/product-sale-report/product-sale-report-resolver.service.spec.ts b/bookie/src/app/product-sale-report/product-sale-report-resolver.service.spec.ts new file mode 100644 index 0000000..dcd2345 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report-resolver.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductSaleReportResolver} from './product-sale-report-resolver.service'; + +describe('ProductSaleReportResolver', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductSaleReportResolver] + }); + }); + + it('should be created', inject([ProductSaleReportResolver], (service: ProductSaleReportResolver) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product-sale-report/product-sale-report-resolver.service.ts b/bookie/src/app/product-sale-report/product-sale-report-resolver.service.ts new file mode 100644 index 0000000..c7cbac2 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report-resolver.service.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs/internal/Observable'; +import {ProductSaleReport} from './product-sale-report'; +import {ProductSaleReportService} from './product-sale-report.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductSaleReportResolver implements Resolve { + + constructor(private ser: ProductSaleReportService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const startDate = route.queryParamMap.get('startDate') || null; + const finishDate = route.queryParamMap.get('finishDate') || null; + return this.ser.get(startDate, finishDate); + } +} diff --git a/bookie/src/app/product-sale-report/product-sale-report-routing.module.spec.ts b/bookie/src/app/product-sale-report/product-sale-report-routing.module.spec.ts new file mode 100644 index 0000000..452df50 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report-routing.module.spec.ts @@ -0,0 +1,13 @@ +import {ProductSaleReportRoutingModule} from './product-sale-report-routing.module'; + +describe('ProductSaleReportRoutingModule', () => { + let productSaleReportRoutingModule: ProductSaleReportRoutingModule; + + beforeEach(() => { + productSaleReportRoutingModule = new ProductSaleReportRoutingModule(); + }); + + it('should create an instance', () => { + expect(productSaleReportRoutingModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product-sale-report/product-sale-report-routing.module.ts b/bookie/src/app/product-sale-report/product-sale-report-routing.module.ts new file mode 100644 index 0000000..d433886 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report-routing.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { ProductSaleReportResolver } from './product-sale-report-resolver.service'; +import { AuthGuard } from '../auth/auth-guard.service'; +import { ProductSaleReportComponent } from './product-sale-report.component'; + +const ProductSaleReportRoutes: Routes = [ + { + path: '', + component: ProductSaleReportComponent, + canActivate: [AuthGuard], + data: { + permission: 'Sales Detail' // rename to Product Sale Report + }, + resolve: { + info: ProductSaleReportResolver + }, + runGuardsAndResolvers: 'always' + } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(ProductSaleReportRoutes) + + ], + exports: [ + RouterModule + ], + providers: [ + ProductSaleReportResolver + ] +}) +export class ProductSaleReportRoutingModule { +} diff --git a/bookie/src/app/product-sale-report/product-sale-report.component.css b/bookie/src/app/product-sale-report/product-sale-report.component.css new file mode 100644 index 0000000..a9626b3 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.component.css @@ -0,0 +1,4 @@ +.right { + display: flex; + justify-content: flex-end; +} diff --git a/bookie/src/app/product-sale-report/product-sale-report.component.html b/bookie/src/app/product-sale-report/product-sale-report.component.html new file mode 100644 index 0000000..74e6f4f --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.component.html @@ -0,0 +1,69 @@ + + + Product Sale Report + + + +
+
+ + + + + + + + + + + +
+
+ + + + + Name + {{row.name}} + + + + + Unbilled + {{row.kot | number:'1.2-2'}} + + + + + Sale + {{row.regularBill | number:'1.2-2'}} + + + + + No Charge + {{row.noCharge | number:'1.2-2'}} + + + + + Staff + {{row.staff | number:'1.2-2'}} + + + + + Void + {{row.void | number:'1.2-2'}} + + + + + +
+
diff --git a/bookie/src/app/product-sale-report/product-sale-report.component.spec.ts b/bookie/src/app/product-sale-report/product-sale-report.component.spec.ts new file mode 100644 index 0000000..04c2fc0 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.component.spec.ts @@ -0,0 +1,25 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProductSaleReportComponent} from './product-sale-report.component'; + +describe('ProductSaleReportComponent', () => { + let component: ProductSaleReportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ProductSaleReportComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductSaleReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product-sale-report/product-sale-report.component.ts b/bookie/src/app/product-sale-report/product-sale-report.component.ts new file mode 100644 index 0000000..a91637a --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.component.ts @@ -0,0 +1,90 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import * as moment from 'moment'; +import { ProductSaleReportDataSource } from './product-sale-report-datasource'; +import { ProductSaleReport } from './product-sale-report'; +import { ToCsvService } from '../shared/to-csv.service'; + +@Component({ + selector: 'app-product-sale-report', + templateUrl: './product-sale-report.component.html', + styleUrls: ['./product-sale-report.component.css'] +}) +export class ProductSaleReportComponent implements OnInit { + dataSource: ProductSaleReportDataSource; + form: FormGroup; + info: ProductSaleReport; + + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ + displayedColumns = ['name', 'unbilled', 'sale', 'noCharge', 'staff', 'void']; + + + constructor( + private route: ActivatedRoute, + private router: Router, + private fb: FormBuilder, + private toCsv: ToCsvService + ) { + this.createForm(); + + } + + ngOnInit() { + this.route.data + .subscribe((data: { info: ProductSaleReport }) => { + this.info = data.info; + this.form.setValue({ + startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(), + finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate() + }); + this.dataSource = new ProductSaleReportDataSource(this.info.amounts); + }); + } + + show() { + const info = this.getInfo(); + this.router.navigate(['product-sale-report'], { + queryParams: { + startDate: info.startDate, + finishDate: info.finishDate + } + }); + } + + createForm() { + this.form = this.fb.group({ + startDate: '', + finishDate: '' + }); + } + + getInfo(): ProductSaleReport { + const formModel = this.form.value; + + return { + startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), + finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY') + }; + } + + exportCsv() { + const headers = { + Date: 'date', + Name: 'name', + Type: 'type', + Narration: 'narration', + Debit: 'debit', + Credit: 'credit', + Running: 'running', + Posted: 'posted' + }; + const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {type: 'text/csv;charset=utf-8;'}); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(csvData); + link.setAttribute('download', 'product-sale-report.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } +} diff --git a/bookie/src/app/product-sale-report/product-sale-report.module.spec.ts b/bookie/src/app/product-sale-report/product-sale-report.module.spec.ts new file mode 100644 index 0000000..6106551 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.module.spec.ts @@ -0,0 +1,13 @@ +import {ProductSaleReportModule} from './product-sale-report.module'; + +describe('ProductSaleReportModule', () => { + let productSaleReportModule: ProductSaleReportModule; + + beforeEach(() => { + productSaleReportModule = new ProductSaleReportModule(); + }); + + it('should create an instance', () => { + expect(productSaleReportModule).toBeTruthy(); + }); +}); diff --git a/bookie/src/app/product-sale-report/product-sale-report.module.ts b/bookie/src/app/product-sale-report/product-sale-report.module.ts new file mode 100644 index 0000000..c317089 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.module.ts @@ -0,0 +1,61 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatTableModule } from '@angular/material/table'; +import { SharedModule} from '../shared/shared.module'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CdkTableModule } from '@angular/cdk/table'; +import { ProductSaleReportRoutingModule } from './product-sale-report-routing.module'; +import { ProductSaleReportComponent } from './product-sale-report.component'; +import { MomentDateAdapter } from '@angular/material-moment-adapter'; +import { A11yModule } from '@angular/cdk/a11y'; +import { FlexLayoutModule } from '@angular/flex-layout'; + +export const MY_FORMATS = { + parse: { + dateInput: 'DD-MMM-YYYY', + }, + display: { + dateInput: 'DD-MMM-YYYY', + monthYearLabel: 'MMM YYYY', + dateA11yLabel: 'DD-MMM-YYYY', + monthYearA11yLabel: 'MMM YYYY', + }, +}; + +@NgModule({ + imports: [ + A11yModule, + CommonModule, + CdkTableModule, + FlexLayoutModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatDatepickerModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatNativeDateModule, + MatTableModule, + ReactiveFormsModule, + SharedModule, + ProductSaleReportRoutingModule + ], + declarations: [ + ProductSaleReportComponent + ], + providers: [ + {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}, + {provide: MAT_DATE_FORMATS, useValue: MY_FORMATS}, + ] +}) +export class ProductSaleReportModule { +} diff --git a/bookie/src/app/product-sale-report/product-sale-report.service.spec.ts b/bookie/src/app/product-sale-report/product-sale-report.service.spec.ts new file mode 100644 index 0000000..d54dcb6 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.service.spec.ts @@ -0,0 +1,15 @@ +import {inject, TestBed} from '@angular/core/testing'; + +import {ProductSaleReportService} from './product-sale-report.service'; + +describe('ProductSaleReportService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProductSaleReportService] + }); + }); + + it('should be created', inject([ProductSaleReportService], (service: ProductSaleReportService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/bookie/src/app/product-sale-report/product-sale-report.service.ts b/bookie/src/app/product-sale-report/product-sale-report.service.ts new file mode 100644 index 0000000..6bbcde8 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { catchError } from 'rxjs/operators'; +import { Observable } from 'rxjs/internal/Observable'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { ProductSaleReport } from './product-sale-report'; +import { ErrorLoggerService } from '../core/error-logger.service'; + +const httpOptions = { + headers: new HttpHeaders({'Content-Type': 'application/json'}) +}; + +const url = '/v1/product-sale-report'; +const serviceName = 'ProductSaleReportService'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductSaleReportService { + + constructor(private http: HttpClient, private log: ErrorLoggerService) { + } + + get(startDate: string, finishDate): Observable { + const options = {params: new HttpParams()}; + if (startDate !== null) { + options.params = options.params.set('s', startDate); + } + if (finishDate !== null) { + options.params = options.params.set('f', finishDate); + } + return >this.http.get(url, options) + .pipe( + catchError(this.log.handleError(serviceName, 'get')) + ); + } +} diff --git a/bookie/src/app/product-sale-report/product-sale-report.ts b/bookie/src/app/product-sale-report/product-sale-report.ts new file mode 100644 index 0000000..a4ae423 --- /dev/null +++ b/bookie/src/app/product-sale-report/product-sale-report.ts @@ -0,0 +1,14 @@ +export class ProductSaleReportItem { + name: string; + kot: number; + regularBill: number; + noCharge: number; + staf: number; + void: number; +} + +export class ProductSaleReport { + startDate: string; + finishDate: string; + amounts?: ProductSaleReportItem[]; +} diff --git a/bookie/src/app/tax-analysis/tax-analysis.ts b/bookie/src/app/tax-analysis/tax-analysis.ts index af69942..654395f 100644 --- a/bookie/src/app/tax-analysis/tax-analysis.ts +++ b/bookie/src/app/tax-analysis/tax-analysis.ts @@ -1,5 +1,7 @@ export class TaxAnalysisItem { name: string; + taxRate: number; + saleAmount: number; amount: number; }