@hed-hog/finance 0.0.365 → 0.0.370
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dto/create-financial-title.dto.d.ts +1 -0
- package/dist/dto/create-financial-title.dto.d.ts.map +1 -1
- package/dist/dto/create-financial-title.dto.js +6 -0
- package/dist/dto/create-financial-title.dto.js.map +1 -1
- package/dist/finance-data.controller.d.ts +4 -0
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +40 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.d.ts +2 -0
- package/dist/finance-statements.controller.d.ts.map +1 -1
- package/dist/finance.service.d.ts +47 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +156 -109
- package/dist/finance.service.js.map +1 -1
- package/dist/mcp-tools/finance-installments.mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools/finance-installments.mcp-tools.js +12 -2
- package/dist/mcp-tools/finance-installments.mcp-tools.js.map +1 -1
- package/hedhog/frontend/app/_components/bank-account-picker-field.tsx.ejs +3 -0
- package/hedhog/frontend/app/_components/bank-account-sheet.tsx.ejs +902 -0
- package/hedhog/frontend/app/_components/finance-picker.tsx.ejs +95 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +117 -43
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +8 -2
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +114 -43
- package/hedhog/frontend/app/administration/categories/page.tsx.ejs +4 -1
- package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +4 -1
- package/hedhog/frontend/app/administration/currencies/page.tsx.ejs +4 -1
- package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +4 -1
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +6 -893
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +4 -1
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +8 -2
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +4 -1
- package/hedhog/frontend/messages/en.json +14 -1
- package/hedhog/frontend/messages/pt.json +14 -1
- package/hedhog/table/financial_title.yaml +6 -1
- package/package.json +6 -6
- package/src/dto/create-financial-title.dto.ts +5 -0
- package/src/finance.service.ts +187 -134
- package/src/mcp-tools/finance-installments.mcp-tools.ts +12 -2
|
@@ -511,7 +511,10 @@ function NovaContaBancariaSheet({
|
|
|
511
511
|
<Button
|
|
512
512
|
type="button"
|
|
513
513
|
variant="outline"
|
|
514
|
-
onClick={() =>
|
|
514
|
+
onClick={() => {
|
|
515
|
+
clearDraft();
|
|
516
|
+
onOpenChange(false);
|
|
517
|
+
}}
|
|
515
518
|
>
|
|
516
519
|
{tBank('common.cancel')}
|
|
517
520
|
</Button>
|
|
@@ -764,7 +767,10 @@ function ImportarExtratoSheet({
|
|
|
764
767
|
<Button
|
|
765
768
|
type="button"
|
|
766
769
|
variant="outline"
|
|
767
|
-
onClick={() =>
|
|
770
|
+
onClick={() => {
|
|
771
|
+
clearDraft();
|
|
772
|
+
handleOpenChange(false);
|
|
773
|
+
}}
|
|
768
774
|
>
|
|
769
775
|
{t('common.cancel')}
|
|
770
776
|
</Button>
|
|
@@ -162,7 +162,8 @@
|
|
|
162
162
|
"cancel": "Cancel",
|
|
163
163
|
"save": "Save",
|
|
164
164
|
"createCategoryAria": "Create new category",
|
|
165
|
-
"createCostCenterAria": "Create new cost center"
|
|
165
|
+
"createCostCenterAria": "Create new cost center",
|
|
166
|
+
"createBankAccountAria": "Create new bank account"
|
|
166
167
|
},
|
|
167
168
|
"categorySheet": {
|
|
168
169
|
"title": "New category",
|
|
@@ -255,6 +256,12 @@
|
|
|
255
256
|
"quarterly": "Quarterly",
|
|
256
257
|
"semiannual": "Semi-annual",
|
|
257
258
|
"annual": "Annual"
|
|
259
|
+
},
|
|
260
|
+
"documentNumberModeLabel": "Document numbering",
|
|
261
|
+
"documentNumberModes": {
|
|
262
|
+
"same": "Same number on all",
|
|
263
|
+
"sequence": "Number in sequence (1/N)",
|
|
264
|
+
"none": "No number"
|
|
258
265
|
}
|
|
259
266
|
},
|
|
260
267
|
"newTitle": {
|
|
@@ -767,6 +774,12 @@
|
|
|
767
774
|
"quarterly": "Quarterly",
|
|
768
775
|
"semiannual": "Semi-annual",
|
|
769
776
|
"annual": "Annual"
|
|
777
|
+
},
|
|
778
|
+
"documentNumberModeLabel": "Document numbering",
|
|
779
|
+
"documentNumberModes": {
|
|
780
|
+
"same": "Same number on all",
|
|
781
|
+
"sequence": "Number in sequence (1/N)",
|
|
782
|
+
"none": "No number"
|
|
770
783
|
}
|
|
771
784
|
},
|
|
772
785
|
"newTitle": {
|
|
@@ -147,7 +147,8 @@
|
|
|
147
147
|
"cancel": "Cancelar",
|
|
148
148
|
"save": "Salvar",
|
|
149
149
|
"createCategoryAria": "Criar nova categoria",
|
|
150
|
-
"createCostCenterAria": "Criar novo centro de custo"
|
|
150
|
+
"createCostCenterAria": "Criar novo centro de custo",
|
|
151
|
+
"createBankAccountAria": "Criar nova conta bancária"
|
|
151
152
|
},
|
|
152
153
|
"categorySheet": {
|
|
153
154
|
"title": "Nova categoria",
|
|
@@ -240,6 +241,12 @@
|
|
|
240
241
|
"quarterly": "Trimestral",
|
|
241
242
|
"semiannual": "Semestral",
|
|
242
243
|
"annual": "Anual"
|
|
244
|
+
},
|
|
245
|
+
"documentNumberModeLabel": "Numeração do documento",
|
|
246
|
+
"documentNumberModes": {
|
|
247
|
+
"same": "Mesmo número em todos",
|
|
248
|
+
"sequence": "Numerar em sequência (1/N)",
|
|
249
|
+
"none": "Sem número"
|
|
243
250
|
}
|
|
244
251
|
},
|
|
245
252
|
"newTitle": {
|
|
@@ -752,6 +759,12 @@
|
|
|
752
759
|
"quarterly": "Trimestral",
|
|
753
760
|
"semiannual": "Semestral",
|
|
754
761
|
"annual": "Anual"
|
|
762
|
+
},
|
|
763
|
+
"documentNumberModeLabel": "Numeração do documento",
|
|
764
|
+
"documentNumberModes": {
|
|
765
|
+
"same": "Mesmo número em todos",
|
|
766
|
+
"sequence": "Numerar em sequência (1/N)",
|
|
767
|
+
"none": "Sem número"
|
|
755
768
|
}
|
|
756
769
|
},
|
|
757
770
|
"newTitle": {
|
|
@@ -38,6 +38,10 @@ columns:
|
|
|
38
38
|
- name: recurrence_end_date
|
|
39
39
|
type: date
|
|
40
40
|
isNullable: true
|
|
41
|
+
- name: recurrence_group_id
|
|
42
|
+
type: varchar
|
|
43
|
+
length: 36
|
|
44
|
+
isNullable: true
|
|
41
45
|
- name: finance_category_id
|
|
42
46
|
type: fk
|
|
43
47
|
isNullable: true
|
|
@@ -60,4 +64,5 @@ columns:
|
|
|
60
64
|
indices:
|
|
61
65
|
- columns: [title_type, status]
|
|
62
66
|
- columns: [person_id]
|
|
63
|
-
- columns: [competence_date]
|
|
67
|
+
- columns: [competence_date]
|
|
68
|
+
- columns: [recurrence_group_id]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.370",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"@nestjs/core": "^11",
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
|
-
"@hed-hog/api": "0.0.8",
|
|
13
|
-
"@hed-hog/api-pagination": "0.0.7",
|
|
14
12
|
"@hed-hog/api-locale": "0.0.14",
|
|
13
|
+
"@hed-hog/api": "0.0.8",
|
|
15
14
|
"@hed-hog/api-prisma": "0.0.6",
|
|
16
|
-
"@hed-hog/
|
|
17
|
-
"@hed-hog/
|
|
15
|
+
"@hed-hog/core": "0.0.370",
|
|
16
|
+
"@hed-hog/api-pagination": "0.0.7",
|
|
17
|
+
"@hed-hog/tag": "0.0.370",
|
|
18
18
|
"@hed-hog/api-types": "0.0.1",
|
|
19
|
-
"@hed-hog/
|
|
19
|
+
"@hed-hog/crm": "0.0.370"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
@@ -28,6 +28,11 @@ export class CreateRecurrenceRuleDto {
|
|
|
28
28
|
@Min(1)
|
|
29
29
|
@Max(600)
|
|
30
30
|
max_occurrences?: number;
|
|
31
|
+
|
|
32
|
+
@IsOptional()
|
|
33
|
+
@IsString()
|
|
34
|
+
@IsIn(['same', 'sequence', 'none'])
|
|
35
|
+
document_number_mode?: string;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export class CreateFinancialInstallmentDto {
|
package/src/finance.service.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
Logger,
|
|
17
17
|
NotFoundException,
|
|
18
18
|
} from '@nestjs/common';
|
|
19
|
-
import { createHash } from 'node:crypto';
|
|
19
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
20
20
|
import { readFile } from 'node:fs/promises';
|
|
21
21
|
import { CreateBankAccountDto } from './dto/create-bank-account.dto';
|
|
22
22
|
import { CreateBankReconciliationDto } from './dto/create-bank-reconciliation.dto';
|
|
@@ -5138,18 +5138,59 @@ export class FinanceService {
|
|
|
5138
5138
|
interest_cents?: number;
|
|
5139
5139
|
penalty_cents?: number;
|
|
5140
5140
|
};
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5141
|
+
type TitleSpec = {
|
|
5142
|
+
documentNumber: string | null;
|
|
5143
|
+
recurrenceGroupId: string | null;
|
|
5144
|
+
totalAmountCents: number;
|
|
5145
|
+
installments: NormalizedInstallment[];
|
|
5146
|
+
};
|
|
5147
|
+
|
|
5148
|
+
// Recurring titles are NOT installment titles: each occurrence becomes an
|
|
5149
|
+
// independent title (with a single installment), all linked by a shared
|
|
5150
|
+
// recurrence_group_id. Installment titles remain a single title with many
|
|
5151
|
+
// installments.
|
|
5152
|
+
let titleSpecs: TitleSpec[];
|
|
5153
|
+
if (isRecurring) {
|
|
5154
|
+
const occurrences = this.buildRecurrenceInstallments(
|
|
5155
|
+
data.due_date,
|
|
5156
|
+
rule!.frequency,
|
|
5157
|
+
this.toCents(data.total_amount),
|
|
5158
|
+
rule!.end_date,
|
|
5159
|
+
rule!.max_occurrences,
|
|
5160
|
+
);
|
|
5161
|
+
const recurrenceGroupId = randomUUID();
|
|
5162
|
+
const baseDocument = data.document_number?.trim() || null;
|
|
5163
|
+
const documentMode = rule!.document_number_mode || 'same';
|
|
5164
|
+
titleSpecs = occurrences.map((occurrence, index) => ({
|
|
5165
|
+
documentNumber: this.resolveRecurrenceDocumentNumber(
|
|
5166
|
+
baseDocument,
|
|
5167
|
+
documentMode,
|
|
5168
|
+
index + 1,
|
|
5169
|
+
occurrences.length,
|
|
5170
|
+
),
|
|
5171
|
+
recurrenceGroupId,
|
|
5172
|
+
totalAmountCents: occurrence.amount_cents,
|
|
5173
|
+
installments: [
|
|
5174
|
+
{
|
|
5175
|
+
installment_number: 1,
|
|
5176
|
+
due_date: occurrence.due_date,
|
|
5177
|
+
amount_cents: occurrence.amount_cents,
|
|
5178
|
+
},
|
|
5179
|
+
],
|
|
5180
|
+
}));
|
|
5181
|
+
} else {
|
|
5182
|
+
titleSpecs = [
|
|
5183
|
+
{
|
|
5184
|
+
documentNumber: data.document_number?.trim() || null,
|
|
5185
|
+
recurrenceGroupId: null,
|
|
5186
|
+
totalAmountCents: this.toCents(data.total_amount),
|
|
5187
|
+
installments: this.normalizeAndValidateInstallments(data, locale),
|
|
5188
|
+
},
|
|
5189
|
+
];
|
|
5190
|
+
}
|
|
5150
5191
|
|
|
5151
|
-
const hasPaidInstallments =
|
|
5152
|
-
(installment) => installment.paid,
|
|
5192
|
+
const hasPaidInstallments = titleSpecs.some((spec) =>
|
|
5193
|
+
spec.installments.some((installment) => installment.paid),
|
|
5153
5194
|
);
|
|
5154
5195
|
|
|
5155
5196
|
const createResult = await this.prisma.$transaction(async (tx) => {
|
|
@@ -5233,158 +5274,157 @@ export class FinanceService {
|
|
|
5233
5274
|
tx,
|
|
5234
5275
|
data.competence_date
|
|
5235
5276
|
? this.parseLocalDate(data.competence_date)
|
|
5236
|
-
: this.parseLocalDate(installments[0].due_date),
|
|
5277
|
+
: this.parseLocalDate(titleSpecs[0].installments[0].due_date),
|
|
5237
5278
|
'create title',
|
|
5238
5279
|
);
|
|
5239
5280
|
|
|
5240
|
-
const
|
|
5241
|
-
? this.toCents(data.total_amount) * installments.length
|
|
5242
|
-
: this.toCents(data.total_amount);
|
|
5243
|
-
|
|
5244
|
-
const title = await tx.financial_title.create({
|
|
5245
|
-
data: {
|
|
5246
|
-
person_id: data.person_id,
|
|
5247
|
-
title_type: titleType,
|
|
5248
|
-
status: 'draft',
|
|
5249
|
-
document_number: data.document_number?.trim() || null,
|
|
5250
|
-
description: data.description,
|
|
5251
|
-
competence_date: data.competence_date
|
|
5252
|
-
? this.parseLocalDate(data.competence_date)
|
|
5253
|
-
: null,
|
|
5254
|
-
issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
|
|
5255
|
-
total_amount_cents: totalAmountCents,
|
|
5256
|
-
is_recurring: isRecurring,
|
|
5257
|
-
recurrence_frequency: rule?.frequency ?? null,
|
|
5258
|
-
recurrence_end_date: rule?.end_date ? this.parseLocalDate(rule.end_date) : null,
|
|
5259
|
-
finance_category_id: data.finance_category_id,
|
|
5260
|
-
created_by_user_id: userId,
|
|
5261
|
-
},
|
|
5262
|
-
});
|
|
5263
|
-
|
|
5264
|
-
if (attachmentFileIds.length > 0) {
|
|
5265
|
-
await tx.financial_title_attachment.createMany({
|
|
5266
|
-
data: attachmentFileIds.map((fileId) => ({
|
|
5267
|
-
title_id: title.id,
|
|
5268
|
-
file_id: fileId,
|
|
5269
|
-
uploaded_by_user_id: userId,
|
|
5270
|
-
})),
|
|
5271
|
-
});
|
|
5272
|
-
}
|
|
5273
|
-
|
|
5281
|
+
const createdTitleIds: number[] = [];
|
|
5274
5282
|
const appliedSettlements: Array<{
|
|
5275
5283
|
settlementId: number;
|
|
5276
5284
|
installmentId: number;
|
|
5277
5285
|
openAmountCentsBefore: number;
|
|
5278
5286
|
openAmountCentsAfter: number;
|
|
5287
|
+
titleId: number;
|
|
5279
5288
|
}> = [];
|
|
5280
5289
|
|
|
5281
|
-
for (
|
|
5282
|
-
const
|
|
5283
|
-
const amountCents = installment.amount_cents;
|
|
5284
|
-
|
|
5285
|
-
const createdInstallment = await tx.financial_installment.create({
|
|
5290
|
+
for (const spec of titleSpecs) {
|
|
5291
|
+
const title = await tx.financial_title.create({
|
|
5286
5292
|
data: {
|
|
5287
|
-
|
|
5288
|
-
|
|
5293
|
+
person_id: data.person_id,
|
|
5294
|
+
title_type: titleType,
|
|
5295
|
+
status: 'draft',
|
|
5296
|
+
document_number: spec.documentNumber,
|
|
5297
|
+
description: data.description,
|
|
5289
5298
|
competence_date: data.competence_date
|
|
5290
5299
|
? this.parseLocalDate(data.competence_date)
|
|
5291
|
-
:
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
notes: data.description,
|
|
5300
|
+
: null,
|
|
5301
|
+
issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
|
|
5302
|
+
total_amount_cents: spec.totalAmountCents,
|
|
5303
|
+
is_recurring: isRecurring,
|
|
5304
|
+
recurrence_frequency: rule?.frequency ?? null,
|
|
5305
|
+
recurrence_end_date: rule?.end_date ? this.parseLocalDate(rule.end_date) : null,
|
|
5306
|
+
recurrence_group_id: spec.recurrenceGroupId,
|
|
5307
|
+
finance_category_id: data.finance_category_id,
|
|
5308
|
+
created_by_user_id: userId,
|
|
5301
5309
|
},
|
|
5302
5310
|
});
|
|
5303
5311
|
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5312
|
+
createdTitleIds.push(title.id);
|
|
5313
|
+
|
|
5314
|
+
if (attachmentFileIds.length > 0) {
|
|
5315
|
+
await tx.financial_title_attachment.createMany({
|
|
5316
|
+
data: attachmentFileIds.map((fileId) => ({
|
|
5317
|
+
title_id: title.id,
|
|
5318
|
+
file_id: fileId,
|
|
5319
|
+
uploaded_by_user_id: userId,
|
|
5320
|
+
})),
|
|
5311
5321
|
});
|
|
5312
5322
|
}
|
|
5313
5323
|
|
|
5314
|
-
|
|
5315
|
-
const
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5324
|
+
for (const installment of spec.installments) {
|
|
5325
|
+
const amountCents = installment.amount_cents;
|
|
5326
|
+
|
|
5327
|
+
const createdInstallment = await tx.financial_installment.create({
|
|
5328
|
+
data: {
|
|
5329
|
+
title_id: title.id,
|
|
5330
|
+
installment_number: installment.installment_number,
|
|
5331
|
+
competence_date: data.competence_date
|
|
5332
|
+
? this.parseLocalDate(data.competence_date)
|
|
5333
|
+
: this.parseLocalDate(installment.due_date),
|
|
5334
|
+
due_date: this.parseLocalDate(installment.due_date),
|
|
5335
|
+
amount_cents: amountCents,
|
|
5336
|
+
open_amount_cents: amountCents,
|
|
5337
|
+
status: this.resolveInstallmentStatus(
|
|
5338
|
+
amountCents,
|
|
5339
|
+
amountCents,
|
|
5340
|
+
this.parseLocalDate(installment.due_date),
|
|
5341
|
+
),
|
|
5342
|
+
notes: data.description,
|
|
5343
|
+
},
|
|
5329
5344
|
});
|
|
5330
5345
|
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5346
|
+
if (data.cost_center_id) {
|
|
5347
|
+
await tx.installment_allocation.create({
|
|
5348
|
+
data: {
|
|
5349
|
+
installment_id: createdInstallment.id,
|
|
5350
|
+
cost_center_id: data.cost_center_id,
|
|
5351
|
+
allocated_amount_cents: amountCents,
|
|
5352
|
+
},
|
|
5353
|
+
});
|
|
5354
|
+
}
|
|
5334
5355
|
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5356
|
+
if (installment.paid) {
|
|
5357
|
+
const appliedSettlement = await this.applyInstallmentSettlement(tx, {
|
|
5358
|
+
title: { id: title.id, person_id: data.person_id },
|
|
5359
|
+
titleType,
|
|
5360
|
+
installmentId: createdInstallment.id,
|
|
5361
|
+
amountCents: installment.paid_amount_cents ?? amountCents,
|
|
5362
|
+
settledAt: this.parseLocalDate(
|
|
5363
|
+
installment.paid_at || installment.due_date,
|
|
5364
|
+
),
|
|
5365
|
+
bankAccountId: data.bank_account_id ?? null,
|
|
5366
|
+
paymentChannel: data.payment_channel,
|
|
5367
|
+
discountCents: installment.discount_cents,
|
|
5368
|
+
interestCents: installment.interest_cents,
|
|
5369
|
+
penaltyCents: installment.penalty_cents,
|
|
5370
|
+
userId,
|
|
5371
|
+
});
|
|
5372
|
+
|
|
5373
|
+
appliedSettlements.push({ ...appliedSettlement, titleId: title.id });
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5339
5376
|
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
summary: `Created ${titleType} title ${title.id} in ${finalStatus}`,
|
|
5346
|
-
afterData: JSON.stringify({
|
|
5347
|
-
status: finalStatus,
|
|
5348
|
-
total_amount_cents: this.toCents(data.total_amount),
|
|
5349
|
-
}),
|
|
5350
|
-
});
|
|
5377
|
+
const titleSettlements = appliedSettlements.filter((s) => s.titleId === title.id);
|
|
5378
|
+
const finalStatus =
|
|
5379
|
+
titleSettlements.length > 0
|
|
5380
|
+
? await this.recalculateTitleStatus(tx, title.id)
|
|
5381
|
+
: 'draft';
|
|
5351
5382
|
|
|
5352
|
-
for (const appliedSettlement of appliedSettlements) {
|
|
5353
5383
|
await this.createAuditLog(tx, {
|
|
5354
|
-
action: '
|
|
5384
|
+
action: 'CREATE_TITLE',
|
|
5355
5385
|
entityTable: 'financial_title',
|
|
5356
5386
|
entityId: String(title.id),
|
|
5357
5387
|
actorUserId: userId,
|
|
5358
|
-
summary: `
|
|
5359
|
-
beforeData: JSON.stringify({
|
|
5360
|
-
title_status: 'draft',
|
|
5361
|
-
installment_open_amount_cents:
|
|
5362
|
-
appliedSettlement.openAmountCentsBefore,
|
|
5363
|
-
}),
|
|
5388
|
+
summary: `Created ${titleType} title ${title.id} in ${finalStatus}`,
|
|
5364
5389
|
afterData: JSON.stringify({
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
appliedSettlement.openAmountCentsAfter,
|
|
5368
|
-
settlement_id: appliedSettlement.settlementId,
|
|
5369
|
-
bank_reconciliation_id: null,
|
|
5390
|
+
status: finalStatus,
|
|
5391
|
+
total_amount_cents: spec.totalAmountCents,
|
|
5370
5392
|
}),
|
|
5371
5393
|
});
|
|
5394
|
+
|
|
5395
|
+
for (const appliedSettlement of titleSettlements) {
|
|
5396
|
+
await this.createAuditLog(tx, {
|
|
5397
|
+
action: 'SETTLE_INSTALLMENT',
|
|
5398
|
+
entityTable: 'financial_title',
|
|
5399
|
+
entityId: String(title.id),
|
|
5400
|
+
actorUserId: userId,
|
|
5401
|
+
summary: `Settled installment ${appliedSettlement.installmentId} of title ${title.id}`,
|
|
5402
|
+
beforeData: JSON.stringify({
|
|
5403
|
+
title_status: 'draft',
|
|
5404
|
+
installment_open_amount_cents: appliedSettlement.openAmountCentsBefore,
|
|
5405
|
+
}),
|
|
5406
|
+
afterData: JSON.stringify({
|
|
5407
|
+
title_status: finalStatus,
|
|
5408
|
+
installment_open_amount_cents: appliedSettlement.openAmountCentsAfter,
|
|
5409
|
+
settlement_id: appliedSettlement.settlementId,
|
|
5410
|
+
bank_reconciliation_id: null,
|
|
5411
|
+
}),
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5372
5414
|
}
|
|
5373
5415
|
|
|
5374
5416
|
return {
|
|
5375
|
-
|
|
5376
|
-
settlementIds: appliedSettlements.map(
|
|
5377
|
-
(appliedSettlement) => appliedSettlement.settlementId,
|
|
5378
|
-
),
|
|
5417
|
+
titleIds: createdTitleIds,
|
|
5418
|
+
settlementIds: appliedSettlements.map((s) => s.settlementId),
|
|
5379
5419
|
};
|
|
5380
5420
|
});
|
|
5381
5421
|
|
|
5382
5422
|
const createdTitle = await this.getTitleById(
|
|
5383
|
-
createResult.
|
|
5423
|
+
createResult.titleIds[0],
|
|
5384
5424
|
titleType,
|
|
5385
5425
|
locale,
|
|
5386
5426
|
);
|
|
5387
|
-
this.financeRealtime.publish({ domain: 'installment', type: 'created', entityId: createResult.
|
|
5427
|
+
this.financeRealtime.publish({ domain: 'installment', type: 'created', entityId: createResult.titleIds[0] });
|
|
5388
5428
|
for (const settlementId of createResult.settlementIds) {
|
|
5389
5429
|
this.financeRealtime.publish({ domain: 'settlement', type: 'settled', entityId: settlementId });
|
|
5390
5430
|
}
|
|
@@ -5406,14 +5446,16 @@ export class FinanceService {
|
|
|
5406
5446
|
}
|
|
5407
5447
|
|
|
5408
5448
|
const isRecurring = Boolean(rule);
|
|
5449
|
+
// When editing a recurring title we only update this single occurrence
|
|
5450
|
+
// (one installment). Re-building the full series is out of scope for edits.
|
|
5409
5451
|
const installments = isRecurring
|
|
5410
|
-
?
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5452
|
+
? [
|
|
5453
|
+
{
|
|
5454
|
+
installment_number: 1,
|
|
5455
|
+
due_date: data.due_date,
|
|
5456
|
+
amount_cents: this.toCents(data.total_amount),
|
|
5457
|
+
},
|
|
5458
|
+
]
|
|
5417
5459
|
: this.normalizeAndValidateInstallments(data, locale);
|
|
5418
5460
|
|
|
5419
5461
|
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
@@ -5537,9 +5579,7 @@ export class FinanceService {
|
|
|
5537
5579
|
}
|
|
5538
5580
|
}
|
|
5539
5581
|
|
|
5540
|
-
const totalAmountCents =
|
|
5541
|
-
? this.toCents(data.total_amount) * installments.length
|
|
5542
|
-
: this.toCents(data.total_amount);
|
|
5582
|
+
const totalAmountCents = this.toCents(data.total_amount);
|
|
5543
5583
|
|
|
5544
5584
|
await tx.financial_title.update({
|
|
5545
5585
|
where: { id: title.id },
|
|
@@ -5555,7 +5595,8 @@ export class FinanceService {
|
|
|
5555
5595
|
is_recurring: isRecurring,
|
|
5556
5596
|
recurrence_frequency: rule?.frequency ?? null,
|
|
5557
5597
|
recurrence_end_date: rule?.end_date ? this.parseLocalDate(rule.end_date) : null,
|
|
5558
|
-
|
|
5598
|
+
// Preserve recurrence_group_id — editing a single occurrence must not
|
|
5599
|
+
// break the link to the rest of the series.
|
|
5559
5600
|
},
|
|
5560
5601
|
});
|
|
5561
5602
|
|
|
@@ -5703,6 +5744,17 @@ export class FinanceService {
|
|
|
5703
5744
|
return installments;
|
|
5704
5745
|
}
|
|
5705
5746
|
|
|
5747
|
+
private resolveRecurrenceDocumentNumber(
|
|
5748
|
+
base: string | null,
|
|
5749
|
+
mode: string,
|
|
5750
|
+
index: number,
|
|
5751
|
+
total: number,
|
|
5752
|
+
): string | null {
|
|
5753
|
+
if (mode === 'none') return null;
|
|
5754
|
+
if (mode === 'sequence') return base ? `${base} ${index}/${total}` : `${index}/${total}`;
|
|
5755
|
+
return base; // 'same'
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5706
5758
|
private normalizeAndValidateInstallments(
|
|
5707
5759
|
data: CreateFinancialTitleDto,
|
|
5708
5760
|
locale: string,
|
|
@@ -7251,6 +7303,7 @@ export class FinanceService {
|
|
|
7251
7303
|
recurrenceEndDate: title.recurrence_end_date
|
|
7252
7304
|
? title.recurrence_end_date.toISOString().slice(0, 10)
|
|
7253
7305
|
: null,
|
|
7306
|
+
recurrenceGroupId: title.recurrence_group_id ?? null,
|
|
7254
7307
|
...(title.title_type === 'payable'
|
|
7255
7308
|
? {
|
|
7256
7309
|
fornecedorId: String(title.person_id),
|
|
@@ -90,7 +90,7 @@ export class FinanceInstallmentsMcpTools {
|
|
|
90
90
|
},
|
|
91
91
|
recurrence_rule: {
|
|
92
92
|
type: 'object',
|
|
93
|
-
description: 'Recurrence rule. When provided,
|
|
93
|
+
description: 'Recurrence rule. When provided, each occurrence becomes an independent title (one installment each) linked by a recurrence_group_id.',
|
|
94
94
|
properties: {
|
|
95
95
|
frequency: {
|
|
96
96
|
type: 'string',
|
|
@@ -99,6 +99,11 @@ export class FinanceInstallmentsMcpTools {
|
|
|
99
99
|
},
|
|
100
100
|
end_date: { type: 'string', description: 'End date (YYYY-MM-DD) — stop generating after this date' },
|
|
101
101
|
max_occurrences: { type: 'number', description: 'Maximum number of repetitions' },
|
|
102
|
+
document_number_mode: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
enum: ['same', 'sequence', 'none'],
|
|
105
|
+
description: 'How to set document_number on each title: same = copy as-is, sequence = append i/N suffix, none = leave blank (default: same)',
|
|
106
|
+
},
|
|
102
107
|
},
|
|
103
108
|
required: ['frequency'],
|
|
104
109
|
},
|
|
@@ -358,7 +363,7 @@ export class FinanceInstallmentsMcpTools {
|
|
|
358
363
|
},
|
|
359
364
|
recurrence_rule: {
|
|
360
365
|
type: 'object',
|
|
361
|
-
description: 'Recurrence rule. When provided,
|
|
366
|
+
description: 'Recurrence rule. When provided, each occurrence becomes an independent title (one installment each) linked by a recurrence_group_id.',
|
|
362
367
|
properties: {
|
|
363
368
|
frequency: {
|
|
364
369
|
type: 'string',
|
|
@@ -367,6 +372,11 @@ export class FinanceInstallmentsMcpTools {
|
|
|
367
372
|
},
|
|
368
373
|
end_date: { type: 'string', description: 'End date (YYYY-MM-DD) — stop generating after this date' },
|
|
369
374
|
max_occurrences: { type: 'number', description: 'Maximum number of repetitions' },
|
|
375
|
+
document_number_mode: {
|
|
376
|
+
type: 'string',
|
|
377
|
+
enum: ['same', 'sequence', 'none'],
|
|
378
|
+
description: 'How to set document_number on each title: same = copy as-is, sequence = append i/N suffix, none = leave blank (default: same)',
|
|
379
|
+
},
|
|
370
380
|
},
|
|
371
381
|
required: ['frequency'],
|
|
372
382
|
},
|