From 595920fe1c9e11bb974fa9d0b3eb81d0153b2106 Mon Sep 17 00:00:00 2001 From: "anatoly.shipitz" Date: Wed, 13 Aug 2025 14:06:04 +0200 Subject: [PATCH] Enhance Weekly Financial Reports with Project Name and Formatting Adjustments - Added 'name' field to the project schema to ensure project identification in financial reports. - Updated `FinAppRepository` to include project names in the data retrieval process. - Removed unnecessary group total hours from report formatting to streamline output. - Adjusted formatting in `WeeklyFinancialReportFormatter` for improved readability and consistency. - Enhanced sorting logic in `WeeklyFinancialReportRepository` to sort groups by name alphabetically. These changes improve the clarity and organization of weekly financial reports, providing better insights into project performance. --- .../fetchFinancialAppData.ts | 1 + .../src/services/FinApp/FinAppRepository.ts | 2 +- .../main/src/services/FinApp/FinAppSchemas.ts | 1 + .../WeeklyFinancialReportFormatter.ts | 39 +++++++++--------- .../WeeklyFinancialReportRepository.ts | 40 +++++++++++-------- 5 files changed, 44 insertions(+), 39 deletions(-) diff --git a/workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts b/workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts index b35ccb43..8624aa85 100644 --- a/workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts +++ b/workers/main/src/activities/weeklyFinancialReports/fetchFinancialAppData.ts @@ -42,6 +42,7 @@ export const fetchFinancialAppData = async ( ? effectiveRevenue[project.quick_books_id]?.totalAmount || 0 : 0, })), + effectiveRevenue, }); return { fileLink: filename }; diff --git a/workers/main/src/services/FinApp/FinAppRepository.ts b/workers/main/src/services/FinApp/FinAppRepository.ts index 538fd793..fc65dc5e 100644 --- a/workers/main/src/services/FinApp/FinAppRepository.ts +++ b/workers/main/src/services/FinApp/FinAppRepository.ts @@ -21,7 +21,7 @@ export class FinAppRepository implements IFinAppRepository { try { return await ProjectModel.find( { redmine_id: { $in: redmineIds } }, - { 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 }, + { 'name': 1, 'redmine_id': 1, 'quick_books_id': 1, 'history.rate': 1 }, ).lean(); } catch (error) { throw new FinAppRepositoryError( diff --git a/workers/main/src/services/FinApp/FinAppSchemas.ts b/workers/main/src/services/FinApp/FinAppSchemas.ts index 36e24a4a..f03d2dc1 100644 --- a/workers/main/src/services/FinApp/FinAppSchemas.ts +++ b/workers/main/src/services/FinApp/FinAppSchemas.ts @@ -24,6 +24,7 @@ export const EmployeeModel = mongoose.model( // Project schema: represents a project with Redmine and QuickBooks IDs, and a history of rates export const projectSchema = new mongoose.Schema({ + name: { type: String, required: true }, redmine_id: { type: Number, required: true, index: true }, quick_books_id: Number, history: historySchema, diff --git a/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportFormatter.ts b/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportFormatter.ts index 7d2ae4fe..c2c14090 100644 --- a/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportFormatter.ts +++ b/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportFormatter.ts @@ -15,7 +15,6 @@ export interface FormatSummaryInput { export interface FormatDetailInput { groupName: string; - groupTotalHours: number; currentQuarter: string; groupTotalRevenue: number; groupTotalCogs: number; @@ -32,7 +31,6 @@ const spacer = ' '.repeat(4); export class WeeklyFinancialReportFormatter { static formatDetail = ({ groupName, - groupTotalHours, currentQuarter, groupTotalRevenue, groupTotalCogs, @@ -43,15 +41,15 @@ export class WeeklyFinancialReportFormatter { effectiveMargin, effectiveMarginality, }: FormatDetailInput) => - `*${groupName}* (${groupTotalHours}h)\n` + - `${spacer}*Period*: ${currentQuarter}\n` + - `${spacer}*Revenue*: ${formatCurrency(groupTotalRevenue)}\n` + - `${spacer}*COGS*: ${formatCurrency(groupTotalCogs)}\n` + - `${spacer}*Margin*: ${formatCurrency(marginAmount)}\n` + - `${spacer}*Marginality*: ${marginalityPercent.toFixed(0)}%\n` + - `${spacer}*Effective Revenue*: ${formatCurrency(effectiveRevenue)}\n` + - `${spacer}*Effective Margin*: ${formatCurrency(effectiveMargin)}\n` + - `${spacer}*Effective Marginality*: ${indicator} ${effectiveMarginality.toFixed(0)}%\n\n`; + `*${groupName}*\n` + + `${spacer}period: ${currentQuarter}\n` + + `${spacer}revenue: ${formatCurrency(groupTotalRevenue)}\n` + + `${spacer}COGS: ${formatCurrency(groupTotalCogs)}\n` + + `${spacer}margin: ${formatCurrency(marginAmount)}\n` + + `${spacer}marginality: ${marginalityPercent.toFixed(0)}%\n` + + `${spacer}effective revenue: ${formatCurrency(effectiveRevenue)}\n` + + `${spacer}effective margin: ${formatCurrency(effectiveMargin)}\n` + + `${spacer}effective marginality: ${indicator} ${effectiveMarginality.toFixed(0)}%\n\n\n`; static formatSummary = ({ reportTitle, @@ -62,24 +60,24 @@ export class WeeklyFinancialReportFormatter { let summary = `${reportTitle}\n`; if (highGroups.length) { - summary += '________________________________\n'; + summary += '\n_______________________\n\n\n'; summary += `:arrowup: *Marginality is ${HIGH_MARGINALITY_THRESHOLD}% or higher*:\n`; - summary += `${spacer}${highGroups.join(`\n${spacer}`)}\n`; + summary += `${spacer}${spacer}${highGroups.join(`\n${spacer}${spacer}`)}\n`; } if (mediumGroups.length) { - summary += '__________________________________\n'; + summary += '\n_______________________\n\n\n'; summary += ` :large_yellow_circle: *Marginality is between ${MEDIUM_MARGINALITY_THRESHOLD}-${HIGH_MARGINALITY_THRESHOLD}%*:\n`; - summary += `${spacer}${mediumGroups.join(`\n${spacer}`)}\n`; + summary += `${spacer}${spacer}${mediumGroups.join(`\n${spacer}${spacer}`)}\n`; } if (lowGroups.length) { - summary += '__________________________________\n'; + summary += '\n_______________________\n\n\n'; summary += `:arrowdown: *Marginality is under ${MEDIUM_MARGINALITY_THRESHOLD}%*:\n`; - summary += `${spacer}${lowGroups.join(`\n${spacer}`)}\n`; + summary += `${spacer}${spacer}${lowGroups.join(`\n${spacer}${spacer}`)}\n`; } - summary += ' -------------------------------------------\n'; + summary += '\n_______________________\n\n\n'; summary += 'The specific figures will be available in the thread'; return summary; @@ -97,12 +95,11 @@ export class WeeklyFinancialReportFormatter { }; } - static formatFooter = (totalHours: number) => { + static formatFooter = () => { const { startDate, endDate } = this.calculateDateWindow(); return ( - `\n*Total hours*: ${totalHours}h\n\n` + - '*Notes:*\n' + + '\n*Notes:*\n' + '1. *Contract Type* is not implemented\n' + `2. *Effective Revenue* calculated for the last ${qboConfig.effectiveRevenueMonths} months (${startDate} - ${endDate})\n` + '3. *Dept Tech* hours are not implemented\n\n' + diff --git a/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts b/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts index cd2039a1..ee1c35d3 100644 --- a/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts +++ b/workers/main/src/services/WeeklyFinancialReport/WeeklyFinancialReportRepository.ts @@ -16,7 +16,6 @@ import { WeeklyFinancialReportFormatter } from './WeeklyFinancialReportFormatter interface GroupData { groupName: string; - groupTotalHours: number; groupTotalRevenue: number; groupTotalCogs: number; effectiveRevenue: number; @@ -26,8 +25,7 @@ interface GroupData { } export class WeeklyFinancialReportRepository - implements IWeeklyFinancialReportRepository -{ + implements IWeeklyFinancialReportRepository { async generateReport({ targetUnits, employees, @@ -45,12 +43,13 @@ export class WeeklyFinancialReportRepository this.sortGroupData(groupData); - const { reportDetails: initialDetails, totalReportedHours } = - this.formatGroupDetails(groupData, currentQuarter); + const { reportDetails: initialDetails } = this.formatGroupDetails( + groupData, + currentQuarter, + ); const reportDetails = - initialDetails + - WeeklyFinancialReportFormatter.formatFooter(totalReportedHours); + initialDetails + WeeklyFinancialReportFormatter.formatFooter(); const { highGroups, mediumGroups, lowGroups } = this.createSortedGroups(groupData); @@ -101,7 +100,7 @@ export class WeeklyFinancialReportRepository }; // Sort by marginality level (High -> Medium -> Low), - // then within each level by descending effectiveMarginality + // then within each level by groupName alphabetically groupData.sort((a, b) => { const levelComparison = levelOrder[b.marginality.level] - levelOrder[a.marginality.level]; @@ -110,7 +109,8 @@ export class WeeklyFinancialReportRepository return levelComparison; } - return b.effectiveMarginality - a.effectiveMarginality; + // Sort by groupName alphabetically within each level + return a.groupName.localeCompare(b.groupName); }); } @@ -120,7 +120,7 @@ export class WeeklyFinancialReportRepository employees: Employee[], projects: Project[], ): GroupData { - const { groupUnits, groupTotalHours } = GroupAggregator.aggregateGroup( + const { groupUnits } = GroupAggregator.aggregateGroup( targetUnits, targetUnit.group_id, ); @@ -138,7 +138,6 @@ export class WeeklyFinancialReportRepository return { groupName: targetUnit.group_name, - groupTotalHours, groupTotalRevenue, groupTotalCogs, effectiveRevenue, @@ -153,7 +152,7 @@ export class WeeklyFinancialReportRepository const mediumGroups: string[] = []; const lowGroups: string[] = []; - // Groups are already sorted by effectiveMarginality, so just distribute them by categories + // Distribute groups by marginality level for (const group of groupData) { this.pushGroupByMarginality(group.marginality.level, group.groupName, { highMarginalityGroups: highGroups, @@ -162,17 +161,20 @@ export class WeeklyFinancialReportRepository }); } + // Sort each group by groupName alphabetically + highGroups.sort((a, b) => a.localeCompare(b)); + mediumGroups.sort((a, b) => a.localeCompare(b)); + lowGroups.sort((a, b) => a.localeCompare(b)); + return { highGroups, mediumGroups, lowGroups }; } private formatGroupDetails(groupData: GroupData[], currentQuarter: string) { let reportDetails = ''; - let totalReportedHours = 0; for (const group of groupData) { reportDetails += WeeklyFinancialReportFormatter.formatDetail({ groupName: group.groupName, - groupTotalHours: group.groupTotalHours, currentQuarter, groupTotalRevenue: group.groupTotalRevenue, groupTotalCogs: group.groupTotalCogs, @@ -183,10 +185,9 @@ export class WeeklyFinancialReportRepository effectiveMargin: group.effectiveMargin, effectiveMarginality: group.effectiveMarginality, }); - totalReportedHours += group.groupTotalHours; } - return { reportDetails, totalReportedHours }; + return { reportDetails }; } private pushGroupByMarginality( @@ -244,6 +245,7 @@ export class WeeklyFinancialReportRepository let groupTotalCogs = 0; let groupTotalRevenue = 0; let effectiveRevenue = 0; + const processedProjects = new Set(); // Отслеживаем обработанные проекты for (const unit of groupUnits) { const employee = employees.find((e) => e.redmine_id === unit.user_id); @@ -254,7 +256,11 @@ export class WeeklyFinancialReportRepository groupTotalCogs += employeeRate * unit.total_hours; groupTotalRevenue += projectRate * unit.total_hours; - effectiveRevenue += projectRate * unit.total_hours; // For now, same as total revenue + + if (project && !processedProjects.has(project.redmine_id)) { + effectiveRevenue += project.effectiveRevenue || 0; + processedProjects.add(project.redmine_id); + } } const effectiveMargin = effectiveRevenue - groupTotalCogs;