From f8418934662abe50ab5ec2c933b5a96e18a6df8f Mon Sep 17 00:00:00 2001 From: David Podhola Date: Tue, 20 Apr 2021 15:35:07 +0200 Subject: [PATCH] Issue 74 customer pricelist (#78) - Part 1 This is the first part about the Customer Price List adding the Customer Price List CRUD screens and the sales invoice line calculation. It is missing: - changes in the invoicing screen - setting up the products for the Customer Price List - reports --- apps/api/src/app/migrations.ts | 2 + .../app/migrations/1595508635320-CreateDB.ts | 2 +- .../1618661208366-CustomerPriceList.ts | 83 ++++++++++ apps/api/src/app/resolvers.ts | 6 + apps/api/src/app/resolvers/bank.resolver.ts | 3 +- .../app/resolvers/customer.group.resolver.ts | 38 +++++ .../resolvers/customer.price.list.resolver.ts | 42 +++++ .../customer.product.price.resolver.ts | 45 ++++++ .../customer.product.price.save.args.ts | 14 ++ .../app/saveArgs/customerGroup.save.args.ts | 10 ++ .../saveArgs/customerPriceList.save.args.ts | 26 ++++ .../src/model/generated/entities/Customer.ts | 13 +- .../model/generated/entities/CustomerGroup.ts | 68 ++++++++ .../generated/entities/CustomerPriceList.ts | 52 +++++++ .../entities/CustomerProductPrice.ts | 34 ++++ .../src/model/generated/entities/Product.ts | 9 ++ apps/api/src/model/index.ts | 9 ++ .../api/src/model/lib/customer.group.model.ts | 7 + .../lib/customer.group.save.args.model.ts | 5 + .../src/model/lib/customer.group.service.ts | 38 +++++ apps/api/src/model/lib/customer.model.ts | 3 + .../model/lib/customer.price.list.model.ts | 11 ++ .../customer.price.list.save.args.model.ts | 18 +++ .../model/lib/customer.price.list.service.ts | 120 +++++++++++++++ .../model/lib/customer.product.price.model.ts | 9 ++ .../customer.product.price.save.args.model.ts | 11 ++ .../lib/customer.product.price.service.ts | 50 ++++++ apps/api/src/model/lib/entities.ts | 6 + .../lib/sales.invoice.line.service.spec.ts | 145 +++++++++++++++++- .../src/model/lib/sales.invoice.service.ts | 48 +++++- apps/api/src/model/serviceProviders.ts | 30 ++++ .../AddOrEditCustomerGroup.svelte | 112 ++++++++++++++ .../addOrEditCustomerGroup.gql | 10 ++ .../customerGroups/CustomerGroupList.svelte | 32 ++++ .../customerGroups/CustomerGroupSelect.svelte | 46 ++++++ .../customerGroups/FirstColumn.svelte | 13 ++ clients/admin/src/generated/graphql.ts | 124 +++++++++++++++ .../admin/src/lib/customerGroup.fragment.gql | 12 ++ clients/admin/src/lib/customerGroup.gql | 5 + clients/admin/src/lib/customerGroup.ts | 45 ++++++ clients/admin/src/lib/fragments.ts | 64 ++++++-- clients/admin/src/lib/mocks.ts | 10 ++ .../admin/src/lib/queries/customerGroup.ts | 40 +++++ .../admin/src/lib/queries/customerGroups.ts | 24 +++ clients/admin/src/locales/en.json | 28 ++++ .../admin/src/pages/AddCustomerGroup.svelte | 21 +++ .../admin/src/pages/AddCustomerGroup.test.ts | 31 ++++ .../src/pages/CustomerGroupDetail.svelte | 78 ++++++++++ .../src/pages/CustomerGroupDetail.test.ts | 21 +++ clients/admin/src/pages/CustomerGroups.svelte | 39 +++++ .../admin/src/pages/CustomerGroups.test.ts | 21 +++ .../admin/src/pages/EditCustomerGroup.svelte | 36 +++++ .../admin/src/pages/EditCustomerGroup.test.ts | 44 ++++++ clients/admin/src/pages/Lists.svelte | 5 + clients/admin/src/pages/customerGroups.gql | 7 + clients/admin/src/pages/pathAndSegment.ts | 7 + clients/admin/src/routes.ts | 8 + .../AddOrEditCustomerGroup.stories.svelte | 24 +++ .../CustomerGroupDetail.stories.svelte | 18 +++ .../stories/CustomerGroupList.stories.svelte | 18 +++ .../CustomerGroupSelect.stories.svelte | 36 +++++ schema.gql | 62 ++++++++ 62 files changed, 1974 insertions(+), 24 deletions(-) create mode 100644 apps/api/src/app/migrations/1618661208366-CustomerPriceList.ts create mode 100644 apps/api/src/app/resolvers/customer.group.resolver.ts create mode 100644 apps/api/src/app/resolvers/customer.price.list.resolver.ts create mode 100644 apps/api/src/app/resolvers/customer.product.price.resolver.ts create mode 100644 apps/api/src/app/saveArgs/customer.product.price.save.args.ts create mode 100644 apps/api/src/app/saveArgs/customerGroup.save.args.ts create mode 100644 apps/api/src/app/saveArgs/customerPriceList.save.args.ts create mode 100644 apps/api/src/model/generated/entities/CustomerGroup.ts create mode 100644 apps/api/src/model/generated/entities/CustomerPriceList.ts create mode 100644 apps/api/src/model/generated/entities/CustomerProductPrice.ts create mode 100644 apps/api/src/model/lib/customer.group.model.ts create mode 100644 apps/api/src/model/lib/customer.group.save.args.model.ts create mode 100644 apps/api/src/model/lib/customer.group.service.ts create mode 100644 apps/api/src/model/lib/customer.price.list.model.ts create mode 100644 apps/api/src/model/lib/customer.price.list.save.args.model.ts create mode 100644 apps/api/src/model/lib/customer.price.list.service.ts create mode 100644 apps/api/src/model/lib/customer.product.price.model.ts create mode 100644 apps/api/src/model/lib/customer.product.price.save.args.model.ts create mode 100644 apps/api/src/model/lib/customer.product.price.service.ts create mode 100644 clients/admin/src/components/add-customerGroup/AddOrEditCustomerGroup.svelte create mode 100644 clients/admin/src/components/add-customerGroup/addOrEditCustomerGroup.gql create mode 100644 clients/admin/src/components/customerGroups/CustomerGroupList.svelte create mode 100644 clients/admin/src/components/customerGroups/CustomerGroupSelect.svelte create mode 100644 clients/admin/src/components/customerGroups/FirstColumn.svelte create mode 100644 clients/admin/src/lib/customerGroup.fragment.gql create mode 100644 clients/admin/src/lib/customerGroup.gql create mode 100644 clients/admin/src/lib/customerGroup.ts create mode 100644 clients/admin/src/lib/queries/customerGroup.ts create mode 100644 clients/admin/src/lib/queries/customerGroups.ts create mode 100644 clients/admin/src/pages/AddCustomerGroup.svelte create mode 100644 clients/admin/src/pages/AddCustomerGroup.test.ts create mode 100644 clients/admin/src/pages/CustomerGroupDetail.svelte create mode 100644 clients/admin/src/pages/CustomerGroupDetail.test.ts create mode 100644 clients/admin/src/pages/CustomerGroups.svelte create mode 100644 clients/admin/src/pages/CustomerGroups.test.ts create mode 100644 clients/admin/src/pages/EditCustomerGroup.svelte create mode 100644 clients/admin/src/pages/EditCustomerGroup.test.ts create mode 100644 clients/admin/src/pages/customerGroups.gql create mode 100644 clients/admin/src/stories/AddOrEditCustomerGroup.stories.svelte create mode 100644 clients/admin/src/stories/CustomerGroupDetail.stories.svelte create mode 100644 clients/admin/src/stories/CustomerGroupList.stories.svelte create mode 100644 clients/admin/src/stories/CustomerGroupSelect.stories.svelte diff --git a/apps/api/src/app/migrations.ts b/apps/api/src/app/migrations.ts index 617c7fd2..021b954b 100644 --- a/apps/api/src/app/migrations.ts +++ b/apps/api/src/app/migrations.ts @@ -8,6 +8,7 @@ import { MenuContent1612983991735 } from './migrations/1612983991735-MenuContent import { EnhanceCustomer1615749063579 } from './migrations/1615749063579-EnhanceCustomer'; import { MenuSettings1615961288134 } from './migrations/1615961288134-MenuSettings'; import { UoM1616228731111 } from './migrations/1616228731111-UoM'; +import { CustomerPriceList1618661208366 } from './migrations/1618661208366-CustomerPriceList'; export const migrations = [ CreateDB1595508635320, @@ -20,4 +21,5 @@ export const migrations = [ EnhanceCustomer1615749063579, MenuSettings1615961288134, UoM1616228731111, + CustomerPriceList1618661208366, ]; diff --git a/apps/api/src/app/migrations/1595508635320-CreateDB.ts b/apps/api/src/app/migrations/1595508635320-CreateDB.ts index e61f32d9..01c610a5 100644 --- a/apps/api/src/app/migrations/1595508635320-CreateDB.ts +++ b/apps/api/src/app/migrations/1595508635320-CreateDB.ts @@ -194,7 +194,7 @@ export class CreateDB1595508635320 implements MigrationInterface { `ALTER TABLE "public"."customer" ADD CONSTRAINT "FK_2c1aeb39925d1e1ace946ca2f21" FOREIGN KEY ("addressId") REFERENCES "public"."address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, ); await queryRunner.query( - `CREATE TABLE IF NOT EXISTS "public"."unit_of_measurement_conversion" ("id" SERIAL NOT NULL, "updtTs" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean NOT NULL DEFAULT true, "isCurrent" boolean NOT NULL DEFAULT true, "currencyMultiplyingRate" double precision NOT NULL, "end" date NOT NULL, "start" date NOT NULL, "updtOpId" integer NOT NULL, "fromId" integer, "toId" integer, CONSTRAINT "PK_f8796ed806b216628b252bb3b4c" PRIMARY KEY ("id"))`, + `CREATE TABLE IF NOT EXISTS "public"."unit_of_measurement_conversion" ("id" SERIAL NOT NULL, "updtTs" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean NOT NULL DEFAULT true, "isCurrent" boolean NOT NULL DEFAULT true, "currencyMultiplyingRate" double precision NOT NULL, "updtOpId" integer NOT NULL, "fromId" integer, "toId" integer, CONSTRAINT "PK_f8796ed806b216628b252bb3b4c" PRIMARY KEY ("id"))`, ); await queryRunner.query( `CREATE TABLE IF NOT EXISTS "public"."unit_of_measurement" ("id" SERIAL NOT NULL, "updtTs" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean NOT NULL DEFAULT true, "isCurrent" boolean NOT NULL DEFAULT true, "displayName" character varying NOT NULL, "updtOpId" integer NOT NULL, CONSTRAINT "PK_f64cb86b321fc095bde6961d6da" PRIMARY KEY ("id"))`, diff --git a/apps/api/src/app/migrations/1618661208366-CustomerPriceList.ts b/apps/api/src/app/migrations/1618661208366-CustomerPriceList.ts new file mode 100644 index 00000000..567298ea --- /dev/null +++ b/apps/api/src/app/migrations/1618661208366-CustomerPriceList.ts @@ -0,0 +1,83 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CustomerPriceList1618661208366 implements MigrationInterface { + name = 'CustomerPriceList1618661208366'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "public"."customerProductPrice" ("id" SERIAL NOT NULL, "sellingPrice" numeric(12,2) NOT NULL, "productId" integer NOT NULL, "customerPriceListId" integer NOT NULL, CONSTRAINT "PK_41712b48577a11adfdbb81efdf9" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "public"."customerPriceList" ("id" SERIAL NOT NULL, "displayName" character varying NOT NULL, "validFrom" TIMESTAMP, "validTo" TIMESTAMP, "customerGroupId" integer NOT NULL, CONSTRAINT "PK_3a410c80620c92c128634387f20" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_customerPriceList_displayName" ON "public"."customerPriceList" ("displayName") `, + ); + await queryRunner.query( + `CREATE TABLE "public"."customerGroup" ("id" SERIAL NOT NULL, "updtTs" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean NOT NULL DEFAULT true, "isCurrent" boolean NOT NULL DEFAULT true, "displayName" character varying NOT NULL, "updtOpId" integer NOT NULL, CONSTRAINT "PK_2f921748bb8e683b3d7007182ca" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_customerGroup_displayName" ON "public"."customerGroup" ("displayName") `, + ); + await queryRunner.query( + `ALTER TABLE "public"."unit_of_measurement_conversion" DROP COLUMN IF EXISTS "end"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."unit_of_measurement_conversion" DROP COLUMN IF EXISTS "start"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customer" ADD "customerGroupId" integer`, + ); + await queryRunner.query( + `ALTER TABLE "public"."user" ALTER COLUMN "updtOpId" SET DEFAULT 0`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerProductPrice" ADD CONSTRAINT "FK_51fd886ac463154b1fb68ad944a" FOREIGN KEY ("productId") REFERENCES "public"."product"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerProductPrice" ADD CONSTRAINT "FK_5fad018dfcf766b9fab575e5ea1" FOREIGN KEY ("customerPriceListId") REFERENCES "public"."customerPriceList"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerPriceList" ADD CONSTRAINT "FK_635c71f6ed02473cca6603a3079" FOREIGN KEY ("customerGroupId") REFERENCES "public"."customerGroup"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerGroup" ADD CONSTRAINT "FK_6622e70e814cce558a98043c611" FOREIGN KEY ("updtOpId") REFERENCES "public"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customer" ADD CONSTRAINT "FK_07b06500ab5d46137b7f87cc53c" FOREIGN KEY ("customerGroupId") REFERENCES "public"."customerGroup"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "public"."customer" DROP CONSTRAINT "FK_07b06500ab5d46137b7f87cc53c"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerGroup" DROP CONSTRAINT "FK_6622e70e814cce558a98043c611"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerPriceList" DROP CONSTRAINT "FK_635c71f6ed02473cca6603a3079"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerProductPrice" DROP CONSTRAINT "FK_5fad018dfcf766b9fab575e5ea1"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customerProductPrice" DROP CONSTRAINT "FK_51fd886ac463154b1fb68ad944a"`, + ); + await queryRunner.query( + `ALTER TABLE "public"."user" ALTER COLUMN "updtOpId" SET DEFAULT '0'`, + ); + await queryRunner.query( + `ALTER TABLE "public"."customer" DROP COLUMN "customerGroupId"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_customerGroup_displayName"`, + ); + await queryRunner.query(`DROP TABLE "public"."customerGroup"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_customerPriceList_displayName"`, + ); + await queryRunner.query(`DROP TABLE "public"."customerPriceList"`); + await queryRunner.query(`DROP TABLE "public"."customerProductPrice"`); + } +} diff --git a/apps/api/src/app/resolvers.ts b/apps/api/src/app/resolvers.ts index e438cc35..08fd9147 100644 --- a/apps/api/src/app/resolvers.ts +++ b/apps/api/src/app/resolvers.ts @@ -8,6 +8,9 @@ import { CurrencyResolver } from './resolvers/currency.resolver'; import { CountryResolver } from './resolvers/country.resolver'; import { BankResolver } from './resolvers/bank.resolver'; import { AccountingSchemeResolver } from './resolvers/accounting.scheme.resolver'; +import { CustomerGroupResolver } from './resolvers/customer.group.resolver'; +import { CustomerPriceListResolver } from './resolvers/customer.price.list.resolver'; +import { CustomerProductPriceResolver } from './resolvers/customer.product.price.resolver'; export const resolvers = [ AppResolver, @@ -21,4 +24,7 @@ export const resolvers = [ CountryResolver, BankResolver, AccountingSchemeResolver, + CustomerGroupResolver, + CustomerPriceListResolver, + CustomerProductPriceResolver, ]; diff --git a/apps/api/src/app/resolvers/bank.resolver.ts b/apps/api/src/app/resolvers/bank.resolver.ts index 68085182..b559f2d1 100644 --- a/apps/api/src/app/resolvers/bank.resolver.ts +++ b/apps/api/src/app/resolvers/bank.resolver.ts @@ -21,8 +21,7 @@ export class BankResolver { @Query(() => Bank) async bank(@Args('id', { type: () => Int }) id: number) { - const result = await this.bankService.loadEntityById(getManager(), id); - return result; + return await this.bankService.loadEntityById(getManager(), id); } @Mutation(() => Bank) diff --git a/apps/api/src/app/resolvers/customer.group.resolver.ts b/apps/api/src/app/resolvers/customer.group.resolver.ts new file mode 100644 index 00000000..8d309ee4 --- /dev/null +++ b/apps/api/src/app/resolvers/customer.group.resolver.ts @@ -0,0 +1,38 @@ +import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Inject, UseGuards } from '@nestjs/common'; +import { CurrentUser, GqlAuthGuard } from '../../auth'; +import { CustomerGroup } from '../../model/generated/entities/CustomerGroup'; +import { + CustomerGroupModel, + CustomerGroupService, + CustomerGroupServiceKey, +} from '../../model'; +import { getManager } from 'typeorm'; +import { CustomerGroupSaveArgs } from '../saveArgs/customerGroup.save.args'; + +@Resolver(() => CustomerGroup) +@UseGuards(GqlAuthGuard) +export class CustomerGroupResolver { + constructor( + @Inject(CustomerGroupServiceKey) + protected readonly customerGroupService: CustomerGroupService, + ) {} + + @Query(() => [CustomerGroup]) + async customerGroups() { + return await this.customerGroupService.loadEntities(getManager()); + } + + @Query(() => CustomerGroup) + async customerGroup(@Args('id', { type: () => Int }) id: number) { + return await this.customerGroupService.loadEntityById(getManager(), id); + } + + @Mutation(() => CustomerGroup) + async saveCustomerGroup( + @Args('args') objData: CustomerGroupSaveArgs, + @CurrentUser() user, + ): Promise { + return await this.customerGroupService.save(getManager(), objData, user); + } +} diff --git a/apps/api/src/app/resolvers/customer.price.list.resolver.ts b/apps/api/src/app/resolvers/customer.price.list.resolver.ts new file mode 100644 index 00000000..5fd33e81 --- /dev/null +++ b/apps/api/src/app/resolvers/customer.price.list.resolver.ts @@ -0,0 +1,42 @@ +import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Inject, UseGuards } from '@nestjs/common'; +import { CurrentUser, GqlAuthGuard } from '../../auth'; +import { CustomerPriceList } from '../../model/generated/entities/CustomerPriceList'; +import { + CustomerPriceListModel, + CustomerPriceListService, + CustomerPriceListServiceKey, +} from '../../model'; +import { getManager } from 'typeorm'; +import { CustomerPriceListSaveArgs } from '../saveArgs/customerPriceList.save.args'; + +@Resolver(() => CustomerPriceList) +@UseGuards(GqlAuthGuard) +export class CustomerPriceListResolver { + constructor( + @Inject(CustomerPriceListServiceKey) + protected readonly customerPriceListService: CustomerPriceListService, + ) {} + + @Query(() => [CustomerPriceList]) + async customerPriceLists() { + return await this.customerPriceListService.loadEntities(getManager()); + } + + @Query(() => CustomerPriceList) + async customerPriceList(@Args('id', { type: () => Int }) id: number) { + return await this.customerPriceListService.loadEntityById(getManager(), id); + } + + @Mutation(() => CustomerPriceList) + async saveCustomerPriceList( + @Args('args') objData: CustomerPriceListSaveArgs, + @CurrentUser() user, + ): Promise { + return await this.customerPriceListService.save( + getManager(), + objData, + user, + ); + } +} diff --git a/apps/api/src/app/resolvers/customer.product.price.resolver.ts b/apps/api/src/app/resolvers/customer.product.price.resolver.ts new file mode 100644 index 00000000..edf25945 --- /dev/null +++ b/apps/api/src/app/resolvers/customer.product.price.resolver.ts @@ -0,0 +1,45 @@ +import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Inject, UseGuards } from '@nestjs/common'; +import { CurrentUser, GqlAuthGuard } from '../../auth'; +import { CustomerProductPrice } from '../../model/generated/entities/CustomerProductPrice'; +import { + CustomerProductPriceModel, + CustomerProductPriceService, + CustomerProductPriceServiceKey, +} from '../../model'; +import { getManager } from 'typeorm'; +import { CustomerProductPriceSaveArgs } from '../saveArgs/customer.product.price.save.args'; + +@Resolver(() => CustomerProductPrice) +@UseGuards(GqlAuthGuard) +export class CustomerProductPriceResolver { + constructor( + @Inject(CustomerProductPriceServiceKey) + protected readonly customerProductPriceService: CustomerProductPriceService, + ) {} + + @Query(() => [CustomerProductPrice]) + async customerProductPrices() { + return await this.customerProductPriceService.loadEntities(getManager()); + } + + @Query(() => CustomerProductPrice) + async customerProductPrice(@Args('id', { type: () => Int }) id: number) { + return await this.customerProductPriceService.loadEntityById( + getManager(), + id, + ); + } + + @Mutation(() => CustomerProductPrice) + async saveCustomerProductPrice( + @Args('args') objData: CustomerProductPriceSaveArgs, + @CurrentUser() user, + ): Promise { + return await this.customerProductPriceService.save( + getManager(), + objData, + user, + ); + } +} diff --git a/apps/api/src/app/saveArgs/customer.product.price.save.args.ts b/apps/api/src/app/saveArgs/customer.product.price.save.args.ts new file mode 100644 index 00000000..1f9b21af --- /dev/null +++ b/apps/api/src/app/saveArgs/customer.product.price.save.args.ts @@ -0,0 +1,14 @@ +import { BaseSaveArgs } from './base.save.args'; +import { CustomerProductPriceSaveArgsModel } from '../../model'; +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class CustomerProductPriceSaveArgs extends BaseSaveArgs + implements CustomerProductPriceSaveArgsModel { + @Field() + customerPriceListDisplayName: string; + @Field() + productSKU: string; + @Field() + sellingPrice: number; +} diff --git a/apps/api/src/app/saveArgs/customerGroup.save.args.ts b/apps/api/src/app/saveArgs/customerGroup.save.args.ts new file mode 100644 index 00000000..3646d8f6 --- /dev/null +++ b/apps/api/src/app/saveArgs/customerGroup.save.args.ts @@ -0,0 +1,10 @@ +import { BaseSaveArgs } from './base.save.args'; +import { CustomerGroupSaveArgsModel } from '../../model'; +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class CustomerGroupSaveArgs extends BaseSaveArgs + implements CustomerGroupSaveArgsModel { + @Field() + displayName: string; +} diff --git a/apps/api/src/app/saveArgs/customerPriceList.save.args.ts b/apps/api/src/app/saveArgs/customerPriceList.save.args.ts new file mode 100644 index 00000000..8decb303 --- /dev/null +++ b/apps/api/src/app/saveArgs/customerPriceList.save.args.ts @@ -0,0 +1,26 @@ +import { BaseSaveArgs } from './base.save.args'; +import { CustomerPriceListSaveArgsModel, ProductPrice } from '../../model'; +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class ProductPriceSaveArgs implements ProductPrice { + @Field() + productSKU: string; + @Field() + sellingPrice: number; +} + +@InputType() +export class CustomerPriceListSaveArgs extends BaseSaveArgs + implements CustomerPriceListSaveArgsModel { + @Field() + customerGroupDisplayName: string; + @Field() + displayName: string; + @Field(() => [ProductPriceSaveArgs]) + productPrices: Array; + @Field() + validFrom: Date; + @Field() + validTo: Date; +} diff --git a/apps/api/src/model/generated/entities/Customer.ts b/apps/api/src/model/generated/entities/Customer.ts index fba71d21..f964e1aa 100644 --- a/apps/api/src/model/generated/entities/Customer.ts +++ b/apps/api/src/model/generated/entities/Customer.ts @@ -15,13 +15,16 @@ import { SalesInvoiceModel } from '../../lib/sales.invoice.model'; import { User } from './User'; import { UserModel } from '../../lib/user.model'; import { DateTimeScalarType } from '../../../app/support/date.scalar'; +import { CustomerGroup } from './CustomerGroup'; +import { CustomerGroupModel } from '../../lib/customer.group.model'; +import { CustomerModel } from '../../lib/customer.model'; @Index('IDX_df529c45726940beb548906481', ['displayName'], { unique: true }) @Index('IDX_71b54ec7502c83c7f503f57c64', ['legalName'], { unique: true }) @Index('IDX_a843215c5e375894bcd5bdf24a', ['vatNumber'], { unique: true }) @Entity('customer', { schema: 'public' }) @ObjectType() -export class Customer { +export class Customer implements CustomerModel { @PrimaryGeneratedColumn({ type: 'integer', name: 'id' }) @Field() id: number; @@ -98,4 +101,12 @@ export class Customer { @JoinColumn([{ name: 'addressId', referencedColumnName: 'id' }]) @Field(() => Address, { nullable: true }) address: AddressModel; + + @Field(() => CustomerGroup) + @ManyToOne( + () => CustomerGroup, + customerGroup => customerGroup.customers, + { nullable: true, eager: true }, + ) + customerGroup?: CustomerGroupModel; } diff --git a/apps/api/src/model/generated/entities/CustomerGroup.ts b/apps/api/src/model/generated/entities/CustomerGroup.ts new file mode 100644 index 00000000..42458708 --- /dev/null +++ b/apps/api/src/model/generated/entities/CustomerGroup.ts @@ -0,0 +1,68 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { DateTimeScalarType } from '../../../app/support/date.scalar'; +import { User } from './User'; +import { UserModel } from '../../lib/user.model'; +import { CustomerGroupModel } from '../../lib/customer.group.model'; +import { CustomerPriceListModel } from '../../lib/customer.price.list.model'; +import { CustomerModel } from '../../lib/customer.model'; +import { Customer } from './Customer'; +import { CustomerPriceList } from './CustomerPriceList'; + +@Index('IDX_customerGroup_displayName', ['displayName'], { unique: true }) +@Entity('customerGroup', { schema: 'public' }) +@ObjectType() +export class CustomerGroup implements CustomerGroupModel { + @PrimaryGeneratedColumn({ type: 'integer', name: 'id' }) + @Field(() => Int) + id: number; + + @Column('timestamp without time zone', { + name: 'updtTs', + default: () => 'now()', + }) + @Field(() => DateTimeScalarType) + updtTs: Date; + + @ManyToOne( + () => User, + user => user.updAccountingSchemes, + { nullable: false, eager: true }, + ) + @JoinColumn([{ name: 'updtOpId', referencedColumnName: 'id' }]) + @Field(() => User) + updtOp: UserModel; + + @Column('boolean', { name: 'isActive', default: () => 'true' }) + @Field() + isActive: boolean; + + @Column('boolean', { name: 'isCurrent', default: () => 'true' }) + @Field() + isCurrent: boolean; + + @Column('character varying', { name: 'displayName' }) + @Field() + displayName: string; + + @Field(() => [Customer], { nullable: true }) + @OneToMany( + () => Customer, + customer => customer.customerGroup, + ) + customers: Array; + + @OneToMany( + () => CustomerPriceList, + customerPriceList => customerPriceList.customerGroup, + ) + customerPriceLists: Array; +} diff --git a/apps/api/src/model/generated/entities/CustomerPriceList.ts b/apps/api/src/model/generated/entities/CustomerPriceList.ts new file mode 100644 index 00000000..ae3a45e3 --- /dev/null +++ b/apps/api/src/model/generated/entities/CustomerPriceList.ts @@ -0,0 +1,52 @@ +import { + Column, + Entity, + Index, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { CustomerPriceListModel } from '../../lib/customer.price.list.model'; +import { CustomerGroup } from './CustomerGroup'; +import { CustomerGroupModel } from '../../lib/customer.group.model'; +import { CustomerProductPrice } from './CustomerProductPrice'; +import { CustomerProductPriceModel } from '../../lib/customer.product.price.model'; + +@Index('IDX_customerPriceList_displayName', ['displayName'], { unique: true }) +@Entity('customerPriceList', { schema: 'public' }) +@ObjectType() +export class CustomerPriceList implements CustomerPriceListModel { + @PrimaryGeneratedColumn({ type: 'integer', name: 'id' }) + @Field(() => Int) + id: number; + + @Field(() => CustomerGroup) + @ManyToOne( + () => CustomerGroup, + customerGroupModel => customerGroupModel.customerPriceLists, + { nullable: false }, + ) + customerGroup: CustomerGroupModel; + + @Field() + @Column() + displayName: string; + + @Field(() => [CustomerProductPrice], { nullable: true }) + @OneToMany( + () => CustomerProductPrice, + customerProductPrice => customerProductPrice.customerPriceList, + ) + productPrices: Array; + + @Field({ nullable: true }) + @Column({ nullable: true }) + @Index() + validFrom: Date; + + @Field({ nullable: true }) + @Column({ nullable: true }) + @Index() + validTo: Date; +} diff --git a/apps/api/src/model/generated/entities/CustomerProductPrice.ts b/apps/api/src/model/generated/entities/CustomerProductPrice.ts new file mode 100644 index 00000000..8d3fb2db --- /dev/null +++ b/apps/api/src/model/generated/entities/CustomerProductPrice.ts @@ -0,0 +1,34 @@ +import { CustomerProductPriceModel } from '../../lib/customer.product.price.model'; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { Product } from './Product'; +import { ProductModel } from '../../lib/product.model'; +import { CustomerPriceList } from './CustomerPriceList'; +import { CustomerPriceListModel } from '../../lib/customer.price.list.model'; + +@Entity('customerProductPrice', { schema: 'public' }) +@ObjectType() +export class CustomerProductPrice implements CustomerProductPriceModel { + @PrimaryGeneratedColumn({ type: 'integer', name: 'id' }) + @Field(() => Int) + id: number; + + @Field(() => Product) + @ManyToOne( + () => Product, + product => product.customerProductPrices, + { nullable: false, eager: true }, + ) + product: ProductModel; + + @Column({ type: 'numeric', scale: 2, precision: 12 }) + @Field() + sellingPrice: number; + + @ManyToOne( + () => CustomerPriceList, + customerPriceList => customerPriceList.productPrices, + { nullable: false }, + ) + customerPriceList: CustomerPriceListModel; +} diff --git a/apps/api/src/model/generated/entities/Product.ts b/apps/api/src/model/generated/entities/Product.ts index 20e8506b..76ae9263 100644 --- a/apps/api/src/model/generated/entities/Product.ts +++ b/apps/api/src/model/generated/entities/Product.ts @@ -13,6 +13,8 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { UserModel } from '../../lib/user.model'; import { UnitOfMeasurementModel } from '../../lib/unit.of.measurement.model'; import { UnitOfMeasurement } from './UnitOfMeasurement'; +import { CustomerProductPrice } from './CustomerProductPrice'; +import { CustomerProductPriceModel } from '../../lib/customer.product.price.model'; @Index('IDX_826d69dcc65d9650be67af6d48', ['displayName'], { unique: true }) @Index('IDX_34f6ca1cd897cc926bdcca1ca3', ['sku'], { unique: true }) @@ -66,4 +68,11 @@ export class Product { @JoinColumn([{ name: 'uomId', referencedColumnName: 'id' }]) @Field(() => UnitOfMeasurement, { nullable: true }) defaultUoM: UnitOfMeasurementModel; + + @Field(() => [CustomerProductPrice], { nullable: true }) + @OneToMany( + () => CustomerProductPrice, + customerProductPrice => customerProductPrice.product, + ) + customerProductPrices: Array; } diff --git a/apps/api/src/model/index.ts b/apps/api/src/model/index.ts index e048e5c1..4db97a77 100644 --- a/apps/api/src/model/index.ts +++ b/apps/api/src/model/index.ts @@ -29,6 +29,15 @@ export * from './lib/currency.service'; export * from './lib/customer.model'; export * from './lib/customer.save.args.model'; export * from './lib/customer.service'; +export * from './lib/customer.group.model'; +export * from './lib/customer.group.service'; +export * from './lib/customer.group.save.args.model'; +export * from './lib/customer.product.price.model'; +export * from './lib/customer.product.price.service'; +export * from './lib/customer.product.price.save.args.model'; +export * from './lib/customer.price.list.model'; +export * from './lib/customer.price.list.service'; +export * from './lib/customer.price.list.save.args.model'; export * from './lib/date.service'; export * from './lib/document.numbering.service'; export * from './lib/entities'; diff --git a/apps/api/src/model/lib/customer.group.model.ts b/apps/api/src/model/lib/customer.group.model.ts new file mode 100644 index 00000000..30792ad4 --- /dev/null +++ b/apps/api/src/model/lib/customer.group.model.ts @@ -0,0 +1,7 @@ +import { BaseModel } from './base.model'; +import { CustomerModel } from './customer.model'; + +export interface CustomerGroupModel extends BaseModel { + displayName: string; + customers: Array; +} diff --git a/apps/api/src/model/lib/customer.group.save.args.model.ts b/apps/api/src/model/lib/customer.group.save.args.model.ts new file mode 100644 index 00000000..46f0c0e8 --- /dev/null +++ b/apps/api/src/model/lib/customer.group.save.args.model.ts @@ -0,0 +1,5 @@ +import { BaseSaveArgsModel } from './base.save.args.model'; + +export interface CustomerGroupSaveArgsModel extends BaseSaveArgsModel { + displayName?: string; +} diff --git a/apps/api/src/model/lib/customer.group.service.ts b/apps/api/src/model/lib/customer.group.service.ts new file mode 100644 index 00000000..3f03b5de --- /dev/null +++ b/apps/api/src/model/lib/customer.group.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { BaseEntityService } from './base.entity.service'; +import { CustomerGroupModel } from './customer.group.model'; +import { CustomerGroupSaveArgsModel } from './customer.group.save.args.model'; +import { EntityManager, Repository } from 'typeorm'; +import { CustomerGroup } from '../generated/entities/CustomerGroup'; +import { CustomerModel } from './customer.model'; + +export const CustomerGroupServiceKey = 'CustomerGroupService'; + +@Injectable() +export class CustomerGroupService extends BaseEntityService< + CustomerGroupModel, + CustomerGroupSaveArgsModel +> { + createEntity(): CustomerGroupModel { + return new CustomerGroup(); + } + + protected async doSave( + transactionalEntityManager: EntityManager, + args: CustomerGroupSaveArgsModel, + entity: CustomerGroupModel, + ): Promise { + entity.displayName = args.displayName; + return entity; + } + + protected getRepository( + transactionalEntityManager: EntityManager, + ): Repository { + return transactionalEntityManager.getRepository(CustomerGroup); + } + + typeName(): string { + return CustomerGroupServiceKey; + } +} diff --git a/apps/api/src/model/lib/customer.model.ts b/apps/api/src/model/lib/customer.model.ts index aa49e3a2..8d4d29c8 100644 --- a/apps/api/src/model/lib/customer.model.ts +++ b/apps/api/src/model/lib/customer.model.ts @@ -1,5 +1,6 @@ import { BaseModel } from './base.model'; import { AddressModel } from './address.model'; +import { CustomerGroupModel } from './customer.group.model'; export interface CustomerModel extends BaseModel { legalAddress: AddressModel; @@ -12,4 +13,6 @@ export interface CustomerModel extends BaseModel { address?: AddressModel; note?: string; + + customerGroup?: CustomerGroupModel; } diff --git a/apps/api/src/model/lib/customer.price.list.model.ts b/apps/api/src/model/lib/customer.price.list.model.ts new file mode 100644 index 00000000..a8906f6c --- /dev/null +++ b/apps/api/src/model/lib/customer.price.list.model.ts @@ -0,0 +1,11 @@ +import { BaseModel } from './base.model'; +import { CustomerGroupModel } from './customer.group.model'; +import { CustomerProductPriceModel } from './customer.product.price.model'; + +export interface CustomerPriceListModel extends BaseModel { + displayName: string; + customerGroup: CustomerGroupModel; + productPrices: Array; + validFrom?: Date; + validTo?: Date; +} diff --git a/apps/api/src/model/lib/customer.price.list.save.args.model.ts b/apps/api/src/model/lib/customer.price.list.save.args.model.ts new file mode 100644 index 00000000..cadc2938 --- /dev/null +++ b/apps/api/src/model/lib/customer.price.list.save.args.model.ts @@ -0,0 +1,18 @@ +import { BaseSaveArgsModel } from './base.save.args.model'; +import { ProductModel } from './product.model'; +import { CustomerGroupModel } from './customer.group.model'; + +export interface ProductPrice { + product?: ProductModel; + productSKU?: string; + sellingPrice: number; +} + +export interface CustomerPriceListSaveArgsModel extends BaseSaveArgsModel { + displayName: string; + customerGroup?: CustomerGroupModel; + customerGroupDisplayName?: string; + productPrices: Array; + validFrom?: Date; + validTo?: Date; +} diff --git a/apps/api/src/model/lib/customer.price.list.service.ts b/apps/api/src/model/lib/customer.price.list.service.ts new file mode 100644 index 00000000..b4c27cb0 --- /dev/null +++ b/apps/api/src/model/lib/customer.price.list.service.ts @@ -0,0 +1,120 @@ +import { BaseEntityService } from './base.entity.service'; +import { CustomerPriceListModel } from './customer.price.list.model'; +import { CustomerPriceListSaveArgsModel } from './customer.price.list.save.args.model'; +import { CustomerPriceList } from '../generated/entities/CustomerPriceList'; +import { EntityManager } from 'typeorm'; +import { UserModel } from './user.model'; +import { Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { getService } from './module.reference.service'; +import { + CustomerGroupService, + CustomerGroupServiceKey, +} from './customer.group.service'; +import { ProductService, ProductServiceKey } from './product.service'; +import { + CustomerProductPriceService, + CustomerProductPriceServiceKey, +} from './customer.product.price.service'; +import { ProductModel } from './product.model'; +import { CustomerGroupModel } from './customer.group.model'; + +export const CustomerPriceListServiceKey = 'CustomerPriceListService'; + +@Injectable() +export class CustomerPriceListService extends BaseEntityService< + CustomerPriceListModel, + CustomerPriceListSaveArgsModel +> { + createEntity(): CustomerPriceListModel { + return new CustomerPriceList(); + } + + protected async doSave( + transactionalEntityManager: EntityManager, + args: CustomerPriceListSaveArgsModel, + entity: CustomerPriceListModel, + currentUser: UserModel, + ): Promise { + const customerGroupService: CustomerGroupService = getService( + CustomerGroupServiceKey, + ); + const productService: ProductService = getService(ProductServiceKey); + const customerProductPriceService: CustomerProductPriceService = getService( + CustomerProductPriceServiceKey, + ); + + entity.customerGroup = args.customerGroup + ? args.customerGroup + : await customerGroupService.loadEntity(transactionalEntityManager, { + where: { displayName: args.customerGroupDisplayName }, + }); + entity.validFrom = args.validFrom; + entity.validTo = args.validTo; + entity.displayName = args.displayName; + + await this.persist(transactionalEntityManager, entity, currentUser); + + entity.productPrices = []; + for (const productPrice of args.productPrices) { + entity.productPrices.push( + await customerProductPriceService.save( + transactionalEntityManager, + { + sellingPrice: productPrice.sellingPrice, + product: productPrice.product + ? productPrice.product + : await productService.getProduct( + transactionalEntityManager, + productPrice.productSKU, + ), + customerPriceList: entity, + }, + currentUser, + ), + ); + } + + return entity; + } + + protected getRepository( + transactionalEntityManager: EntityManager, + ): Repository { + return transactionalEntityManager.getRepository(CustomerPriceList); + } + + typeName(): string { + return CustomerPriceListServiceKey; + } + + async loadDateValidByCustomerGroupAndProduct( + transactionalEntityManager: EntityManager, + customerGroup: CustomerGroupModel, + product: ProductModel, + ): Promise { + const productId = product.id; + const customerGroupId = customerGroup.id; + const result = await this.getRepository(transactionalEntityManager) + .createQueryBuilder('customerPriceList') + .leftJoinAndSelect('customerPriceList.customerGroup', 'customerGroup') + .leftJoinAndSelect( + 'customerPriceList.productPrices', + 'customerProductPriceModel', + ) + .leftJoinAndSelect('customerProductPriceModel.product', 'product') + .where( + 'product.id=:productId AND customerGroup.id=:customerGroupId AND ' + + ' ( now() > customerPriceList.validFrom OR customerPriceList.validFrom IS NULL )' + + ' ( now() < customerPriceList.validTo customerPriceList.validTo IS NULL )', + { + productId, + customerGroupId, + }, + ) + .orderBy('customerPriceList.validFrom', 'DESC') + .getMany(); + + return result; + } +} diff --git a/apps/api/src/model/lib/customer.product.price.model.ts b/apps/api/src/model/lib/customer.product.price.model.ts new file mode 100644 index 00000000..ddf8f942 --- /dev/null +++ b/apps/api/src/model/lib/customer.product.price.model.ts @@ -0,0 +1,9 @@ +import { ProductModel } from './product.model'; +import { BaseModel } from './base.model'; +import { CustomerPriceListModel } from './customer.price.list.model'; + +export interface CustomerProductPriceModel extends BaseModel { + product: ProductModel; + sellingPrice: number; + customerPriceList: CustomerPriceListModel; +} diff --git a/apps/api/src/model/lib/customer.product.price.save.args.model.ts b/apps/api/src/model/lib/customer.product.price.save.args.model.ts new file mode 100644 index 00000000..cb5c325e --- /dev/null +++ b/apps/api/src/model/lib/customer.product.price.save.args.model.ts @@ -0,0 +1,11 @@ +import { BaseSaveArgsModel } from './base.save.args.model'; +import { ProductModel } from './product.model'; +import { CustomerPriceListModel } from './customer.price.list.model'; + +export interface CustomerProductPriceSaveArgsModel extends BaseSaveArgsModel { + product?: ProductModel; + productSKU?: string; + sellingPrice: number; + customerPriceList?: CustomerPriceListModel; + customerPriceListDisplayName?: string; +} diff --git a/apps/api/src/model/lib/customer.product.price.service.ts b/apps/api/src/model/lib/customer.product.price.service.ts new file mode 100644 index 00000000..9b34dc81 --- /dev/null +++ b/apps/api/src/model/lib/customer.product.price.service.ts @@ -0,0 +1,50 @@ +import { BaseEntityService } from './base.entity.service'; +import { CustomerProductPriceModel } from './customer.product.price.model'; +import { CustomerProductPriceSaveArgsModel } from './customer.product.price.save.args.model'; +import { CustomerProductPrice } from '../generated/entities/CustomerProductPrice'; +import { EntityManager } from 'typeorm'; +import { Repository } from 'typeorm'; +import { getService } from './module.reference.service'; +import { + CustomerPriceListService, + CustomerPriceListServiceKey, +} from './customer.price.list.service'; + +export const CustomerProductPriceServiceKey = 'CustomerProductPriceServiceKey'; + +export class CustomerProductPriceService extends BaseEntityService< + CustomerProductPriceModel, + CustomerProductPriceSaveArgsModel +> { + createEntity(): CustomerProductPriceModel { + return new CustomerProductPrice(); + } + + protected async doSave( + transactionalEntityManager: EntityManager, + args: CustomerProductPriceSaveArgsModel, + entity: CustomerProductPriceModel, + ): Promise { + const customerPriceListService: CustomerPriceListService = getService( + CustomerPriceListServiceKey, + ); + entity.product = args.product; + entity.sellingPrice = args.sellingPrice; + entity.customerPriceList = + args.customerPriceList || + (await customerPriceListService.loadEntity(transactionalEntityManager, { + where: { displayName: args.customerPriceListDisplayName }, + })); + return entity; + } + + protected getRepository( + transactionalEntityManager: EntityManager, + ): Repository { + return transactionalEntityManager.getRepository(CustomerProductPrice); + } + + typeName(): string { + return CustomerProductPriceServiceKey; + } +} diff --git a/apps/api/src/model/lib/entities.ts b/apps/api/src/model/lib/entities.ts index 5daad873..ea21e648 100644 --- a/apps/api/src/model/lib/entities.ts +++ b/apps/api/src/model/lib/entities.ts @@ -21,6 +21,9 @@ import { MenuItem } from '../generated/entities/MenuItem'; import { Menu } from '../generated/entities/Menu'; import { UnitOfMeasurement } from '../generated/entities/UnitOfMeasurement'; import { UnitOfMeasurementConversion } from '../generated/entities/UnitOfMeasurementConversion'; +import { CustomerGroup } from '../generated/entities/CustomerGroup'; +import { CustomerPriceList } from '../generated/entities/CustomerPriceList'; +import { CustomerProductPrice } from '../generated/entities/CustomerProductPrice'; export const entities = [ Address, @@ -46,4 +49,7 @@ export const entities = [ Menu, UnitOfMeasurement, UnitOfMeasurementConversion, + CustomerGroup, + CustomerPriceList, + CustomerProductPrice, ]; diff --git a/apps/api/src/model/lib/sales.invoice.line.service.spec.ts b/apps/api/src/model/lib/sales.invoice.line.service.spec.ts index ea171d09..51297b05 100644 --- a/apps/api/src/model/lib/sales.invoice.line.service.spec.ts +++ b/apps/api/src/model/lib/sales.invoice.line.service.spec.ts @@ -13,6 +13,18 @@ import { SaveArgsValidationServiceKey, } from './save.args.validation.service'; import { UserModel } from './user.model'; +import { CustomerGroupModel } from './customer.group.model'; +import { CustomerPriceListServiceKey } from './customer.price.list.service'; +import { CustomerPriceListModel } from './customer.price.list.model'; +import { CustomerProductPriceModel } from './customer.product.price.model'; +import * as moment from 'moment'; +import * as _ from 'lodash'; + +const customerGroup1: CustomerGroupModel = { + id: 0, + displayName: 'AAA', + customers: [], +}; const customer: CustomerModel = { invoicingEmail: '', @@ -21,6 +33,7 @@ const customer: CustomerModel = { displayName: '', legalName: '', legalAddress: {} as any, + customerGroup: customerGroup1, }; const invoice: SalesInvoiceModel = { @@ -47,13 +60,19 @@ const invoice: SalesInvoiceModel = { totalLinesAccountingSchemeCurrency: 0, }; -const product: ProductModel = { +const product1: ProductModel = { sku: '', - id: 0, + id: 1, + displayName: '', +}; +const product2: ProductModel = { + sku: '', + id: 2, displayName: '', }; const QUANTITY = 10; +const PRODUCT_GROUP_PRICE = 123; const mockTaxService = {}; export const mockTaxServiceProvider = { @@ -70,6 +89,32 @@ export const mockSalesInvoiceServiceProvider = { provide: SalesInvoiceServiceKey, useValue: mockSalesInvoiceService, }; +const customerPriceListModel: CustomerPriceListModel = { + id: 1, + displayName: '', + customerGroup: customerGroup1, + productPrices: [ + { + product: product2, + sellingPrice: PRODUCT_GROUP_PRICE, + } as CustomerProductPriceModel, + ], +}; + +const mockCustomerPriceListService = { + loadDateValidByCustomerGroupAndProduct: ( + transactionalEntityManager, + customerGroup, + product, + ): CustomerPriceListModel[] | null => + product === product2 && customerGroup === customerGroup1 + ? [customerPriceListModel] + : null, +}; +const mockCustomerPriceListServiceProvider = { + provide: CustomerPriceListServiceKey, + useValue: mockCustomerPriceListService, +}; const mockEntityManager = { getRepository: () => ({ @@ -78,10 +123,14 @@ const mockEntityManager = { } as any; (global as any).moduleRef = { - get: token => - token === SalesInvoiceServiceKey - ? mockSalesInvoiceService - : new SaveArgsValidationService(), + get: token => { + switch (token) { + case SalesInvoiceServiceKey: + return mockSalesInvoiceService; + default: + return new SaveArgsValidationService(); + } + }, }; const saveArgsValidationServiceProvider = { @@ -100,6 +149,7 @@ describe('SalesInvoiceLineService', () => { mockProductServiceProvider, mockSalesInvoiceServiceProvider, saveArgsValidationServiceProvider, + mockCustomerPriceListServiceProvider, ], }).compile(); @@ -116,10 +166,91 @@ describe('SalesInvoiceLineService', () => { lineOrder: 0, quantity: QUANTITY, lineTax: {} as any, - product, + product: product1, }, { id: 1 } as UserModel, ); expect(line.linePrice).toBe(2 * QUANTITY); }); + + it('line price is taken from the customer group price list if that exists', async () => { + customerPriceListModel.validTo = null; + customerPriceListModel.validFrom = null; + const line = await service.save( + mockEntityManager, + { + narration: '', + linePrice: 2 * QUANTITY, + invoice, + lineOrder: 0, + quantity: QUANTITY, + lineTax: {} as any, + product: product2, + }, + { id: 1 } as UserModel, + ); + expect(line.linePrice).toBe(PRODUCT_GROUP_PRICE * QUANTITY); + }); + + it('line price is taken from the customer group price list if that exists and is valid', async () => { + customerPriceListModel.validTo = null; + customerPriceListModel.validFrom = moment() + .add(1, 'days') + .toDate(); + const line = await service.save( + mockEntityManager, + { + narration: '', + linePrice: 2 * QUANTITY, + invoice, + lineOrder: 0, + quantity: QUANTITY, + lineTax: {} as any, + product: product2, + }, + { id: 1 } as UserModel, + ); + expect(line.linePrice).not.toBe(PRODUCT_GROUP_PRICE * QUANTITY); + }); + + it('line price is taken from the customer group price list that is with the newest start', async () => { + customerPriceListModel.validTo = null; + customerPriceListModel.validFrom = null; + const customerPriceListModel2: CustomerPriceListModel = _.cloneDeep( + customerPriceListModel, + ); + customerPriceListModel2.validFrom = moment() + .add(-1, 'days') + .toDate(); + customerPriceListModel2.productPrices[0].sellingPrice = + PRODUCT_GROUP_PRICE / 2; + const remember = + mockCustomerPriceListService.loadDateValidByCustomerGroupAndProduct; + mockCustomerPriceListService.loadDateValidByCustomerGroupAndProduct = ( + transactionalEntityManager, + customerGroup, + product, + ): CustomerPriceListModel[] | null => + product === product2 && customerGroup === customerGroup1 + ? [customerPriceListModel, customerPriceListModel2] + : null; + try { + const line = await service.save( + mockEntityManager, + { + narration: '', + linePrice: 2 * QUANTITY, + invoice, + lineOrder: 0, + quantity: QUANTITY, + lineTax: {} as any, + product: product2, + }, + { id: 1 } as UserModel, + ); + expect(line.linePrice).toBe((PRODUCT_GROUP_PRICE / 2) * QUANTITY); + } finally { + mockCustomerPriceListService.loadDateValidByCustomerGroupAndProduct = remember; + } + }); }); diff --git a/apps/api/src/model/lib/sales.invoice.service.ts b/apps/api/src/model/lib/sales.invoice.service.ts index 481f1cfd..c8cfe29a 100644 --- a/apps/api/src/model/lib/sales.invoice.service.ts +++ b/apps/api/src/model/lib/sales.invoice.service.ts @@ -40,6 +40,11 @@ import { SalesInvoiceLine } from '../generated/entities/SalesInvoiceLine'; import { SalesInvoice } from '../generated/entities/SalesInvoice'; import { UserModel } from './user.model'; import { SalesInvoiceMonthlySaveArgsModel } from './sales.invoice.monthly.save.args.model'; +import { + CustomerPriceListService, + CustomerPriceListServiceKey, +} from './customer.price.list.service'; +import { CustomerProductPriceModel } from './customer.product.price.model'; export const SalesInvoiceServiceKey = 'SalesInvoiceService'; @@ -71,6 +76,8 @@ export class SalesInvoiceLineService extends BaseEntityService< constructor( @Inject(TaxServiceKey) public readonly taxService: TaxService, @Inject(ProductServiceKey) public readonly productService: ProductService, + @Inject(CustomerPriceListServiceKey) + public readonly customerPriceListService: CustomerPriceListService, ) { super(); this.salesInvoiceService = getService( @@ -111,8 +118,45 @@ export class SalesInvoiceLineService extends BaseEntityService< args.invoiceId, )); line.invoice = invoice; - await invoice.customer; - line.linePrice = args.linePrice; + + const customer = invoice.customer; + const customerGroup = customer.customerGroup; + const now = new Date(); + const customerPriceListModels = customerGroup + ? ( + await this.customerPriceListService.loadDateValidByCustomerGroupAndProduct( + transactionalEntityManager, + customerGroup, + line.product, + ) + )?.filter( + x => + (!x.validFrom || x.validFrom < now) && + (!x.validTo || x.validTo > now), + ) + : null; + if (customerPriceListModels) { + customerPriceListModels.sort((a, b) => { + if (!a.validFrom || a.validFrom < b.validFrom) { + return 1; + } + if (!b.validFrom || a.validFrom > b.validFrom) { + return -1; + } + return 0; + }); + } + + const customerProductPriceModel: CustomerProductPriceModel = + customerPriceListModels && customerPriceListModels.length > 0 + ? customerPriceListModels[0].productPrices.find( + x => x.product.id === line.product.id, + ) + : null; + + line.linePrice = customerProductPriceModel + ? customerProductPriceModel.sellingPrice * args.quantity + : args.linePrice; line.quantity = args.quantity; line.narration = args.narration; diff --git a/apps/api/src/model/serviceProviders.ts b/apps/api/src/model/serviceProviders.ts index 68e551c6..e3da4864 100644 --- a/apps/api/src/model/serviceProviders.ts +++ b/apps/api/src/model/serviceProviders.ts @@ -51,6 +51,18 @@ import { UnitOfMeasurementService, UnitOfMeasurementServiceKey, } from './lib/unit.of.measurement.service'; +import { + CustomerGroupService, + CustomerGroupServiceKey, +} from './lib/customer.group.service'; +import { + CustomerPriceListService, + CustomerPriceListServiceKey, +} from './lib/customer.price.list.service'; +import { + CustomerProductPriceService, + CustomerProductPriceServiceKey, +} from './lib/customer.product.price.service'; const accountingSchemeServiceProvider = { provide: AccountingSchemeServiceKey, @@ -162,6 +174,21 @@ const unitOfMeasurementServiceProvider = { useClass: UnitOfMeasurementService, }; +const customerGroupServiceProvider = { + provide: CustomerGroupServiceKey, + useClass: CustomerGroupService, +}; + +const customerPriceListServiceProvider = { + provide: CustomerPriceListServiceKey, + useClass: CustomerPriceListService, +}; + +const customerProductPriceServiceProvider = { + provide: CustomerProductPriceServiceKey, + useClass: CustomerProductPriceService, +}; + export const serviceProviders = [ accountingSchemeServiceProvider, addressServiceProvider, @@ -185,4 +212,7 @@ export const serviceProviders = [ userServiceProvider, saveArgsValidationServiceProvider, unitOfMeasurementServiceProvider, + customerGroupServiceProvider, + customerPriceListServiceProvider, + customerProductPriceServiceProvider, ]; diff --git a/clients/admin/src/components/add-customerGroup/AddOrEditCustomerGroup.svelte b/clients/admin/src/components/add-customerGroup/AddOrEditCustomerGroup.svelte new file mode 100644 index 00000000..083c3887 --- /dev/null +++ b/clients/admin/src/components/add-customerGroup/AddOrEditCustomerGroup.svelte @@ -0,0 +1,112 @@ + + +
+
+
+
+
+

+ {$_('page.customerGroups.add.internalInformation')} +

+

+ {$_('page.customerGroups.add.description.internalInformation')} +

+
+
+
+
+
+
+
+ +
+
+
+ {#if customerGroup} +
+
+ {/if} +
+
+
+
+
+
+
diff --git a/clients/admin/src/components/add-customerGroup/addOrEditCustomerGroup.gql b/clients/admin/src/components/add-customerGroup/addOrEditCustomerGroup.gql new file mode 100644 index 00000000..f0a1f94e --- /dev/null +++ b/clients/admin/src/components/add-customerGroup/addOrEditCustomerGroup.gql @@ -0,0 +1,10 @@ +mutation SaveCustomerGroup( + $id: Int + $displayName: String! +) { saveCustomerGroup( args : +{ + id: $id + displayName: $displayName +} + +) { id } } diff --git a/clients/admin/src/components/customerGroups/CustomerGroupList.svelte b/clients/admin/src/components/customerGroups/CustomerGroupList.svelte new file mode 100644 index 00000000..d908f51d --- /dev/null +++ b/clients/admin/src/components/customerGroups/CustomerGroupList.svelte @@ -0,0 +1,32 @@ + + + diff --git a/clients/admin/src/components/customerGroups/CustomerGroupSelect.svelte b/clients/admin/src/components/customerGroups/CustomerGroupSelect.svelte new file mode 100644 index 00000000..a4ab84c0 --- /dev/null +++ b/clients/admin/src/components/customerGroups/CustomerGroupSelect.svelte @@ -0,0 +1,46 @@ + + + +