Compare commits

..

85 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
6991ec7780 Merge pull request #997 from bigcapitalhq/fix/model-timestamps-missing-columns
fix(models): remove timestamps from models where tables lack createdAt/updatedAt columns
2026-02-26 05:59:49 +02:00
Ahmed Bouhuolia
ad252d2e4a fix(models): remove timestamps from models where tables lack createdAt/updatedAt columns
Add withDateSessionMixin for proper timestamp handling and fix models
to return empty timestamps array when database tables don't have
created_at/updated_at columns. This prevents ORM insert/update errors.

Models updated:
- Branch, Role, RolePermission, ViewColumn, ViewRole
- InventoryAdjustment, InventoryAdjustmentEntry, InventoryTransactionMeta
- BillLandedCostEntry, CreditNote, CreditNoteAppliedInvoice, RefundCreditNote
- PaymentReceived, SaleInvoice, SaleReceipt, Item, ItemEntry
- RefundVendorCredit, VendorCreditAppliedBill
- ItemWarehouseQuantity, Warehouse, WarehouseTransfer, WarehouseTransferEntry
- Setting, TenantMetadataModel, TenantUser

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 05:57:55 +02:00
Ahmed Bouhuolia
12eb8c32dc Merge pull request #996 from bigcapitalhq/fix/account-type-not-selected-banking-edit
fix(webapp): account type not pre-selected when editing from banking page
2026-02-25 22:12:38 +02:00
Ahmed Bouhuolia
ca68918caa fix(webapp): account type not pre-selected when editing from banking page
Change 'id' to 'accountId' in CashflowAccountsGrid to match the
AccountDialogProvider expected payload. The dialog provider expects
'accountId' to fetch account details and populate the form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 22:08:58 +02:00
Ahmed Bouhuolia
fa1acc9773 Merge pull request #995 from bigcapitalhq/fix/date-formats-banking-transactions
fix: use organization date format in banking transactions and financial reports
2026-02-25 20:35:23 +02:00
Ahmed Bouhuolia
558fc29962 fix: use organization date format in banking transactions and reports
- Add OrganizationSettingsModule to BankingTransactionsModule
- Update GetBankAccountTransactions to pass dateFormat from settings
- Add meta support to FinancialSheet base class
- Refactor TransactionsByReference to use IFinancialReportMeta
- Update frontend to use server-provided formatted_date
2026-02-25 20:33:31 +02:00
Ahmed Bouhuolia
8a32e13a79 Merge pull request #994 from bigcapitalhq/feat/account-settings-service
fix(accounts): add account settings service
2026-02-25 19:29:45 +02:00
Ahmed Bouhuolia
d35915b16b feat(accounts): add account settings service
- Add AccountsSettingsService for managing account-related settings
- Update validators, create and edit services to use settings
- Add constants for account configuration
- Update frontend utils and translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 19:27:53 +02:00
Ahmed Bouhuolia
b5d1a2c9d0 Merge pull request #992 from bigcapitalhq/fix/organization-date-formats-and-address-fields
fix(financial-statements): use stored date format settings in all reports
2026-02-25 07:10:41 +02:00
Ahmed Bouhuolia
f5e74f3e88 fix(inventory): update baseCurrency retrieval in InventoryDetailsService
- Replace tenantMetadata.baseCurrency with meta.baseCurrency in InventoryDetailsService to ensure consistent currency usage across reports.
2026-02-25 07:10:09 +02:00
Ahmed Bouhuolia
c83132b867 fix(financial-statements): use stored date format settings in all reports
- Replace hardcoded date formats ('YYYY/MM/DD') in all Meta classes with meta.dateFormat
- Add IFinancialReportMeta interface with baseCurrency and dateFormat fields
- Add DEFAULT_REPORT_META constant with default date format 'YYYY MMM DD'
- Update all sheet classes to accept IFinancialReportMeta parameter
- Update all services to pass dateFormat from meta to sheet constructors
- Fix customer/vendor transactions date formatting in table rows
- Add TenancyModule to SalesTaxLiabilityModule for dependency injection
- Make dateFormat optional in JournalSheet and GeneralLedger query types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 07:05:31 +02:00
Ahmed Bouhuolia
88ff5db0f3 Merge pull request #989 from bigcapitalhq/fix/organization-date-formats-and-address-fields
fix(organization): align date formats and fix address field naming
2026-02-24 22:45:10 +02:00
Ahmed Bouhuolia
f35e85c3d2 fix(organization): align date formats and fix address field naming
- Fix date format mismatch between Miscellaneous and Organization constants
- Fix default date format casing ('DD MMM yyyy' -> 'DD MMM YYYY')
- Rename address fields from address_1/address_2 to address1/address2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 22:42:29 +02:00
Ahmed Bouhuolia
29decf9c5a Merge pull request #987 from bigcapitalhq/feat/contact-address-country-fields
fix: country and address fields of customer and vendor forms
2026-02-24 20:55:23 +02:00
Ahmed Bouhuolia
f149ff43b4 feat(contacts): add country field to customer and vendor address forms
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 20:53:14 +02:00
Ahmed Bouhuolia
fb05af8c00 Merge pull request #979 from yk-a11y/fix/credit-note-apply-invoice-validation
fix: validate credit note per-entry amount against each invoice due amount
2026-02-24 02:55:47 +02:00
Ahmed Bouhuolia
688b1bfb56 fix(server): add invoices Map for validateInvoicesRemainingAmount 2026-02-24 02:52:28 +02:00
Ahmed Bouhuolia
0f8147daff Merge branch 'develop' into fix/credit-note-apply-invoice-validation 2026-02-24 02:42:23 +02:00
Ahmed Bouhuolia
96b24d4fb9 Merge pull request #982 from bigcapitalhq/fix/credit-notes-applied-invoice-delete
fix: Add DELETE endpoint for credit notes applied invoices
2026-02-24 02:17:37 +02:00
Ahmed Bouhuolia
2a87103bc8 fix: Add DELETE endpoint for credit notes applied invoices
- Add missing DELETE /credit-notes/applied-invoices/:id endpoint
- Fix CreditNotesApplyInvoice controller to use correct service methods
- Add missing GetCreditNoteAssociatedInvoicesToApply endpoint
- Add proper DTO for ApplyCreditNoteToInvoices
- Update frontend creditNote hook to use correct API paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 02:15:32 +02:00
Ahmed Bouhuolia
238b60144f Merge pull request #981 from bigcapitalhq/fix/register-verify-dark-mode
fix: add dark mode support to email confirmation UI
2026-02-23 01:10:50 +02:00
Ahmed Bouhuolia
fcee85e358 fix: add dark mode support to email confirmation UI
Refactored RegisterVerify component to use xstyled for styling
with proper dark mode color values instead of hardcoded light theme colors.
2026-02-23 00:48:32 +02:00
Ahmed Bouhuolia
64a10053e3 Merge pull request #980 from bigcapitalhq/fix-signup-verification
fix: signup confirmation
2026-02-23 00:39:45 +02:00
Ahmed Bouhuolia
ce9f2a238f fix: signup confirmation 2026-02-23 00:37:56 +02:00
Yong ke Weng
75b98c39d8 fix: validate credit note per-entry amount against each invoice due amount
The `validateInvoicesRemainingAmount` method was incorrectly comparing the
total credit amount (sum of all entries) against each individual invoice's
due amount. This caused valid credit note applications to be rejected when
applying to multiple invoices where the total exceeded any single invoice's
due amount.

Changed the validation to compare each invoice's due amount against only the
specific entry amount being applied to that invoice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 09:50:39 -05:00
Ahmed Bouhuolia
80e545072d Merge pull request #975 from bigcapitalhq/fix/localize-financial-reports
fix: localize hardcoded strings in financial reports
2026-02-18 22:09:05 +02:00
Ahmed Bouhuolia
de3d4698ea fix: localize hardcoded strings in financial reports
- Fix cash_flow_statement.net_cash_investing not being localized
- Add translation keys for Account name, Total, sheet name, From/To dates
- Create contact_summary_balance.json for Customer/Vendor Balance Summary
- Create trial_balance_sheet.json for Trial Balance Sheet columns
- Create inventory_item_details.json for Inventory Item Details
- Create transactions_by_contact.json for Transactions by Contact
- Fix hardcoded strings in TrialBalanceSheetTable column labels
- Fix hardcoded 'Total' in CustomerBalanceSummary and VendorBalanceSummary
- Fix hardcoded column headers in InventoryItemDetailsTable
- Fix hardcoded Opening/Closing balance strings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:07:00 +02:00
Ahmed Bouhuolia
171091e0e0 Merge pull request #972 from bigcapitalhq/fix/ahmedbouhuolia/invite-user-service
fix: invite user service issues
2026-02-18 12:33:57 +02:00
Ahmed Bouhuolia
78032d7bfc fix: invite user service issues 2026-02-18 12:32:04 +02:00
Ahmed Bouhuolia
06b8a836c5 Merge pull request #967 from bigcapitalhq/fix/ahmedbouhuolia/baseurl-config-key
fix: correct config key for base URL in email services
2026-02-18 01:31:08 +02:00
Ahmed Bouhuolia
37fa9f9bc6 fix: correct config key for base URL in email services 2026-02-18 01:28:48 +02:00
Ahmed Bouhuolia
17deeb18e3 Merge pull request #965 from bigcapitalhq/fix/ahmedbouhuolia/cashflow-transaction-type-consistency
fix: correct cash flow transaction type naming inconsistencies
2026-02-16 23:05:22 +02:00
Ahmed Bouhuolia
8416b45f4e fix: correct cash flow transaction type naming inconsistencies
- Fix typo ONWERS_DRAWING -> OWNERS_DRAWING in server constants
- Change OwnerDrawing -> owner_drawing for consistency in webapp
- Fix typo TRANSACRIONS_TYPE -> TRANSACTIONS_TYPE
- Fix typo OnwersDrawing -> OwnerDrawing
- Add missing Icon and FDateInput imports
- Add dark mode styling for BranchRowDivider

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-02-16 23:02:38 +02:00
Ahmed Bouhuolia
3cc5aab80e Merge pull request #963 from bigcapitalhq/fix/ahmedbouhuolia/mail-queue-cleanup
fix: correct queue name, add missing await, and clean up constants
2026-02-16 22:27:08 +02:00
Ahmed Bouhuolia
93711a7bf4 fix: correct queue name, add missing await, and clean up constants 2026-02-16 22:23:41 +02:00
Ahmed Bouhuolia
43066c4f1f Merge pull request #962 from bigcapitalhq/feature/ahmedbouhuolia/add-permission-guards-to-credit-controllers
fix(server): add permission guards to credit note and vendor credit controllers
2026-02-16 20:07:36 +02:00
Ahmed Bouhuolia
d5402b6a9b feat: add permission guards to credit note and vendor credit controllers
Add AuthorizationGuard and PermissionGuard to the following controllers:
- CreditNoteRefundsController
- CreditNotesApplyInvoiceController
- VendorCreditApplyBillsController
- VendorCreditsRefundController

Add @RequirePermission decorators with appropriate actions:
- View action for GET endpoints
- Edit action for POST/DELETE endpoints
- Refund action for refund-related operations

Also fixes AuthorizationGuard to use userId from clsService instead of
user.id from request for consistency with the abilities cache.
2026-02-16 20:04:48 +02:00
Ahmed Bouhuolia
174aec78ca fix(webapp): send mail preview style 2026-02-16 17:45:04 +02:00
Ahmed Bouhuolia
7162e948dd Merge pull request #961 from bigcapitalhq/fix/trial-balance-sheet-filtering
fix: correct trial balance sheet filtering logic
2026-02-16 15:34:33 +02:00
Ahmed Bouhuolia
8ad1be1d52 fix: correct trial balance sheet filtering logic
- Include child account transactions when filtering parent accounts
- Fix order of operations in accounts section mapping
- Ensure accounts with child transactions are not incorrectly filtered out

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:27:33 +02:00
Ahmed Bouhuolia
a96ced10db Merge pull request #958 from bigcapitalhq/premissions-guard
fix(server): permissions guard for read and write endpoints
2026-02-15 22:58:01 +02:00
Ahmed Bouhuolia
2d39e38578 fix(server): premissions guard for read and write endpoints 2026-02-15 22:55:10 +02:00
Ahmed Bouhuolia
af80afcf59 Merge pull request #955 from bigcapitalhq/fix/user-invite-email
fix: user invite email not sending and null variables
2026-02-14 00:34:08 +02:00
Ahmed Bouhuolia
66cb0521e5 fix: user invite email not sending and null variables
- Add missing BullModule queue registration and BullBoardModule to UsersModule
- Add invitingUser to event payloads to track who sent the invite
- Fix incorrect global variable in SendInviteUsersMailMessage (__views_dir -> __images_dirname)
- Use invitingUser as fromUser instead of invited user in email
- Update processors to use BullMQ pattern

Fixes issues:
1. Email not sending due to missing queue/processor registration
2. Null variables in email (firstName/lastName) because fromUser was the invited user
3. Image attachment failing due to wrong path
2026-02-14 00:31:28 +02:00
Ahmed Bouhuolia
9204b76346 Merge pull request #950 from bigcapitalhq/fix-tax-rates
fix(server): use `DrawerActionsBar` instead of `DashboardActionsBar` in drawer components
2026-02-12 23:21:28 +02:00
Ahmed Bouhuolia
36cbb1eef5 fix: use DrawerActionsBar instead of DashboardActionsBar in drawer components
Replace DashboardActionsBar with DrawerActionsBar in all drawer action bars
for consistency with the design system:
- ContactDetailActionsBar
- CustomerDetailsActionsBar
- VendorCreditDetailActionsBar
- TaxRateDetailsContentActionsBar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:19:16 +02:00
Ahmed Bouhuolia
441e27581b Merge pull request #949 from bigcapitalhq/fix-tax-rates
fix: tax rates API and UI improvements
2026-02-12 20:08:49 +02:00
Ahmed Bouhuolia
e0d9a56a29 fix: tax rates API and UI improvements
- Add @ToNumber() decorator to rate field for proper validation
- Fix getTaxRates to return { data: taxRates } response
- Fix useTaxRate URL typo and response handling
- Fix activate/inactivate endpoint methods and paths
- Apply TEXT_MUTED class to description and compound tax
- Add dark mode support for rate number display
2026-02-12 20:06:49 +02:00
Ahmed Bouhuolia
5a017104ce Merge pull request #948 from bigcapitalhq/fix/abouolia/rerecognize-transactions-on-rule-edit
fix: paper template scrollable area
2026-02-12 15:02:12 +02:00
Ahmed Bouhuolia
25ca620836 fix: add consistent Box wrapper to paper template forms in customize components 2026-02-12 14:59:55 +02:00
Ahmed Bouhuolia
5a3655e093 Merge pull request #944 from bigcapitalhq/fix/abouolia/rerecognize-transactions-on-rule-edit
fix(server): re-recognize transactions when bank rule is edited
2026-02-11 23:18:18 +02:00
Ahmed Bouhuolia
49c2777587 fix: re-recognize transactions when bank rule is edited (closes #809) 2026-02-11 23:15:20 +02:00
Ahmed Bouhuolia
a5680c08c2 Merge pull request #943 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): add missing S3_FORCE_PATH_STYLE environment variable
2026-02-11 19:36:52 +02:00
Ahmed Bouhuolia
d909dad1bf fix: add missing S3_FORCE_PATH_STYLE environment variable
The S3 module was referencing config.forcePathStyle but the value
was never being read from the environment. This adds the missing
forcePathStyle configuration to the S3 config.

Closes #940

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:35:21 +02:00
Ahmed Bouhuolia
f32cc752ef Merge pull request #942 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): allow 'payee' field in bank rule conditions validation
2026-02-11 19:13:35 +02:00
Ahmed Bouhuolia
a7f98201cc fix: allow 'payee' field in bank rule conditions validation
The BankRuleConditionDto validation only allowed 'description' and 'amount'
fields, but the frontend also sends 'payee' as a valid condition field.
This caused a 400 Bad Request error when creating rules with payee conditions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:11:58 +02:00
Ahmed Bouhuolia
a1d0fc3f0a Merge pull request #941 from bigcapitalhq/fix/ahmedbouhuolia/phone-validation-formatted-numbers
fix(webapp): allow formatted phone numbers in customer and vendor forms
2026-02-11 18:39:52 +02:00
Ahmed Bouhuolia
11575cfb96 fix(webapp): allow formatted phone numbers in customer and vendor forms 2026-02-11 18:37:39 +02:00
Ahmed Bouhuolia
a2160c0595 Merge pull request #939 from bigcapitalhq/fix/ahmedbouhuolia/docker-healthcheck-endpoint
fix(server): fix Docker healthcheck endpoint
2026-02-10 00:25:25 +02:00
Ahmed Bouhuolia
956a9b58dd fix(server): register SystemDatabaseController and add PublicRoute decorator
- Register SystemDatabaseController in SystemDatabaseModule to expose /api/system_db endpoint
- Add PublicRoute decorator to bypass authentication for healthcheck endpoint
- Update ping() method to return { status: 'ok' } with HTTP 200

This fixes the Docker healthcheck that was failing with 404 Not Found errors.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 00:22:40 +02:00
Ahmed Bouhuolia
acb701d618 Merge pull request #938 from bigcapitalhq/fix/universal-search-api-endpoints
refactor: update UniversalSearch components with TypeScript and TextStatus
2026-02-09 19:55:37 +02:00
Ahmed Bouhuolia
09ff72d302 fix: add TypeScript types to If component 2026-02-09 19:52:17 +02:00
Ahmed Bouhuolia
7375512fec refactor: update UniversalSearch components with TypeScript and TextStatus 2026-02-09 19:26:26 +02:00
Ahmed Bouhuolia
77e65389a4 Merge pull request #937 from bigcapitalhq/fix/universal-search-api-endpoints
fix: universal search API endpoint errors
2026-02-09 13:42:53 +02:00
Ahmed Bouhuolia
1972861c97 fix(server): add missing searchRoles to Item model
Add searchRoles static property to enable searching items by name and code.
This fixes the 500 Internal Server Error when searching items via
/api/items?search_keyword=...

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:34 +02:00
Ahmed Bouhuolia
c47acdee03 fix(webapp): correct API endpoint URLs for universal search
Update resource URL mappings to match backend NestJS controller routes:
- /sales/invoices -> /sale-invoices
- /sales/estimates -> /sale-estimates
- /sales/receipts -> /sale-receipts
- /purchases/bills -> /bills
- /sales/payment_receives -> /payments-received
- /purchases/bill_payments -> /bill-payments
- /sales/credit_notes -> /credit-notes
- /purchases/vendor-credit -> /vendor-credits

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:18 +02:00
Ahmed Bouhuolia
8689962bf3 Merge pull request #935 from bigcapitalhq/feat/abouolia/add-throttle-organization-build-job
feat: expand rate limiting of getting org build job endpoint
2026-02-09 13:23:49 +02:00
Ahmed Bouhuolia
3258159474 feat: add rate limiting to organization build job endpoint
Add @Throttle decorator to GET /build/:buildJobId endpoint to limit
to 300 requests per minute to prevent abuse.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 09:39:55 +02:00
Ahmed Bouhuolia
36bfa573ad 🐛 fix(manual-journal): fix race condition in form submission handlers
Fix the order of setSubmitPayload and submitForm calls in all six
button handlers to prevent race condition where submitForm reads
stale state before setSubmitPayload updates it.

Changes:
- handleSubmitPublishBtnClick
- handleSubmitPublishAndNewBtnClick
- handleSubmitPublishContinueEditingBtnClick
- handleSubmitDraftBtnClick
- handleSubmitDraftAndNewBtnClick
- handleSubmitDraftContinueEditingBtnClick

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-07 16:58:57 +02:00
Ahmed Bouhuolia
2c05785096 Merge pull request #934 from bigcapitalhq/fix/branches-activation-bills
fix(server): branches activation not marking bills and payments with primary branch
2026-02-05 16:06:39 +02:00
Ahmed Bouhuolia
6af4be9c6c fix(server): branches activation not marking bills and payments with primary branch
When activating the multi-branches feature, existing bills, vendor credits,
and bill payments were not being marked with the default primary branch.

Changes:
- Add missing @Inject decorators to BillActivateBranches, VendorCreditActivateBranches,
  and BillPaymentsActivateBranches services
- Create BillBranchesActivateSubscriber to listen to onActivated event
- Create VendorCreditBranchesActivateSubscriber to listen to onActivated event
- Register BillPaymentsActivateBranches and PaymentMadeActivateBranchesSubscriber
  in BranchesModule
- Add branch object to BillResponseDto for API responses
- Add branch to BillTransformer includeAttributes

Fixes: #935
2026-02-05 16:03:57 +02:00
Ahmed Bouhuolia
8def1d31d2 Merge pull request #933 from bigcapitalhq/20260205-151219-7770
feat(webapp): add blurry background to sticky data table cells
2026-02-05 15:29:47 +02:00
Ahmed Bouhuolia
afab02a053 feat(webapp): add blurry background to sticky data table cells
Add backdrop-filter blur effect to sticky column cells in financial reports
to prevent content from showing through during horizontal scrolling.
The effect only applies when rows are not hovered to preserve hover
background interactions.
2026-02-05 15:27:45 +02:00
Ahmed Bouhuolia
8e925c62f2 Merge pull request #932 from bigcapitalhq/20260205-151219-7770
fix(server): balance sheet query validation schema
2026-02-05 15:14:45 +02:00
Ahmed Bouhuolia
1b7d513adf fix(server): balance sheet query validation schema 2026-02-05 15:12:54 +02:00
Ahmed Bouhuolia
7d764fb390 Merge pull request #931 from bigcapitalhq/fix/item-error-handling
fix(items): correct error type handling and add swagger documentation
2026-02-04 21:44:45 +02:00
Ahmed Bouhuolia
c571f50a74 fix(items): correct error type handling and add swagger documentation
- Fix error type mismatch: change 'ITEM.NAME.ALREADY.EXISTS' to 'ITEM_NAME_EXISTS'
- Add ItemErrorType constant with UpperCamelCase keys for better maintainability
- Update all error checks to use the new ItemErrorType constant
- Add ItemErrorResponse.dto.ts with documented error types for swagger
- Add @ApiResponse decorators to document 400 validation errors in swagger
2026-02-04 21:42:39 +02:00
Ahmed Bouhuolia
6549026344 Merge pull request #930 from bigcapitalhq/fix/account-delete-error-handling
fix(webapp): account delete error handling response types
2026-02-04 21:31:38 +02:00
Ahmed Bouhuolia
0963394b04 fix(webapp): account delete error handling response types 2026-02-04 21:27:25 +02:00
Ahmed Bouhuolia
6cab0651fc Merge pull request #927 from bigcapitalhq/feature/20260202223150
fix(webapp): darkmode warehouses list page
2026-02-02 22:36:42 +02:00
Ahmed Bouhuolia
4af537d6dd fix(webapp): darkmode warehouses list page 2026-02-02 22:31:53 +02:00
Ahmed Bouhuolia
34db64612c Merge pull request #926 from bigcapitalhq/20260202-185120-9c84
fix(webapp): constrant not found row color
2026-02-02 18:53:48 +02:00
Ahmed Bouhuolia
10225bbfed fix(webapp): constrant not found row color 2026-02-02 18:51:52 +02:00
Ahmed Bouhuolia
c3a4fe6b37 Merge pull request #924 from bigcapitalhq/20260201-180532-f578
fix(webapp): normalize api path
2026-02-01 18:06:51 +02:00
Ahmed Bouhuolia
02be959461 fix(webapp): normalize api path 2026-02-01 18:05:51 +02:00
290 changed files with 2588 additions and 982 deletions

View File

@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
}));

View File

@@ -9,5 +9,10 @@
"net_cash_financing": "Net cash provided by financing activities",
"cash_beginning_period": "Cash at beginning of period",
"net_cash_increase": "NET CASH INCREASE FOR PERIOD",
"cash_end_period": "CASH AT END OF PERIOD"
"cash_end_period": "CASH AT END OF PERIOD",
"account_name": "Account name",
"total": "Total",
"sheet_name": "Statement of Cash Flow",
"from_date": "From",
"to_date": "To"
}

View File

@@ -0,0 +1,5 @@
{
"account_name": "Account name",
"total": "Total",
"percentage_column": "% of Column"
}

View File

@@ -0,0 +1,14 @@
{
"opening_balance": "Opening balance",
"closing_balance": "Closing balance",
"date": "Date",
"transaction_type": "Transaction type",
"transaction_number": "Transaction #",
"quantity": "Quantity",
"rate": "Rate",
"total": "Total",
"value": "Value",
"profit_margin": "Profit Margin",
"running_quantity": "Running quantity",
"running_value": "Running Value"
}

View File

@@ -0,0 +1,4 @@
{
"opening_balance": "Opening balance",
"closing_balance": "Closing balance"
}

View File

@@ -0,0 +1,6 @@
{
"account": "Account",
"debit": "Debit",
"credit": "Credit",
"total": "Total"
}

View File

@@ -1,5 +1,6 @@
import { QueryBuilder, Model } from 'objection';
import { QueryBuilder, Model, mixin } from 'objection';
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
import { withDateSessionMixin } from './withDateSessionMixin';
interface PaginationResult<M extends Model> {
results: M[];
@@ -69,6 +70,7 @@ export class PaginationQueryBuilder<
dependentRelationNames.forEach((relationName: string) => {
recordQuery.withGraphFetched(relationName);
});
const record = await recordQuery;
const hasRelations = dependentRelationNames.some((name) => {
@@ -97,7 +99,7 @@ export class BaseQueryBuilder<
}
}
export class BaseModel extends Model {
export class BaseModel extends mixin(Model, [withDateSessionMixin]) {
public readonly id: number;
public readonly tableName: string;

View File

@@ -0,0 +1,40 @@
import * as moment from 'moment';
import { Model } from 'objection';
type Constructor<T = {}> = new (...args: any[]) => T;
export const withDateSessionMixin = <T extends Constructor<Model>>(BaseModel: T) => {
return class DateSession extends BaseModel {
constructor(...args: any[]) {
super(...args);
}
get timestamps() {
return [];
}
$beforeUpdate(opt, context) {
const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => {
const key = this.timestamps[1];
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
$beforeInsert(context) {
const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => {
const key = this.timestamps[0];
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
}
}

View File

@@ -9,6 +9,7 @@ import {
ParseIntPipe,
Put,
HttpCode,
UseGuards,
} from '@nestjs/common';
import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto';
@@ -32,6 +33,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { AccountAction } from './Accounts.types';
@Controller('accounts')
@ApiTags('Accounts')
@@ -40,11 +46,13 @@ import {
@ApiExtraModels(GetAccountTransactionResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) { }
@Post('validate-bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({
summary:
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
@@ -67,6 +75,7 @@ export class AccountsController {
@Post('bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
@ApiResponse({
status: 200,
@@ -81,6 +90,7 @@ export class AccountsController {
}
@Post()
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
@ApiOperation({ summary: 'Create an account' })
@ApiResponse({
status: 200,
@@ -91,6 +101,7 @@ export class AccountsController {
}
@Put(':id')
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Edit the given account.' })
@ApiResponse({
status: 200,
@@ -111,6 +122,7 @@ export class AccountsController {
}
@Delete(':id')
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Delete the given account.' })
@ApiResponse({
status: 200,
@@ -129,6 +141,7 @@ export class AccountsController {
@Post(':id/activate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Activate the given account.' })
@ApiResponse({
status: 200,
@@ -147,6 +160,7 @@ export class AccountsController {
@Post(':id/inactivate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Inactivate the given account.' })
@ApiResponse({
status: 200,
@@ -164,6 +178,7 @@ export class AccountsController {
}
@Get('types')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account types.' })
@ApiResponse({
status: 200,
@@ -180,6 +195,7 @@ export class AccountsController {
}
@Get('transactions')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account transactions.' })
@ApiResponse({
status: 200,
@@ -198,6 +214,7 @@ export class AccountsController {
}
@Get(':id')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account details.' })
@ApiResponse({
status: 200,
@@ -216,6 +233,7 @@ export class AccountsController {
}
@Get()
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the accounts.' })
@ApiResponse({
status: 200,

View File

@@ -21,6 +21,7 @@ import { AccountsExportable } from './AccountsExportable.service';
import { AccountsImportable } from './AccountsImportable.service';
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
import { AccountsSettingsService } from './AccountsSettings.service';
const models = [RegisterTenancyModel(BankAccount)];
@@ -29,6 +30,7 @@ const models = [RegisterTenancyModel(BankAccount)];
controllers: [AccountsController],
providers: [
AccountsApplication,
AccountsSettingsService,
CreateAccountService,
TenancyContext,
CommandAccountValidators,
@@ -49,9 +51,10 @@ const models = [RegisterTenancyModel(BankAccount)];
exports: [
AccountRepository,
CreateAccountService,
AccountsSettingsService,
...models,
AccountsExportable,
AccountsImportable
AccountsImportable,
],
})
export class AccountsModule {}

View File

@@ -0,0 +1,33 @@
import { Inject, Injectable } from '@nestjs/common';
import { SettingsStore } from '../Settings/SettingsStore';
import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
export interface IAccountsSettings {
accountCodeRequired: boolean;
accountCodeUnique: boolean;
}
@Injectable()
export class AccountsSettingsService {
constructor(
@Inject(SETTINGS_PROVIDER)
private readonly settingsStore: () => SettingsStore,
) {}
/**
* Retrieves account settings (account code required, account code unique).
*/
public async getAccountsSettings(): Promise<IAccountsSettings> {
const settingsStore = await this.settingsStore();
return {
accountCodeRequired: settingsStore.get(
{ group: 'accounts', key: 'account_code_required' },
false,
),
accountCodeUnique: settingsStore.get(
{ group: 'accounts', key: 'account_code_unique' },
true,
),
};
}
}

View File

@@ -106,6 +106,20 @@ export class CommandAccountValidators {
}
}
/**
* Throws error if account code is missing or blank when required.
* @param {string|undefined} code - Account code.
*/
public validateAccountCodeRequiredOrThrow(code: string | undefined) {
const trimmed = typeof code === 'string' ? code.trim() : '';
if (!trimmed) {
throw new ServiceError(
ERRORS.ACCOUNT_CODE_REQUIRED,
'Account code is required.',
);
}
}
/**
* Validates the account name uniquiness.
* @param {string} accountName - Account name.

View File

@@ -15,6 +15,7 @@ import { events } from '@/common/events/events';
import { CreateAccountDTO } from './CreateAccount.dto';
import { PartialModelObject } from 'objection';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { AccountsSettingsService } from './AccountsSettings.service';
@Injectable()
export class CreateAccountService {
@@ -32,6 +33,7 @@ export class CreateAccountService {
private readonly uow: UnitOfWork,
private readonly validator: CommandAccountValidators,
private readonly tenancyContext: TenancyContext,
private readonly accountsSettings: AccountsSettingsService,
) {}
/**
@@ -43,14 +45,21 @@ export class CreateAccountService {
baseCurrency: string,
params?: CreateAccountParams,
) => {
const { accountCodeRequired, accountCodeUnique } =
await this.accountsSettings.getAccountsSettings();
// Validate account code required when setting is enabled.
if (accountCodeRequired) {
this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code);
}
// Validate the account code uniquiness when setting is enabled.
if (accountCodeUnique && accountDTO.code?.trim()) {
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
}
// Validate account name uniquiness.
if (!params.ignoreUniqueName) {
await this.validator.validateAccountNameUniquiness(accountDTO.name);
}
// Validate the account code uniquiness.
if (accountDTO.code) {
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
}
// Retrieve the account type meta or throw service error if not found.
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);

View File

@@ -7,6 +7,7 @@ import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { EditAccountDTO } from './EditAccount.dto';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { AccountsSettingsService } from './AccountsSettings.service';
@Injectable()
export class EditAccount {
@@ -17,7 +18,8 @@ export class EditAccount {
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
) { }
private readonly accountsSettings: AccountsSettingsService,
) {}
/**
* Authorize the account editing.
@@ -30,6 +32,24 @@ export class EditAccount {
accountDTO: EditAccountDTO,
oldAccount: Account,
) => {
const { accountCodeRequired, accountCodeUnique } =
await this.accountsSettings.getAccountsSettings();
// Validate account code required when setting is enabled.
if (accountCodeRequired) {
this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code);
}
// Validate the account code uniquiness when setting is enabled.
if (
accountCodeUnique &&
accountDTO.code?.trim() &&
accountDTO.code !== oldAccount.code
) {
await this.validator.isAccountCodeUniqueOrThrowError(
accountDTO.code,
oldAccount.id,
);
}
// Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness(
accountDTO.name,
@@ -40,13 +60,6 @@ export class EditAccount {
oldAccount,
accountDTO,
);
// Validate the account code not exists on the storage.
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
await this.validator.isAccountCodeUniqueOrThrowError(
accountDTO.code,
oldAccount.id,
);
}
// Retrieve the parent account of throw not found service error.
if (accountDTO.parentAccountId) {
const parentAccount = await this.validator.getParentAccountOrThrowError(

View File

@@ -3,6 +3,7 @@ export const ERRORS = {
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_CODE_REQUIRED: 'account_code_required',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',

View File

@@ -1,5 +1,5 @@
import { ModuleRef } from '@nestjs/core';
import bluebird from 'bluebird';
import * as bluebird from 'bluebird';
import { Knex } from 'knex';
import {
validateLinkModelEntryExists,
@@ -53,7 +53,8 @@ export class LinkAttachment {
const foundLinkModel = await LinkModel().query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel);
const foundLinks = await this.documentLinkModel().query(trx)
const foundLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef)
.where('modelId', modelId)
.where('documentId', foundFile.id);
@@ -70,7 +71,7 @@ export class LinkAttachment {
/**
* Links the given file keys to the given model type and id.
* @param {string[]} filekeys - File keys.
* @param {string[]} filekeys - File keys.
* @param {string} modelRef - Model reference.
* @param {number} modelId - Model id.
* @param {Knex.Transaction} trx - Knex transaction.

View File

@@ -65,7 +65,7 @@ export class AuthController {
return this.authApp.signUp(signupDto);
}
@Post('/signup/confirm')
@Post('/signup/verify')
@ApiOperation({ summary: 'Confirm user signup' })
@ApiBody({
schema: {

View File

@@ -20,7 +20,7 @@ export class AuthenticationMailMesssages {
* @returns {Mail}
*/
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
const baseURL = this.configService.get('baseURL');
const baseURL = this.configService.get('app.baseUrl');
return new Mail()
.setSubject('Bigcapital - Password Reset')
@@ -54,7 +54,7 @@ export class AuthenticationMailMesssages {
* @returns {Mail}
*/
signupVerificationMail(email: string, fullName: string, token: string) {
const baseURL = this.configService.get('baseURL');
const baseURL = this.configService.get('app.baseUrl');
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
return new Mail()

View File

@@ -7,17 +7,13 @@ import {
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
import { Controller, Get, Post } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
import { TenantAgnosticRoute } from '../Tenancy/TenancyGlobal.guard';
import { AuthenticationApplication } from './AuthApplication.sevice';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard';
@Controller('/auth')
@ApiTags('Auth')
@ApiExcludeController()
@IgnoreTenantSeededRoute()
@IgnoreTenantInitializedRoute()
@TenantAgnosticRoute()
@IgnoreUserVerifiedRoute()
@Throttle({ auth: {} })
export class AuthedController {

View File

@@ -13,7 +13,6 @@ import {
IAuthSignedUpEventPayload,
IAuthSigningUpEventPayload,
} from '../Auth.interfaces';
import { defaultTo } from 'ramda';
import { ERRORS } from '../Auth.constants';
import { hashPassword } from '../Auth.utils';
import { ClsService } from 'nestjs-cls';
@@ -51,10 +50,10 @@ export class AuthSignupService {
const signupConfirmation = this.configService.get('signupConfirmation');
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
const verifiedEnabed = defaultTo(signupConfirmation.enabled, false);
const verifiedEnabed = signupConfirmation.enabled ?? false;
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
const verified = !verifiedEnabed;
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
// Triggers signin up event.

View File

@@ -4,7 +4,6 @@ import { SystemUser } from '@/modules/System/models/SystemUser';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../Auth.constants';
import { events } from '@/common/events/events';
import { ModelObject } from 'objection';
import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';

View File

@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
class BankRuleConditionDto {
@IsNotEmpty()
@IsIn(['description', 'amount'])
@IsIn(['description', 'amount', 'payee'])
field: string;
@IsNotEmpty()

View File

@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
ruleId: number,
transactionsCriteria: any;
transactionsCriteria?: RecognizeTransactionsCriteria;
/**
* When true, first reverts recognized transactions before recognizing again.
* Used when a bank rule is edited to ensure transactions previously recognized
* by lower-priority rules are re-evaluated against the updated rule.
*/
shouldRevert?: boolean;
}

View File

@@ -93,6 +93,10 @@ export class RecognizeTranasctionsService {
q.whereIn('id', rulesIds);
}
q.withGraphFetched('conditions');
// Order by the 'order' field to ensure higher priority rules (lower order values)
// are matched first.
q.orderBy('order', 'asc');
});
const bankRulesByAccountId = transformToMapBy(

View File

@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = {
ruleId: bankRule.id,
shouldRevert: true,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// Re-recognize the transactions based on the new rules.
// Setting shouldRevert to true ensures that transactions previously recognized
// by this or lower-priority rules are re-evaluated against the updated rule.
await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob,
payload,

View File

@@ -3,6 +3,7 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
import {
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
@@ -15,10 +16,12 @@ import {
export class RegonizeTransactionsPrcessor extends WorkerHost {
/**
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
* @param {ClsService} clsService -
*/
constructor(
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
private readonly clsService: ClsService,
) {
super();
@@ -29,12 +32,21 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
*/
@UseCls()
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
const { ruleId, transactionsCriteria } = job.data;
const { ruleId, transactionsCriteria, shouldRevert } = job.data;
this.clsService.set('organizationId', job.data.organizationId);
this.clsService.set('userId', job.data.userId);
try {
// If shouldRevert is true, first revert recognized transactions before re-recognizing.
// This is used when a bank rule is edited to ensure transactions previously recognized
// by lower-priority rules are re-evaluated against the updated rule.
if (shouldRevert) {
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
ruleId,
transactionsCriteria,
);
}
await this.recognizeTranasctionsService.recognizeTransactions(
ruleId,
transactionsCriteria,

View File

@@ -24,6 +24,7 @@ import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BankAccount } from './models/BankAccount';
import { LedgerModule } from '../Ledger/Ledger.module';
import { TenancyModule } from '../Tenancy/Tenancy.module';
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service';
import { GetBankAccountTransactionsRepository } from './queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service';
import { GetUncategorizedTransactions } from './queries/GetUncategorizedTransactions';
@@ -46,6 +47,7 @@ const models = [
LedgerModule,
BranchesModule,
DynamicListModule,
TenancyModule,
...models,
],
controllers: [

View File

@@ -27,7 +27,7 @@ export enum CASHFLOW_DIRECTION {
}
export enum CASHFLOW_TRANSACTION_TYPE {
ONWERS_DRAWING = 'OwnerDrawing',
OWNERS_DRAWING = 'OwnerDrawing',
OWNER_CONTRIBUTION = 'OwnerContribution',
OTHER_INCOME = 'OtherIncome',
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
@@ -36,7 +36,7 @@ export enum CASHFLOW_TRANSACTION_TYPE {
}
export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: {
[`${CASHFLOW_TRANSACTION_TYPE.OWNERS_DRAWING}`]: {
type: 'OwnerDrawing',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EQUITY],

View File

@@ -4,12 +4,14 @@ import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactio
import { GetBankAccountTransactions } from './GetBankAccountTransactions';
import { GetBankTransactionsQueryDto } from '../../dtos/GetBankTranasctionsQuery.dto';
import { I18nService } from 'nestjs-i18n';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class GetBankAccountTransactionsService {
constructor(
private readonly getBankAccountTransactionsRepository: GetBankAccountTransactionsRepository,
private readonly i18nService: I18nService
private readonly i18nService: I18nService,
private readonly tenancyContext: TenancyContext,
) {}
/**
@@ -28,11 +30,16 @@ export class GetBankAccountTransactionsService {
await this.getBankAccountTransactionsRepository.asyncInit();
// Retrieve the tenant metadata to get the date format.
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
const dateFormat = tenantMetadata?.dateFormat;
// Retrieve the computed report.
const report = new GetBankAccountTransactions(
this.getBankAccountTransactionsRepository,
parsedQuery,
this.i18nService
this.i18nService,
dateFormat,
);
const transactions = report.reportData();
const pagination = this.getBankAccountTransactionsRepository.pagination;

View File

@@ -24,17 +24,20 @@ export class GetBankAccountTransactions extends FinancialSheet {
* @param {IAccountTransaction[]} transactions -
* @param {number} openingBalance -
* @param {ICashflowAccountTransactionsQuery} query -
* @param {string} dateFormat - The date format from organization settings.
*/
constructor(
repo: GetBankAccountTransactionsRepository,
query: ICashflowAccountTransactionsQuery,
i18n: I18nService,
dateFormat?: string,
) {
super();
this.repo = repo;
this.query = query;
this.i18n = i18n;
this.dateFormat = dateFormat || this.dateFormat;
this.runningBalance = runningBalance(this.repo.openingBalance);
}
@@ -98,7 +101,7 @@ export class GetBankAccountTransactions extends FinancialSheet {
return {
date: transaction.date,
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
formattedDate: this.getDateFormatted(transaction.date),
withdrawal: transaction.credit,
deposit: transaction.debit,

View File

@@ -13,6 +13,13 @@ export class BillLandedCostEntry extends BaseModel {
return 'bill_located_cost_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -7,6 +7,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import {
@@ -26,12 +27,18 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { IPaymentMadeAction } from './types/BillPayments.types';
@Controller('bill-payments')
@ApiTags('Bill Payments')
@ApiExtraModels(BillPaymentResponseDto)
@ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillPaymentsController {
constructor(
private billPaymentsApplication: BillPaymentsApplication,
@@ -39,12 +46,14 @@ export class BillPaymentsController {
) {}
@Post()
@RequirePermission(IPaymentMadeAction.Create, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Create a new bill payment.' })
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
}
@Delete(':billPaymentId')
@RequirePermission(IPaymentMadeAction.Delete, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Delete the given bill payment.' })
@ApiParam({
name: 'billPaymentId',
@@ -59,6 +68,7 @@ export class BillPaymentsController {
}
@Put(':billPaymentId')
@RequirePermission(IPaymentMadeAction.Edit, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Edit the given bill payment.' })
@ApiParam({
name: 'billPaymentId',
@@ -77,6 +87,7 @@ export class BillPaymentsController {
}
@Get('/new-page/entries')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({
summary:
'Retrieves the payable entries of the new page once vendor be selected.',
@@ -95,6 +106,7 @@ export class BillPaymentsController {
}
@Get(':billPaymentId/bills')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
@ApiParam({
name: 'billPaymentId',
@@ -107,6 +119,7 @@ export class BillPaymentsController {
}
@Get('/:billPaymentId/edit-page')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({
summary: 'Retrieves the edit page of the given bill payment.',
})
@@ -126,6 +139,7 @@ export class BillPaymentsController {
}
@Get(':billPaymentId')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
@ApiResponse({
status: 200,
@@ -145,6 +159,7 @@ export class BillPaymentsController {
}
@Get()
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bill payments list.' })
@ApiResponse({
status: 200,

View File

@@ -17,6 +17,7 @@ import {
Get,
Query,
HttpCode,
UseGuards,
} from '@nestjs/common';
import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types';
@@ -28,6 +29,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { BillAction } from './Bills.types';
@Controller('bills')
@ApiTags('Bills')
@@ -35,10 +41,12 @@ import {
@ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillsController {
constructor(private billsApplication: BillsApplication) { }
@Post('validate-bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({
summary: 'Validate which bills can be deleted and return the results.',
})
@@ -58,6 +66,7 @@ export class BillsController {
}
@Post('bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Deletes multiple bills.' })
@HttpCode(200)
@ApiResponse({
@@ -73,12 +82,14 @@ export class BillsController {
}
@Post()
@RequirePermission(BillAction.Create, AbilitySubject.Bill)
@ApiOperation({ summary: 'Create a new bill.' })
createBill(@Body() billDTO: CreateBillDto) {
return this.billsApplication.createBill(billDTO);
}
@Put(':id')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Edit the given bill.' })
@ApiParam({
name: 'id',
@@ -91,6 +102,7 @@ export class BillsController {
}
@Delete(':id')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Delete the given bill.' })
@ApiParam({
name: 'id',
@@ -103,6 +115,7 @@ export class BillsController {
}
@Get()
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bills.' })
@ApiResponse({
status: 200,
@@ -132,6 +145,7 @@ export class BillsController {
}
@Get(':id/payment-transactions')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({
summary: 'Retrieve the specific bill associated payment transactions.',
})
@@ -146,6 +160,7 @@ export class BillsController {
}
@Get(':id')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bill details.' })
@ApiResponse({
status: 200,
@@ -165,6 +180,7 @@ export class BillsController {
}
@Patch(':id/open')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Open the given bill.' })
@ApiParam({
name: 'id',
@@ -177,6 +193,7 @@ export class BillsController {
}
@Get('due')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the due bills.' })
getDueBills(@Body('vendorId') vendorId?: number) {
return this.billsApplication.getDueBills(vendorId);

View File

@@ -1,6 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto';
import { DiscountType } from '@/common/types/Discount';
export class BillResponseDto {
@@ -89,6 +91,14 @@ export class BillResponseDto {
})
branchId?: number;
@ApiProperty({
description: 'Branch details',
type: () => BranchResponseDto,
required: false,
})
@Type(() => BranchResponseDto)
branch?: BranchResponseDto;
@ApiProperty({
description: 'The ID of the project',
example: 301,

View File

@@ -30,6 +30,7 @@ export class BillTransformer extends Transformer {
'taxes',
'entries',
'attachments',
'branch',
];
};

View File

@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches';
import { BillActivateBranches } from './integrations/Purchases/BillBranchesActivate';
import { VendorCreditActivateBranches } from './integrations/Purchases/VendorCreditBranchesActivate';
import { BillPaymentsActivateBranches } from './integrations/Purchases/PaymentMadeBranchesActivate';
import { BillBranchesActivateSubscriber } from './subscribers/Activate/BillBranchesActivateSubscriber';
import { VendorCreditBranchesActivateSubscriber } from './subscribers/Activate/VendorCreditBranchesActivateSubscriber';
import { PaymentMadeActivateBranchesSubscriber } from './subscribers/Activate/PaymentMadeBranchesActivateSubscriber';
import { FeaturesModule } from '../Features/Features.module';
@Module({
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
ValidateBranchExistance,
ManualJournalBranchesValidator,
CashflowTransactionsActivateBranches,
ExpensesActivateBranches
ExpensesActivateBranches,
BillActivateBranches,
VendorCreditActivateBranches,
BillPaymentsActivateBranches,
BillBranchesActivateSubscriber,
VendorCreditBranchesActivateSubscriber,
PaymentMadeActivateBranchesSubscriber
],
exports: [
BranchesSettingsService,

View File

@@ -1,11 +1,14 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill';
@Injectable()
export class BillActivateBranches {
constructor(private readonly billModel: TenantModelProxy<typeof Bill>) {}
constructor(
@Inject(Bill.name)
private readonly billModel: TenantModelProxy<typeof Bill>,
) {}
/**
* Updates all bills transactions with the primary branch.
@@ -17,7 +20,7 @@ export class BillActivateBranches {
primaryBranchId: number,
trx?: Knex.Transaction,
) => {
// Updates the sale invoice with primary branch.
await Bill.query(trx).update({ branchId: primaryBranchId });
// Updates the bills with primary branch.
await this.billModel().query(trx).update({ branchId: primaryBranchId });
};
}

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BillPaymentsActivateBranches {
constructor(
@Inject(BillPayment.name)
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
) {}

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
@Injectable()
export class VendorCreditActivateBranches {
constructor(
@Inject(VendorCredit.name)
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
) {}

View File

@@ -29,7 +29,7 @@ export class Branch extends BaseModel{
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -0,0 +1,28 @@
import { IBranchesActivatedPayload } from '../../Branches.types';
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
import { BillActivateBranches } from '../../integrations/Purchases/BillBranchesActivate';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class BillBranchesActivateSubscriber {
constructor(
private readonly billActivateBranches: BillActivateBranches,
) { }
/**
* Updates bills transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
@OnEvent(events.branch.onActivated)
async updateBillsWithBranchOnActivated({
primaryBranch,
trx,
}: IBranchesActivatedPayload) {
await this.billActivateBranches.updateBillsWithBranch(
primaryBranch.id,
trx,
);
}
}

View File

@@ -0,0 +1,28 @@
import { IBranchesActivatedPayload } from '../../Branches.types';
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
import { VendorCreditActivateBranches } from '../../integrations/Purchases/VendorCreditBranchesActivate';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class VendorCreditBranchesActivateSubscriber {
constructor(
private readonly vendorCreditActivateBranches: VendorCreditActivateBranches,
) { }
/**
* Updates vendor credits transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
@OnEvent(events.branch.onActivated)
async updateVendorCreditsWithBranchOnActivated({
primaryBranch,
trx,
}: IBranchesActivatedPayload) {
await this.vendorCreditActivateBranches.updateVendorCreditsWithBranch(
primaryBranch.id,
trx,
);
}
}

View File

@@ -10,7 +10,7 @@ export interface IContactAddress {
billingAddressCity: string;
billingAddressCountry: string;
billingAddressEmail: string;
billingAddressZipcode: string;
billingAddressPostcode: string;
billingAddressPhone: string;
billingAddressState: string;
@@ -19,7 +19,7 @@ export interface IContactAddress {
shippingAddressCity: string;
shippingAddressCountry: string;
shippingAddressEmail: string;
shippingAddressZipcode: string;
shippingAddressPostcode: string;
shippingAddressPhone: string;
shippingAddressState: string;
}
@@ -29,7 +29,7 @@ export interface IContactAddressDTO {
billingAddressCity?: string;
billingAddressCountry?: string;
billingAddressEmail?: string;
billingAddressZipcode?: string;
billingAddressPostcode?: string;
billingAddressPhone?: string;
billingAddressState?: string;
@@ -38,7 +38,7 @@ export interface IContactAddressDTO {
shippingAddressCity?: string;
shippingAddressCountry?: string;
shippingAddressEmail?: string;
shippingAddressZipcode?: string;
shippingAddressPostcode?: string;
shippingAddressPhone?: string;
shippingAddressState?: string;
}

View File

@@ -1,20 +1,35 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
import { RefundCreditNote } from './models/RefundCreditNote';
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
@Controller('credit-notes')
@ApiTags('Credit Note Refunds')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNoteRefundsController {
constructor(
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
) {}
@Get(':creditNoteId/refunds')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Retrieve the credit note graph.' })
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
return this.creditNotesRefundsApplication.getCreditNoteRefunds(
@@ -29,6 +44,7 @@ export class CreditNoteRefundsController {
* @returns {Promise<RefundCreditNote>}
*/
@Post(':creditNoteId/refunds')
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Create a refund for the given credit note.' })
createRefundCreditNote(
@Param('creditNoteId') creditNoteId: number,
@@ -46,6 +62,7 @@ export class CreditNoteRefundsController {
* @returns {Promise<void>}
*/
@Delete('refunds/:refundCreditId')
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete a refund for the given credit note.' })
deleteRefundCreditNote(
@Param('refundCreditId') refundCreditId: number,

View File

@@ -34,7 +34,7 @@ export class RefundCreditNote extends BaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -18,6 +18,7 @@ import {
Put,
Query,
Res,
UseGuards,
} from '@nestjs/common';
import { CreditNoteApplication } from './CreditNoteApplication.service';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
@@ -30,6 +31,11 @@ import {
ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto';
import { AcceptType } from '@/constants/accept-type';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from './types/CreditNotes.types';
@Controller('credit-notes')
@ApiTags('Credit Notes')
@@ -37,6 +43,7 @@ import { AcceptType } from '@/constants/accept-type';
@ApiExtraModels(PaginatedResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNotesController {
/**
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
@@ -44,6 +51,7 @@ export class CreditNotesController {
constructor(private creditNoteApplication: CreditNoteApplication) { }
@Post()
@RequirePermission(CreditNoteAction.Create, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Create a new credit note' })
@ApiResponse({ status: 201, description: 'Credit note successfully created' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
@@ -52,6 +60,7 @@ export class CreditNotesController {
}
@Get('state')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note state' })
@ApiResponse({ status: 200, description: 'Returns the credit note state' })
getCreditNoteState() {
@@ -59,6 +68,7 @@ export class CreditNotesController {
}
@Get(':id')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get a specific credit note by ID' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({
@@ -92,6 +102,7 @@ export class CreditNotesController {
}
@Get()
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get all credit notes' })
@ApiResponse({
status: 200,
@@ -115,6 +126,7 @@ export class CreditNotesController {
}
@Put(':id')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Update a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully updated' })
@@ -131,6 +143,7 @@ export class CreditNotesController {
}
@Put(':id/open')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Open a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully opened' })
@@ -140,6 +153,7 @@ export class CreditNotesController {
}
@Post('validate-bulk-delete')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({
summary:
'Validates which credit notes can be deleted and returns the results.',
@@ -161,6 +175,7 @@ export class CreditNotesController {
}
@Post('bulk-delete')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Deletes multiple credit notes.' })
@ApiResponse({
status: 200,
@@ -173,6 +188,7 @@ export class CreditNotesController {
}
@Delete(':id')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully deleted' })

View File

@@ -56,7 +56,7 @@ export class CreditNote extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -1,15 +1,39 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNoteApplyToInvoices } from './commands/CreditNoteApplyToInvoices.service';
import { DeleteCreditNoteApplyToInvoices } from './commands/DeleteCreditNoteApplyToInvoices.service';
import { ApplyCreditNoteToInvoicesDto } from './dtos/ApplyCreditNoteToInvoices.dto';
@Controller('credit-notes')
@ApiTags('Credit Notes Apply Invoice')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNotesApplyInvoiceController {
constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
private readonly getCreditNoteAssociatedInvoicesToApplyService: GetCreditNoteAssociatedInvoicesToApply,
private readonly creditNoteApplyToInvoicesService: CreditNoteApplyToInvoices,
private readonly deleteCreditNoteApplyToInvoicesService: DeleteCreditNoteApplyToInvoices,
) {}
@Get(':creditNoteId/applied-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Applied credit note to invoices' })
@ApiResponse({
status: 200,
@@ -23,7 +47,25 @@ export class CreditNotesApplyInvoiceController {
);
}
@Get(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note associated invoices to apply' })
@ApiResponse({
status: 200,
description: 'Credit note associated invoices to apply',
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
getCreditNoteAssociatedInvoicesToApply(
@Param('creditNoteId') creditNoteId: number,
) {
return this.getCreditNoteAssociatedInvoicesToApplyService.getCreditAssociatedInvoicesToApply(
creditNoteId,
);
}
@Post(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' })
@ApiResponse({
status: 200,
@@ -31,9 +73,32 @@ export class CreditNotesApplyInvoiceController {
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) {
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices(
applyCreditNoteToInvoices(
@Param('creditNoteId') creditNoteId: number,
@Body() applyDto: ApplyCreditNoteToInvoicesDto,
) {
return this.creditNoteApplyToInvoicesService.applyCreditNoteToInvoices(
creditNoteId,
applyDto,
);
}
@Delete('applied-invoices/:applyCreditToInvoicesId')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete applied credit note to invoice' })
@ApiResponse({
status: 200,
description: 'Credit note application successfully deleted',
})
@ApiResponse({
status: 404,
description: 'Credit note application not found',
})
deleteApplyCreditNoteToInvoices(
@Param('applyCreditToInvoicesId') applyCreditToInvoicesId: number,
) {
return this.deleteCreditNoteApplyToInvoicesService.deleteApplyCreditNoteToInvoices(
applyCreditToInvoicesId,
);
}
}

View File

@@ -9,6 +9,8 @@ import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
import { CreditNoteApplySyncCreditSubscriber } from './subscribers/CreditNoteApplySyncCreditSubscriber';
import { CreditNoteApplySyncInvoicesCreditedAmountSubscriber } from './subscribers/CreditNoteApplySyncInvoicesSubscriber';
@Module({
providers: [
@@ -19,6 +21,8 @@ import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.con
CreditNoteApplySyncCredit,
GetCreditNoteAssociatedAppliedInvoices,
GetCreditNoteAssociatedInvoicesToApply,
CreditNoteApplySyncCreditSubscriber,
CreditNoteApplySyncInvoicesCreditedAmountSubscriber,
],
exports: [DeleteCustomerLinkedCreditNoteService],
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],

View File

@@ -1,6 +1,6 @@
import { Knex } from 'knex';
import { Injectable, Inject } from '@nestjs/common';
import Bluebird from 'bluebird';
import * as Bluebird from 'bluebird';
import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';

View File

@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { sumBy } from 'lodash';
import {
ICreditNoteAppliedToInvoice,
ICreditNoteAppliedToInvoiceModel,
IApplyCreditToInvoicesDTO,
IApplyCreditToInvoicesCreatedPayload,
@@ -17,6 +18,7 @@ import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ApplyCreditNoteToInvoicesDto } from '../dtos/ApplyCreditNoteToInvoices.dto';
@Injectable()
export class CreditNoteApplyToInvoices {
@@ -48,7 +50,7 @@ export class CreditNoteApplyToInvoices {
*/
public async applyCreditNoteToInvoices(
creditNoteId: number,
applyCreditToInvoicesDTO: IApplyCreditToInvoicesDTO,
applyCreditToInvoicesDTO: ApplyCreditNoteToInvoicesDto,
): Promise<CreditNoteAppliedInvoice[]> {
// Saves the credit note or throw not found service error.
const creditNote = await this.creditNoteModel()
@@ -71,7 +73,7 @@ export class CreditNoteApplyToInvoices {
// Validate invoices has remaining amount to apply.
this.validateInvoicesRemainingAmount(
appliedInvoicesEntries,
creditNoteAppliedModel.amount,
creditNoteAppliedModel.entries,
);
// Validate the credit note remaining amount.
this.creditNoteDTOTransform.validateCreditRemainingAmount(
@@ -122,18 +124,20 @@ export class CreditNoteApplyToInvoices {
};
/**
* Validate the invoice remaining amount.
* Validate each invoice has sufficient remaining amount for the applied credit.
* @param {ISaleInvoice[]} invoices
* @param {number} amount
* @param {ICreditNoteAppliedToInvoice[]} entries
*/
private validateInvoicesRemainingAmount = (
invoices: SaleInvoice[],
amount: number,
entries: ICreditNoteAppliedToInvoice[],
) => {
const invalidInvoices = invoices.filter(
(invoice) => invoice.dueAmount < amount,
);
if (invalidInvoices.length > 0) {
const invoiceMap = new Map(invoices.map((inv) => [inv.id, inv]));
const invalidEntries = entries.filter((entry) => {
const invoice = invoiceMap.get(entry.invoiceId);
return invoice != null && invoice.dueAmount < entry.amount;
});
if (invalidEntries.length > 0) {
throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT);
}
};

View File

@@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsInt,
IsNotEmpty,
IsNumber,
ValidateNested,
} from 'class-validator';
export class ApplyCreditNoteInvoiceEntryDto {
@IsNotEmpty()
@IsInt()
@ApiProperty({ description: 'Invoice ID to apply credit to', example: 1 })
invoiceId: number;
@IsNotEmpty()
@IsNumber()
@ApiProperty({ description: 'Amount to apply', example: 100.5 })
amount: number;
}
export class ApplyCreditNoteToInvoicesDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => ApplyCreditNoteInvoiceEntryDto)
@ApiProperty({
description: 'Entries of invoice ID and amount to apply',
type: [ApplyCreditNoteInvoiceEntryDto],
example: [
{ invoice_id: 1, amount: 100.5 },
{ invoice_id: 2, amount: 50 },
],
})
entries: ApplyCreditNoteInvoiceEntryDto[];
}

View File

@@ -26,7 +26,7 @@ export class CreditNoteAppliedInvoice extends BaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -8,7 +8,7 @@ import { CreditNoteApplySyncInvoicesCreditedAmount } from '../commands/CreditNot
import { events } from '@/common/events/events';
@Injectable()
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
export class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
constructor(
private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount,
) {}

View File

@@ -29,6 +29,7 @@ export interface IApplyCreditToInvoicesDeletedPayload {
export interface ICreditNoteAppliedToInvoice {
amount: number;
creditNoteId: number;
invoiceId: number;
}
export interface ICreditNoteAppliedToInvoiceModel {
amount: number;

View File

@@ -7,6 +7,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { CustomersApplication } from './CustomersApplication.service';
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
@@ -26,15 +27,22 @@ import {
ValidateBulkDeleteCustomersResponseDto,
} from './dtos/BulkDeleteCustomers.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CustomerAction } from './types/Customers.types';
@Controller('customers')
@ApiTags('Customers')
@ApiExtraModels(CustomerResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CustomersController {
constructor(private customersApplication: CustomersApplication) { }
@Get(':id')
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customer details.' })
@ApiResponse({
status: 200,
@@ -46,6 +54,7 @@ export class CustomersController {
}
@Get()
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customers paginated list.' })
@ApiResponse({
status: 200,
@@ -60,6 +69,7 @@ export class CustomersController {
}
@Post()
@RequirePermission(CustomerAction.Create, AbilitySubject.Customer)
@ApiOperation({ summary: 'Create a new customer.' })
@ApiResponse({
status: 201,
@@ -71,6 +81,7 @@ export class CustomersController {
}
@Put(':id')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the given customer.' })
@ApiResponse({
status: 200,
@@ -85,6 +96,7 @@ export class CustomersController {
}
@Delete(':id')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Delete the given customer.' })
@ApiResponse({
status: 200,
@@ -95,6 +107,7 @@ export class CustomersController {
}
@Put(':id/opening-balance')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the opening balance of the given customer.' })
@ApiResponse({
status: 200,
@@ -112,6 +125,7 @@ export class CustomersController {
}
@Post('validate-bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({
summary:
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
@@ -131,6 +145,7 @@ export class CustomersController {
}
@Post('bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
@ApiResponse({
status: 200,

View File

@@ -27,10 +27,10 @@ export class ContactAddressDto {
@IsEmail()
billingAddressEmail?: string;
@ApiProperty({ required: false, description: 'Billing address zipcode' })
@ApiProperty({ required: false, description: 'Billing address postcode' })
@IsOptional()
@IsString()
billingAddressZipcode?: string;
billingAddressPostcode?: string;
@ApiProperty({ required: false, description: 'Billing address phone' })
@IsOptional()
@@ -67,10 +67,10 @@ export class ContactAddressDto {
@IsEmail()
shippingAddressEmail?: string;
@ApiProperty({ required: false, description: 'Shipping address zipcode' })
@ApiProperty({ required: false, description: 'Shipping address postcode' })
@IsOptional()
@IsString()
shippingAddressZipcode?: string;
shippingAddressPostcode?: string;
@ApiProperty({ required: false, description: 'Shipping address phone' })
@IsOptional()

View File

@@ -7,6 +7,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ExpensesApplication } from './ExpensesApplication.service';
import { IExpensesFilter } from './Expenses.types';
@@ -25,6 +26,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ExpenseAction } from './Expenses.types';
@Controller('expenses')
@ApiTags('Expenses')
@@ -34,10 +40,12 @@ import {
ValidateBulkDeleteResponseDto,
)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ExpensesController {
constructor(private readonly expensesApplication: ExpensesApplication) { }
@Post('validate-bulk-delete')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({
summary: 'Validate which expenses can be deleted and return the results.',
})
@@ -58,6 +66,7 @@ export class ExpensesController {
}
@Post('bulk-delete')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ summary: 'Deletes multiple expenses.' })
@ApiResponse({
status: 200,
@@ -76,6 +85,7 @@ export class ExpensesController {
* @param {IExpenseCreateDTO} expenseDTO
*/
@Post()
@RequirePermission(ExpenseAction.Create, AbilitySubject.Expense)
@ApiOperation({ summary: 'Create a new expense transaction.' })
public createExpense(@Body() expenseDTO: CreateExpenseDto) {
return this.expensesApplication.createExpense(expenseDTO);
@@ -87,6 +97,7 @@ export class ExpensesController {
* @param {IExpenseEditDTO} expenseDTO
*/
@Put(':id')
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
@ApiOperation({ summary: 'Edit the given expense transaction.' })
public editExpense(
@Param('id') expenseId: number,
@@ -100,6 +111,7 @@ export class ExpensesController {
* @param {number} expenseId
*/
@Delete(':id')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ summary: 'Delete the given expense transaction.' })
public deleteExpense(@Param('id') expenseId: number) {
return this.expensesApplication.deleteExpense(expenseId);
@@ -110,6 +122,7 @@ export class ExpensesController {
* @param {number} expenseId
*/
@Post(':id/publish')
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
@ApiOperation({ summary: 'Publish the given expense transaction.' })
public publishExpense(@Param('id') expenseId: number) {
return this.expensesApplication.publishExpense(expenseId);
@@ -119,6 +132,7 @@ export class ExpensesController {
* Get the expense transaction details.
*/
@Get()
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
@ApiOperation({ summary: 'Get the expense transactions.' })
@ApiResponse({
status: 200,
@@ -146,6 +160,7 @@ export class ExpensesController {
* @param {number} expenseId
*/
@Get(':id')
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
@ApiOperation({ summary: 'Get the expense transaction details.' })
@ApiResponse({
status: 200,

View File

@@ -15,6 +15,7 @@ export class FinancialSheet {
negativeFormat: 'mines',
};
public baseCurrency: string;
public dateFormat: string = 'YYYY MMM DD';
/**
* Transformes the number format query to settings
@@ -140,13 +141,19 @@ export class FinancialSheet {
* @param {string} format
* @returns
*/
protected getDateMeta(date: moment.MomentInput, format = 'YYYY-MM-DD') {
protected getDateMeta(date: moment.MomentInput, format?: string) {
const dateFormat = format || this.dateFormat || 'YYYY MMM DD';
return {
formattedDate: moment(date).format(format),
formattedDate: moment(date).format(dateFormat),
date: moment(date).toDate(),
};
}
protected getDateFormatted(date: moment.MomentInput, format?: string) {
const dateFormat = format || this.dateFormat || 'YYYY MMM DD';
return moment(date).format(dateFormat);
}
getPercentageBasis = (base, amount) => {
return base ? amount / base : 0;
};

View File

@@ -1,5 +1,5 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type';
import {
@@ -11,14 +11,21 @@ import {
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/payable-aging-summary')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class APAgingSummaryController {
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
@Get()
@RequirePermission(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get payable aging summary' })
@ApiResponse({
status: 200,

View File

@@ -32,18 +32,19 @@ export class APAgingSummaryService {
this.APAgingSummaryRepository.setFilter(filter);
await this.APAgingSummaryRepository.load();
// Retrieve the aging summary report meta first to get date format.
const meta = await this.APAgingSummaryMeta.meta(filter);
// A/P aging summary report instance.
const APAgingSummaryReport = new APAgingSummarySheet(
filter,
this.APAgingSummaryRepository,
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
);
// A/P aging summary report data and columns.
const data = APAgingSummaryReport.reportData();
const columns = APAgingSummaryReport.reportColumns();
// Retrieve the aging summary report meta.
const meta = await this.APAgingSummaryMeta.meta(filter);
// Triggers `onPayableAgingViewed` event.
await this.eventPublisher.emitAsync(events.reports.onPayableAgingViewed, {
query,

View File

@@ -14,6 +14,7 @@ import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
import { Bill } from '@/modules/Bills/models/Bill';
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
export class APAgingSummarySheet extends AgingSummaryReport {
readonly repository: APAgingSummaryRepository;
@@ -31,12 +32,14 @@ export class APAgingSummarySheet extends AgingSummaryReport {
constructor(
query: APAgingSummaryQueryDto,
repository: APAgingSummaryRepository,
meta: IFinancialReportMeta,
) {
super();
this.query = query;
this.repository = repository;
this.numberFormat = this.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.overdueInvoicesByContactId = this.repository.overdueBillsByVendorId;
this.currentInvoicesByContactId = this.repository.dueBillsByVendorId;

View File

@@ -1,5 +1,4 @@
import { Controller, Get, Headers } from '@nestjs/common';
import { Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
@@ -12,14 +11,21 @@ import {
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/receivable-aging-summary')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ARAgingSummaryController {
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
@Get()
@RequirePermission(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get receivable aging summary' })
@ApiResponse({
status: 200,

View File

@@ -28,18 +28,19 @@ export class ARAgingSummaryService {
this.ARAgingSummaryRepository.setFilter(filter);
await this.ARAgingSummaryRepository.load();
// Retrieve the aging summary report meta first to get date format.
const meta = await this.ARAgingSummaryMeta.meta(filter);
// A/R aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet(
filter,
this.ARAgingSummaryRepository,
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
);
// A/R aging summary report data and columns.
const data = ARAgingSummaryReport.reportData();
const columns = ARAgingSummaryReport.reportColumns();
// Retrieve the aging summary report meta.
const meta = await this.ARAgingSummaryMeta.meta(filter);
// Triggers `onReceivableAgingViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onReceivableAgingViewed,

View File

@@ -14,6 +14,7 @@ import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
import { Customer } from '@/modules/Customers/models/Customer';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
export class ARAgingSummarySheet extends AgingSummaryReport {
readonly query: ARAgingSummaryQueryDto;
@@ -32,16 +33,19 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
* Constructor method.
* @param {ARAgingSummaryQueryDto} query - Query
* @param {ARAgingSummaryRepository} repository - Repository.
* @param {IFinancialReportMeta} meta - Report meta.
*/
constructor(
query: ARAgingSummaryQueryDto,
repository: ARAgingSummaryRepository,
meta: IFinancialReportMeta,
) {
super();
this.query = query;
this.repository = repository;
this.numberFormat = this.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.overdueInvoicesByContactId =
this.repository.overdueInvoicesByContactId;

View File

@@ -13,7 +13,7 @@ export class AgingSummaryMeta {
*/
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedAsDate = moment(query.asDate).format(commonMeta.dateFormat);
const formattedDateRange = `As ${formattedAsDate}`;
return {

View File

@@ -1,5 +1,5 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { AcceptType } from '@/constants/accept-type';
import { BalanceSheetApplication } from './BalanceSheetApplication';
import {
@@ -11,10 +11,16 @@ import {
import { BalanceSheetQueryDto } from './BalanceSheet.dto';
import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/balance-sheet')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BalanceSheetStatementController {
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
@@ -25,6 +31,7 @@ export class BalanceSheetStatementController {
* @param {string} acceptHeader - Accept header.
*/
@Get('')
@RequirePermission(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report)
@ApiOperation({ summary: 'Get balance sheet statement' })
@ApiResponse({
status: 200,

View File

@@ -24,14 +24,14 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods' = 'total';
@ApiProperty({
enum: ['day', 'month', 'year'],
enum: ['day', 'month', 'year', 'quarter'],
default: 'year',
description: 'Time period for column display',
})
@IsString()
@IsOptional()
@IsEnum(['day', 'month', 'year'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
@IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({
description: 'Start date for the balance sheet period',

View File

@@ -19,7 +19,7 @@ import { BalanceSheetFiltering } from './BalanceSheetFiltering';
import { BalanceSheetNetIncome } from './BalanceSheetNetIncome';
import { BalanceSheetAggregators } from './BalanceSheetAggregators';
import { BalanceSheetAccounts } from './BalanceSheetAccounts';
import { INumberFormatQuery } from '../../types/Report.types';
import { INumberFormatQuery, IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
import { FinancialSheet } from '../../common/FinancialSheet';
export class BalanceSheet extends R.pipe(
@@ -66,21 +66,23 @@ export class BalanceSheet extends R.pipe(
/**
* Constructor method.
* @param {IBalanceSheetQuery} query -
* @param {IAccount[]} accounts -
* @param {string} baseCurrency -
* @param {BalanceSheetRepository} repository -
* @param {I18nService} i18n -
* @param {IFinancialReportMeta} meta -
*/
constructor(
query: IBalanceSheetQuery,
repository: BalanceSheetRepository,
baseCurrency: string,
i18n: I18nService,
meta: IFinancialReportMeta,
) {
super();
this.query = new BalanceSheetQuery(query);
this.repository = repository;
this.baseCurrency = baseCurrency;
this.baseCurrency = meta.baseCurrency;
this.numberFormat = this.query.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.i18n = i18n;
}

View File

@@ -40,19 +40,19 @@ export class BalanceSheetInjectable {
// Loads all resources.
await this.balanceSheetRepository.asyncInitialize(filter);
// Balance sheet meta first to get date format.
const meta = await this.balanceSheetMeta.meta(filter);
// Balance sheet report instance.
const balanceSheetInstanace = new BalanceSheet(
filter,
this.balanceSheetRepository,
tenantMetadata.baseCurrency,
this.i18n,
{ baseCurrency: tenantMetadata.baseCurrency, dateFormat: meta.dateFormat },
);
// Balance sheet data.
const data = balanceSheetInstanace.reportData();
// Balance sheet meta.
const meta = await this.balanceSheetMeta.meta(filter);
// Triggers `onBalanceSheetViewed` event.
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
query,

View File

@@ -13,7 +13,7 @@ export class BalanceSheetMetaInjectable {
*/
public async meta(query: IBalanceSheetQuery): Promise<IBalanceSheetMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedAsDate = moment(query.toDate).format(commonMeta.dateFormat);
const formattedDateRange = `As ${formattedAsDate}`;
const sheetName = 'Balance Sheet Statement';

View File

@@ -27,7 +27,7 @@ import { DISPLAY_COLUMNS_BY } from './constants';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { INumberFormatQuery } from '../../types/Report.types';
import { INumberFormatQuery, IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
import { transformToMapBy } from '@/utils/transform-to-map-by';
import { accumSum } from '@/utils/accum-sum';
import { ModelObject } from 'objection';
@@ -62,12 +62,12 @@ export class CashFlowStatement extends R.pipe(
cashLedger: ILedger,
netIncomeLedger: ILedger,
query: ICashFlowStatementQuery,
baseCurrency: string,
i18n: I18nService,
meta: IFinancialReportMeta,
) {
super();
this.baseCurrency = baseCurrency;
this.baseCurrency = meta.baseCurrency;
this.i18n = i18n;
this.ledger = ledger;
this.cashLedger = cashLedger;
@@ -76,6 +76,7 @@ export class CashFlowStatement extends R.pipe(
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
this.query = query;
this.numberFormat = this.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.dateRangeSet = [];
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;

View File

@@ -85,6 +85,9 @@ export class CashFlowStatementService {
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
const netIncomeLedger = Ledger.fromTransactions(netIncome);
// Retrieve the cashflow sheet meta first to get date format.
const meta = await this.cashflowSheetMeta.meta(filter);
// Cash flow statement.
const cashFlowInstance = new CashFlowStatement(
accounts,
@@ -92,11 +95,9 @@ export class CashFlowStatementService {
cashLedger,
netIncomeLedger,
filter,
tenant.metadata.baseCurrency,
this.i18n,
{ baseCurrency: tenant.metadata.baseCurrency, dateFormat: meta.dateFormat },
);
// Retrieve the cashflow sheet meta.
const meta = await this.cashflowSheetMeta.meta(filter);
return {
data: cashFlowInstance.reportData(),

View File

@@ -34,13 +34,13 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({
description: 'Display columns by time period',
required: false,
enum: ['day', 'month', 'year'],
enum: ['day', 'month', 'year', 'quarter'],
default: 'year',
})
@IsString()
@IsOptional()
@IsEnum(['day', 'month', 'year'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
@IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({
description: 'Type of column display',

View File

@@ -239,7 +239,7 @@ export class CashFlowTable {
section: ICashFlowStatementSection,
): ICashFlowStatementSection => {
const label = section.footerLabel
? section.footerLabel
? this.i18n.t(section.footerLabel)
: this.i18n.t('financial_sheet.total_row', {
args: { value: section.label },
});
@@ -302,7 +302,7 @@ export class CashFlowTable {
* @returns {ITableColumn}
*/
private totalColumns = (): ITableColumn[] => {
return [{ key: 'total', label: this.i18n.t('Total') }];
return [{ key: 'total', label: this.i18n.t('cash_flow_statement.total') }];
};
/**
@@ -366,7 +366,7 @@ export class CashFlowTable {
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
R.concat([{ key: 'name', label: this.i18n.t('Account name') }]),
R.concat([{ key: 'name', label: this.i18n.t('cash_flow_statement.account_name') }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumns()),

View File

@@ -1,5 +1,5 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { AcceptType } from '@/constants/accept-type';
import { CashflowSheetApplication } from './CashflowSheetApplication';
import {
@@ -11,14 +11,21 @@ import {
import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto';
import { CashflowStatementResponseExample } from './CashflowStatement.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/cashflow-statement')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CashflowController {
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { }
@Get()
@RequirePermission(ReportsAction.READ_CASHFLOW, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Cashflow statement report',

View File

@@ -1,5 +1,6 @@
import * as moment from 'moment';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
import {
ICashFlowStatementMeta,
@@ -8,7 +9,10 @@ import {
@Injectable()
export class CashflowSheetMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
constructor(
private readonly financialSheetMeta: FinancialSheetMeta,
private readonly i18n: I18nService,
) {}
/**
* Cashflow sheet meta.
@@ -19,11 +23,13 @@ export class CashflowSheetMeta {
query: ICashFlowStatementQuery,
): Promise<ICashFlowStatementMeta> {
const meta = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
const formattedToDate = moment(query.toDate).format(meta.dateFormat);
const formattedFromDate = moment(query.fromDate).format(meta.dateFormat);
const fromLabel = this.i18n.t('cash_flow_statement.from_date');
const toLabel = this.i18n.t('cash_flow_statement.to_date');
const formattedDateRange = `${fromLabel} ${formattedFromDate} | ${toLabel} ${formattedToDate}`;
const sheetName = 'Statement of Cash Flow';
const sheetName = this.i18n.t('cash_flow_statement.sheet_name');
return {
...meta,

View File

@@ -8,7 +8,7 @@ import {
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { ModelObject } from 'objection';
import { INumberFormatQuery } from '../../types/Report.types';
import { INumberFormatQuery, IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
import { Customer } from '@/modules/Customers/models/Customer';
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
@@ -23,21 +23,22 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
* @param {IJournalPoster} receivableLedger
* @param {ICustomer[]} customers
* @param {ICustomerBalanceSummaryQuery} filter
* @param {string} baseCurrency
* @param {IFinancialReportMeta} meta
*/
constructor(
ledger: ILedger,
customers: ModelObject<Customer>[],
filter: ICustomerBalanceSummaryQuery,
baseCurrency: string
meta: IFinancialReportMeta,
) {
super();
this.ledger = ledger;
this.baseCurrency = baseCurrency;
this.baseCurrency = meta.baseCurrency;
this.customers = customers;
this.filter = filter;
this.numberFormat = this.filter.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
}
/**

View File

@@ -19,7 +19,7 @@ export class CustomerBalanceSummaryMeta {
query: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedAsDate = moment(query.asDate).format(commonMeta.dateFormat);
const formattedDateRange = `As ${formattedAsDate}`;
return {

View File

@@ -63,15 +63,16 @@ export class CustomerBalanceSummaryService {
// Ledger query.
const ledger = new Ledger(customersEntries);
// Retrieve the customer balance summary meta first to get date format.
const meta = await this.customerBalanceSummaryMeta.meta(filter);
// Report instance.
const report = new CustomerBalanceSummaryReport(
ledger,
customers,
filter,
tenantMetadata.baseCurrency,
{ baseCurrency: tenantMetadata.baseCurrency, dateFormat: meta.dateFormat },
);
// Retrieve the customer balance summary meta.
const meta = await this.customerBalanceSummaryMeta.meta(filter);
// Triggers `onCustomerBalanceSummaryViewed` event.
await this.eventPublisher.emitAsync(

View File

@@ -91,7 +91,7 @@ export class CustomerBalanceSummaryTable {
*/
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', value: this.i18n.t('Total') },
{ key: 'name', value: this.i18n.t('contact_summary_balance.total') },
{ key: 'total', accessor: 'total.formattedAmount' },
];
// @ts-ignore

View File

@@ -5,22 +5,29 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { AcceptType } from '@/constants/accept-type';
import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto';
import { GeneralLedgerResponseExample } from './GeneralLedger.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/general-ledger')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class GeneralLedgerController {
constructor(
private readonly generalLedgerApplication: GeneralLedgerApplication,
) {}
@Get()
@RequirePermission(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'General ledger report',

View File

@@ -19,6 +19,7 @@ import { Account } from '@/modules/Accounts/models/Account.model';
import { ModelObject } from 'objection';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
FinancialSheet,
@@ -33,18 +34,21 @@ export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
* @param {IGeneralLedgerSheetQuery} query -
* @param {GeneralLedgerRepository} repository -
* @param {I18nService} i18n -
* @param {IFinancialReportMeta} meta -
*/
constructor(
query: IGeneralLedgerSheetQuery,
repository: GeneralLedgerRepository,
i18n: I18nService,
meta: IFinancialReportMeta,
) {
super();
this.query = query;
this.numberFormat = this.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.repository = repository;
this.baseCurrency = this.repository.tenant.metadata.baseCurrency;
this.baseCurrency = meta.baseCurrency;
this.i18n = i18n;
}
@@ -87,7 +91,7 @@ export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
return {
id: entry.id,
date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
dateFormatted: moment(entry.date).format(this.dateFormat),
referenceType: entry.transactionType,
referenceId: entry.transactionId,

View File

@@ -6,6 +6,7 @@ export interface IGeneralLedgerSheetQuery {
toDate: Date | string;
basis: string;
numberFormat: IGeneralLedgerNumberFormat;
dateFormat?: string;
noneTransactions: boolean;
accountsIds: number[];
branchesIds?: number[];

View File

@@ -19,8 +19,8 @@ export class GeneralLedgerMeta {
): Promise<IGeneralLedgerMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedToDate = moment(query.toDate).format(commonMeta.dateFormat);
const formattedFromDate = moment(query.fromDate).format(commonMeta.dateFormat);
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
return {

View File

@@ -37,18 +37,19 @@ export class GeneralLedgerService {
this.generalLedgerRepository.setFilter(filter);
await this.generalLedgerRepository.asyncInitialize();
// Retrieve general ledger report metadata first to get the date format.
const meta = await this.generalLedgerMeta.meta(filter);
// General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet(
filter,
this.generalLedgerRepository,
this.i18n,
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
);
// Retrieve general ledger report data.
const reportData = generalLedgerInstance.reportData();
// Retrieve general ledger report metadata.
const meta = await this.generalLedgerMeta.meta(filter);
// Triggers `onGeneralLedgerViewed` event.
await this.eventEmitter.emitAsync(events.reports.onGeneralLedgerViewed, {});

View File

@@ -8,6 +8,7 @@ import { InventoryDetails } from './InventoryItemDetails';
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
import { InventoryDetailsMetaInjectable } from './InventoryItemDetailsMeta';
import { getInventoryItemDetailsDefaultQuery } from './constant';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class InventoryDetailsService {
@@ -15,6 +16,7 @@ export class InventoryDetailsService {
private readonly inventoryItemDetailsRepository: InventoryItemDetailsRepository,
private readonly inventoryDetailsMeta: InventoryDetailsMetaInjectable,
private readonly i18n: I18nService,
private readonly tenancyContext: TenancyContext,
) {}
/**
@@ -34,13 +36,16 @@ export class InventoryDetailsService {
this.inventoryItemDetailsRepository.setFilter(filter);
await this.inventoryItemDetailsRepository.asyncInit();
// Retrieve the meta first to get date format.
const meta = await this.inventoryDetailsMeta.meta(query);
// Inventory details report mapper.
const inventoryDetailsInstance = new InventoryDetails(
filter,
this.inventoryItemDetailsRepository,
this.i18n,
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
);
const meta = await this.inventoryDetailsMeta.meta(query);
return {
data: inventoryDetailsInstance.reportData(),

View File

@@ -17,6 +17,8 @@ import { Item } from '@/modules/Items/models/Item';
import {
IFormatNumberSettings,
INumberFormatQuery,
IFinancialReportMeta,
DEFAULT_REPORT_META,
} from '../../types/Report.types';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
@@ -35,11 +37,13 @@ export class InventoryDetails extends FinancialSheet {
* Constructor method.
* @param {InventoryItemDetailsRepository} repository - The repository.
* @param {I18nService} i18n - The i18n service.
* @param {IFinancialReportMeta} meta - Report meta.
*/
constructor(
filter: IInventoryDetailsQuery,
repository: InventoryItemDetailsRepository,
i18n: I18nService,
meta: IFinancialReportMeta,
) {
super();
@@ -48,6 +52,7 @@ export class InventoryDetails extends FinancialSheet {
this.query = filter;
this.numberFormat = this.query.numberFormat;
this.i18n = i18n;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
}
/**
@@ -89,7 +94,7 @@ export class InventoryDetails extends FinancialSheet {
*/
public getDateMeta(date: Date | string): IInventoryDetailsDate {
return {
formattedDate: moment(date).format('YYYY-MM-DD'),
formattedDate: moment(date).format(this.dateFormat),
date: moment(date).toDate(),
};
}

View File

@@ -19,8 +19,8 @@ export class InventoryDetailsMetaInjectable {
): Promise<IInventoryItemDetailMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedToDay = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format(commonMeta.dateFormat);
const formattedToDay = moment(query.toDate).format(commonMeta.dateFormat);
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDay}`;
const sheetName = 'Inventory Item Details';

View File

@@ -93,7 +93,7 @@ export class InventoryItemDetailsTable {
): ITableRow => {
const columns: Array<IColumnMapperMeta> = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: this.i18n.t('Opening balance') },
{ key: 'closing', value: this.i18n.t('inventory_item_details.opening_balance') },
{ key: 'empty', value: '' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty', value: '' },
@@ -115,7 +115,7 @@ export class InventoryItemDetailsTable {
): ITableRow => {
const columns: Array<IColumnMapperMeta> = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: this.i18n.t('Closing balance') },
{ key: 'closing', value: this.i18n.t('inventory_item_details.closing_balance') },
{ key: 'empty', value: '' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty', value: '' },
@@ -193,16 +193,16 @@ export class InventoryItemDetailsTable {
*/
public tableColumns = (): ITableColumn[] => {
return [
{ key: 'date', label: this.i18n.t('Date') },
{ key: 'transaction_type', label: this.i18n.t('Transaction type') },
{ key: 'transaction_id', label: this.i18n.t('Transaction #') },
{ key: 'quantity', label: this.i18n.t('Quantity') },
{ key: 'rate', label: this.i18n.t('Rate') },
{ key: 'total', label: this.i18n.t('Total') },
{ key: 'value', label: this.i18n.t('Value') },
{ key: 'profit_margin', label: this.i18n.t('Profit Margin') },
{ key: 'running_quantity', label: this.i18n.t('Running quantity') },
{ key: 'running_value', label: this.i18n.t('Running Value') },
{ key: 'date', label: this.i18n.t('inventory_item_details.date') },
{ key: 'transaction_type', label: this.i18n.t('inventory_item_details.transaction_type') },
{ key: 'transaction_id', label: this.i18n.t('inventory_item_details.transaction_number') },
{ key: 'quantity', label: this.i18n.t('inventory_item_details.quantity') },
{ key: 'rate', label: this.i18n.t('inventory_item_details.rate') },
{ key: 'total', label: this.i18n.t('inventory_item_details.total') },
{ key: 'value', label: this.i18n.t('inventory_item_details.value') },
{ key: 'profit_margin', label: this.i18n.t('inventory_item_details.profit_margin') },
{ key: 'running_quantity', label: this.i18n.t('inventory_item_details.running_quantity') },
{ key: 'running_value', label: this.i18n.t('inventory_item_details.running_value') },
];
};
}

View File

@@ -12,6 +12,7 @@ import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/Inventor
import { FinancialSheet } from '../../common/FinancialSheet';
import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
export class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery;
@@ -21,16 +22,19 @@ export class InventoryValuationSheet extends FinancialSheet {
* Constructor method.
* @param {IInventoryValuationReportQuery} query - Inventory valuation query.
* @param {InventoryValuationSheetRepository} repository - Inventory valuation sheet repository.
* @param {IFinancialReportMeta} meta - Report meta.
*/
constructor(
query: IInventoryValuationReportQuery,
repository: InventoryValuationSheetRepository,
meta: IFinancialReportMeta,
) {
super();
this.query = query;
this.repository = repository;
this.numberFormat = this.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
}
/**

View File

@@ -18,7 +18,7 @@ export class InventoryValuationMetaInjectable {
query: IInventoryValuationReportQuery,
): Promise<IInventoryValuationSheetMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedAsDate = moment(query.asDate).format(commonMeta.dateFormat);
const formattedDateRange = `As ${formattedAsDate}`;
return {

View File

@@ -34,16 +34,17 @@ export class InventoryValuationSheetService {
this.inventoryValuationSheetRepository.setFilter(filter);
await this.inventoryValuationSheetRepository.asyncInit();
// Retrieves the inventorty valuation meta first to get date format.
const meta = await this.inventoryValuationMeta.meta(filter);
const inventoryValuationInstance = new InventoryValuationSheet(
filter,
this.inventoryValuationSheetRepository,
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
);
// Retrieve the inventory valuation report data.
const inventoryValuationData = inventoryValuationInstance.reportData();
// Retrieves the inventorty valuation meta.
const meta = await this.inventoryValuationMeta.meta(filter);
// Triggers `onInventoryValuationViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onInventoryValuationViewed,

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { AcceptType } from '@/constants/accept-type';
import { JournalSheetApplication } from './JournalSheetApplication';
@@ -11,14 +11,21 @@ import {
import { JournalSheetQueryDto } from './JournalSheetQuery.dto';
import { JournalSheetResponseExample } from './JournalSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/journal')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class JournalSheetController {
constructor(private readonly journalSheetApp: JournalSheetApplication) {}
@Get()
@RequirePermission(ReportsAction.READ_JOURNAL, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Journal report',

View File

@@ -11,6 +11,7 @@ import { FinancialSheet } from '../../common/FinancialSheet';
import { JournalSheetRepository } from './JournalSheetRepository';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
export class JournalSheet extends FinancialSheet {
readonly query: IJournalReportQuery;
@@ -22,20 +23,24 @@ export class JournalSheet extends FinancialSheet {
* @param {IJournalReportQuery} query -
* @param {JournalSheetRepository} repository -
* @param {I18nService} i18n -
* @param {IFinancialReportMeta} meta -
*/
constructor(
query: IJournalReportQuery,
repository: JournalSheetRepository,
i18n: I18nService,
meta: IFinancialReportMeta,
) {
super();
this.query = query;
this.repository = repository;
this.baseCurrency = meta.baseCurrency;
this.numberFormat = {
...this.numberFormat,
...this.query.numberFormat,
};
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.i18n = i18n;
}
@@ -94,7 +99,7 @@ export class JournalSheet extends FinancialSheet {
return {
date: moment(groupEntry.date).toDate(),
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
dateFormatted: moment(groupEntry.date).format(this.dateFormat),
transactionType: groupEntry.transactionType,
referenceId: groupEntry.transactionId,

View File

@@ -8,6 +8,7 @@ export interface IJournalReportQuery {
noCents: boolean;
divideOn1000: boolean;
};
dateFormat?: string;
transactionType: string;
transactionId: string;

View File

@@ -17,8 +17,8 @@ export class JournalSheetMeta {
): Promise<IJournalSheetMeta> {
const common = await this.financialSheetMeta.meta();
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedToDate = moment(query.toDate).format(common.dateFormat);
const formattedFromDate = moment(query.fromDate).format(common.dateFormat);
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
return {

View File

@@ -30,18 +30,19 @@ export class JournalSheetService {
this.journalRepository.setFilter(query);
await this.journalRepository.load();
// Retrieve the journal sheet meta first to get the date format.
const meta = await this.journalSheetMeta.meta(filter);
// Journal report instance.
const journalSheetInstance = new JournalSheet(
filter,
this.journalRepository,
this.i18n,
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
);
// Retrieve journal report columns.
const journalSheetData = journalSheetInstance.reportData();
// Retrieve the journal sheet meta.
const meta = await this.journalSheetMeta.meta(filter);
// Triggers `onJournalViewed` event.
await this.eventPublisher.emitAsync(events.reports.onJournalViewed, {
query,

View File

@@ -1,5 +1,5 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { ProfitLossSheetApplication } from './ProfitLossSheetApplication';
import { AcceptType } from '@/constants/accept-type';
import {
@@ -11,10 +11,16 @@ import {
import { ProfitLossSheetQueryDto } from './ProfitLossSheetQuery.dto';
import { ProfitLossSheetResponseExample } from './ProfitLossSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/profit-loss-sheet')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ProfitLossSheetController {
constructor(
private readonly profitLossSheetApp: ProfitLossSheetApplication,
@@ -27,6 +33,7 @@ export class ProfitLossSheetController {
* @param {string} acceptHeader
*/
@Get('/')
@RequirePermission(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Profit & loss statement',

View File

@@ -28,6 +28,7 @@ import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Account } from '@/modules/Accounts/models/Account.model';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
export default class ProfitLossSheet extends R.pipe(
ProfitLossSheetPreviousYear,
@@ -71,20 +72,24 @@ export default class ProfitLossSheet extends R.pipe(
/**
* Constructor method.
* @param {ProfitLossSheetRepository} repository -
* @param {IProfitLossSheetQuery} query -
* @param {IAccount[]} accounts -
* @param {IJournalPoster} transactionsJournal -
* @param {I18nService} i18n -
* @param {IFinancialReportMeta} meta -
*/
constructor(
repository: ProfitLossSheetRepository,
query: IProfitLossSheetQuery,
i18n: I18nService,
meta: IFinancialReportMeta,
) {
super();
this.query = new ProfitLossSheetQuery(query);
this.repository = repository;
this.baseCurrency = meta.baseCurrency;
this.numberFormat = this.query.query.numberFormat;
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
this.i18n = i18n;
}

Some files were not shown because too many files have changed in this diff Show More