feat: profit report
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 6s

This commit is contained in:
Methapon2001 2025-03-04 15:19:44 +07:00
parent 54dba11dc4
commit b4df1a5d4e

View file

@ -1,3 +1,4 @@
import config from "../config.json";
import {
Customer,
CustomerBranch,
@ -11,9 +12,12 @@ import prisma from "../db";
import { createPermCondition } from "../services/permission";
import { RequestWithUser } from "../interfaces/user";
import { PaymentStatus } from "../generated/kysely/types";
import { precisionRound } from "../utils/arithmetic";
const permissionCondCompany = createPermCondition((_) => true);
const VAT_DEFAULT = config.vat;
@Route("/api/v1/report")
@Security("keycloak")
@Tags("Report")
@ -127,30 +131,73 @@ export class StatsController extends Controller {
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.product.findMany({
include: {
quotationProductServiceList: {
include: {
quotation: true,
await prisma.$transaction(async (tx) => {
const record = await tx.product.findMany({
include: {
quotationProductServiceList: {
include: {
quotation: true,
},
},
},
},
select: {
id: true,
code: true,
name: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
quotationProductServiceList: {
where: {
quotation: {
quotationStatus: {
select: {
id: true,
code: true,
name: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
quotationProductServiceList: {
where: {
quotation: {
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
},
},
},
},
},
},
where: {
quotationProductServiceList: {
some: {
quotation: { createdAt: { gte: startDate, lte: endDate } },
},
},
productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } },
},
orderBy: {
quotationProductServiceList: { _count: "desc" },
},
take: limit,
});
const doing = await tx.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
requestWork: {
some: {
stepStatus: {
some: {
workStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
RequestWorkStatus.Pending,
RequestWorkStatus.InProgress,
RequestWorkStatus.Validate,
RequestWorkStatus.Completed,
RequestWorkStatus.Ended,
],
},
},
@ -158,72 +205,31 @@ export class StatsController extends Controller {
},
},
},
},
where: {
quotationProductServiceList: {
some: {
quotation: { createdAt: { gte: startDate, lte: endDate } },
});
const order = await tx.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
},
productGroup: { registeredBranch: { OR: permissionCondCompany(req.user) } },
},
orderBy: {
quotationProductServiceList: { _count: "desc" },
},
take: limit,
});
});
const doing = await prisma.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
requestWork: {
some: {
stepStatus: {
some: {
workStatus: {
in: [
RequestWorkStatus.Pending,
RequestWorkStatus.InProgress,
RequestWorkStatus.Validate,
RequestWorkStatus.Completed,
RequestWorkStatus.Ended,
],
},
},
},
},
},
},
return record.map((v) => ({
document: "product",
code: v.code,
name: v.name,
sale: v._count.quotationProductServiceList,
did: doing.find((item) => item.productId === v.id)?._count || 0,
order: order.find((item) => item.productId === v.id)?._count || 0,
createdAt: v.createdAt,
updatedAt: v.updatedAt,
}));
});
const order = await prisma.quotationProductServiceList.groupBy({
_count: true,
by: "productId",
where: {
quotation: {
createdAt: { gte: startDate, lte: endDate },
registeredBranch: { OR: permissionCondCompany(req.user) },
},
productId: { in: record.map((v) => v.id) },
},
});
return record.map((v) => ({
document: "product",
code: v.code,
name: v.name,
sale: v._count.quotationProductServiceList,
did: doing.find((item) => item.productId === v.id)?._count || 0,
order: order.find((item) => item.productId === v.id)?._count || 0,
createdAt: v.createdAt,
updatedAt: v.updatedAt,
}));
}
@Get("sale")
@ -306,4 +312,72 @@ export class StatsController extends Controller {
{ byProductGroup: [], bySale: [], byCustomer: [] },
);
}
@Get("profit")
async profit(
@Request() req: RequestWithUser,
@Query() limit?: number,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const record = await prisma.quotationProductServiceList.findMany({
include: {
product: {
select: {
agentPrice: true,
agentPriceCalcVat: true,
agentPriceVatIncluded: true,
serviceCharge: true,
serviceChargeCalcVat: true,
serviceChargeVatIncluded: true,
price: true,
calcVat: true,
vatIncluded: true,
},
},
quotation: {
select: {
agentPrice: true,
},
},
},
where: {
quotation: {
quotationStatus: {
in: [
QuotationStatus.PaymentInProcess,
QuotationStatus.PaymentSuccess,
QuotationStatus.ProcessComplete,
],
},
registeredBranch: {
OR: permissionCondCompany(req.user),
},
},
},
take: limit,
});
let income = 0;
let expenses = 0;
let netProfit = 0;
record.forEach((v) => {
const originalPrice = v.product.serviceCharge;
const productExpenses = precisionRound(
originalPrice + (v.product.serviceChargeVatIncluded ? 0 : originalPrice * VAT_DEFAULT),
);
const finalPrice = v.pricePerUnit * v.amount * (1 + config.vat);
income += finalPrice;
expenses += productExpenses;
netProfit += finalPrice - productExpenses;
});
return {
income: precisionRound(income),
expenses: precisionRound(expenses),
netProfit: precisionRound(netProfit),
};
}
}