Compare commits

...

77 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
2cfa9123b8 fix: Syntax error 2024-09-01 23:32:54 +02:00
Ahmed Bouhuolia
bb9614aafb Merge pull request #646 from bigcapitalhq/events-tracking
feat(server): Events tracking using Posthog
2024-09-01 23:06:38 +02:00
Ahmed Bouhuolia
302564a56e fix: remove the un-used file 2024-09-01 23:02:41 +02:00
Ahmed Bouhuolia
dcfc231d4d feat(server): Events tracking using Posthog 2024-09-01 23:01:25 +02:00
Ahmed Bouhuolia
64080ed678 Merge pull request #644 from bigcapitalhq/set-default-index-entries
fix: Set default index to transaction entries
2024-09-01 15:10:40 +02:00
Ahmed Bouhuolia
263935d91e Merge pull request #643 from bigcapitalhq/import-bugs
fix: Import bugs
2024-09-01 15:02:31 +02:00
Ahmed Bouhuolia
7f5ffb8da1 fix: Add the missing columns to the payment received and made models 2024-09-01 15:00:51 +02:00
Ahmed Bouhuolia
f07d25edbe fix: Set default index to transaction entries 2024-09-01 13:32:47 +02:00
Ahmed Bouhuolia
7c07d6b5ff fix: The unimported functions 2024-09-01 11:16:50 +02:00
Ahmed Bouhuolia
e433f4ad68 fix: Item resource columns labels 2024-09-01 11:07:55 +02:00
Ahmed Bouhuolia
a79b9caff6 Merge pull request #641 from bigcapitalhq/fix-getting-sheet-columns
fix: Getting the sheet columns in import sheet
2024-08-30 17:56:53 +02:00
Ahmed Bouhuolia
2227cead66 Merge pull request #624 from bigcapitalhq/subscription-middleware
fix: Subscription middleware
2024-08-30 17:56:02 +02:00
Ahmed Bouhuolia
410c4ea3e2 fix: Subscription active detarminer 2024-08-30 17:52:53 +02:00
Ahmed Bouhuolia
f92acbcbe0 fix: Getting the sheet columns in import sheet 2024-08-30 17:03:16 +02:00
Ahmed Bouhuolia
c986585cd9 Merge pull request #640 from bigcapitalhq/fix-typo-one-click-demo
fix: Typo one-click demo page
2024-08-30 00:15:29 +02:00
Ahmed Bouhuolia
250f0a30ef fix: Typo one-click demo page 2024-08-30 00:14:59 +02:00
Ahmed Bouhuolia
84b5e1adc1 Merge pull request #639 from bigcapitalhq/add-customer-type-to-customers
fix: Add customer type to customers resource
2024-08-29 22:50:20 +02:00
Ahmed Bouhuolia
2ab28370db fix: Add customer type to customers resource 2024-08-29 22:49:49 +02:00
Ahmed Bouhuolia
a88a525326 Merge pull request #638 from bigcapitalhq/use-standard-date-format-export
fix: use standard ISO 8601 format for exported data
2024-08-29 22:40:59 +02:00
Ahmed Bouhuolia
fce8e2c5a4 fix: use standard ISO 8601 format for exported data 2024-08-29 22:39:51 +02:00
Ahmed Bouhuolia
095608266c Merge pull request #632 from bigcapitalhq/split-lazy-loading
feat: Optimize loading perf. by splitting big chunks and lazy loading them
2024-08-29 21:27:40 +02:00
Ahmed Bouhuolia
0ec8aaa330 fix: delete unwanted files 2024-08-29 21:27:20 +02:00
Ahmed Bouhuolia
9fcb3ef77d feat: Add fallback spinner to authentication lazy-loaded pages 2024-08-29 21:19:36 +02:00
Ahmed Bouhuolia
dc61c57daf fix: Add spinner to preferences lazy loaded pages 2024-08-29 21:03:27 +02:00
Ahmed Bouhuolia
af284f3f6d feat: split the preferences pages 2024-08-29 20:49:08 +02:00
Ahmed Bouhuolia
c43123db76 Merge pull request #636 from bigcapitalhq/expand-export-page-size
fix: Expand the resources export page size limitation
2024-08-29 14:23:48 +02:00
Ahmed Bouhuolia
ebbcab3926 fix: Expand the resources export page size limitation 2024-08-29 14:22:45 +02:00
Ahmed Bouhuolia
a235f573c0 Merge pull request #635 from bigcapitalhq/fix-avoid-cost-job-import-preview
fix: Avoid running the cost job in import preview
2024-08-29 10:09:13 +02:00
Ahmed Bouhuolia
84a0b8f495 fix: re-schedule the jobs have date from the current moment 2024-08-29 10:05:38 +02:00
Ahmed Bouhuolia
b87321c897 fix: Avoid running the cost job in import preview 2024-08-28 22:15:15 +02:00
Ahmed Bouhuolia
fc6ebfea5c Debounce scheduling calculating items cost 2024-08-28 21:25:47 +02:00
Ahmed Bouhuolia
c9fe6d9b37 feat: Optimize loading perf. by spliting big chunks and lazy loading them 2024-08-26 22:51:40 +02:00
Ahmed Bouhuolia
161d60393a Merge pull request #629 from bigcapitalhq/details-subscription
fix: Add subscription plans offer text
2024-08-25 19:44:34 +02:00
Ahmed Bouhuolia
79413fa85e fix: Add subscription plans offer text 2024-08-25 19:43:54 +02:00
Ahmed Bouhuolia
58552c6c94 Merge pull request #628 from bigcapitalhq/fix-webapp-env-variables
fix: Make webapp package env variables dynamic
2024-08-25 18:21:55 +02:00
Ahmed Bouhuolia
2072e35cfa fix: Make webapp package env variables dynamic 2024-08-25 18:21:08 +02:00
Ahmed Bouhuolia
1eaac9d691 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-08-25 14:18:04 +02:00
Ahmed Bouhuolia
a916e8a0cb fix: syntax error 2024-08-25 14:17:23 +02:00
Ahmed Bouhuolia
a56f560036 Merge pull request #627 from bigcapitalhq/fix-style-tweeks-in-onboarding-page
fix: Style tweaks in onboarding page
2024-08-25 13:20:39 +02:00
Ahmed Bouhuolia
0fb886936c Merge pull request #626 from bigcapitalhq/disable-sms-service
fix: Disable sms service until Twilo integration
2024-08-25 13:20:14 +02:00
Ahmed Bouhuolia
670136916f fix: Style tweaks in onboarding page 2024-08-25 13:19:16 +02:00
Ahmed Bouhuolia
768297f137 fix: Disable sms service until Twilo integration 2024-08-25 13:07:07 +02:00
Ahmed Bouhuolia
ef505a0a62 Merge pull request #625 from bigcapitalhq/manual-journal-number-prefix
fix: Add prefix J-00001 to manual journals increments
2024-08-25 13:05:52 +02:00
Ahmed Bouhuolia
42d40620ec fix: add prefix J-00001 to manual journals increments 2024-08-25 13:05:28 +02:00
Ahmed Bouhuolia
ee2d8d3065 fix: subscription middleare 2024-08-25 12:42:42 +02:00
Ahmed Bouhuolia
959ef7a691 Merge branch 'listen-payment-webhooks' into develop 2024-08-24 21:52:04 +02:00
Ahmed Bouhuolia
60f03f534b fix(subscription): event TS types 2024-08-24 21:51:15 +02:00
Ahmed Bouhuolia
e44ebb700a Merge pull request #623 from bigcapitalhq/listen-payment-webhooks
fix: Listen to payment webhooks
2024-08-24 21:49:36 +02:00
Ahmed Bouhuolia
8e94c7a755 feat(subscription): invalidate subscription cache 2024-08-24 21:40:28 +02:00
Ahmed Bouhuolia
3a2ca36c07 Merge branch 'develop' into listen-payment-webhooks 2024-08-24 21:08:47 +02:00
Ahmed Bouhuolia
88ece74c8a Merge pull request #622 from wolone/develop
Fix: Syntax error caused error
2024-08-24 20:48:14 +02:00
Ahmed Bouhuolia
67a8610328 feat: cancel/resume LS subscriptions 2024-08-24 20:46:30 +02:00
Ahmed Bouhuolia
278d61ce61 fix: Listen to payment webhooks 2024-08-24 18:50:12 +02:00
wolone
e72d6ad6b8 Fix: Syntax error caused error 2024-08-24 23:33:53 +08:00
Ahmed Bouhuolia
bf66b31679 Merge pull request #590 from bigcapitalhq/filter-uncategorized-bank-transactions
feat(banking): Filter uncategorized bank transactions by date
2024-08-23 17:42:59 +02:00
Ahmed Bouhuolia
b4d426d2e8 feat: Filter uncategorized transactions by date 2024-08-23 17:40:05 +02:00
Ahmed Bouhuolia
7f7dd270e7 Merge pull request #619 from bigcapitalhq/change-banking-service-language
feat: change banking service language
2024-08-23 02:00:47 +02:00
Ahmed Bouhuolia
f6bad8fe30 fix: wip filter uncategorized transactions by date 2024-08-23 01:57:52 +02:00
Ahmed Bouhuolia
820b363f79 feat: change banking service language 2024-08-22 20:49:15 +02:00
Ahmed Bouhuolia
07740a51ef Merge pull request #616 from bigcapitalhq/one-click-demo-account
feat(ee): One-click demo account
2024-08-22 20:33:06 +02:00
Ahmed Bouhuolia
d15fb6fe19 chore: document http query 2024-08-22 20:32:48 +02:00
Ahmed Bouhuolia
5749ccec81 feat: seed more demo bank transactions 2024-08-22 19:50:31 +02:00
Ahmed Bouhuolia
4a99f6c0cf feat: one-click demo account 2024-08-22 19:21:23 +02:00
Ahmed Bouhuolia
59f480f9d5 feat: add more demo account seeders 2024-08-22 13:04:51 +02:00
Ahmed Bouhuolia
6cb9c919b5 Merge branch 'develop' into one-click-demo-account 2024-08-22 10:48:24 +02:00
Ahmed Bouhuolia
1062b65b5b feat: wip bank account transactions date filter 2024-08-22 01:03:03 +02:00
Ahmed Bouhuolia
bf3a70cabd Merge branch 'develop' into filter-uncategorized-bank-transactions 2024-08-22 00:12:37 +02:00
Ahmed Bouhuolia
705b8da053 fix: protect the one-click demo accounts endpoints 2024-08-21 01:04:18 +02:00
Ahmed Bouhuolia
4a05ccc692 feat: add more seedders 2024-08-20 23:40:23 +02:00
Ahmed Bouhuolia
3200d65d90 feat: add demo account button on onboarding 2024-08-20 22:01:36 +02:00
Ahmed Bouhuolia
3f23038227 chore: comment 2024-08-20 18:49:39 +02:00
Ahmed Bouhuolia
408c807fc2 feat: import sheet files on initializing demo account 2024-08-20 18:30:21 +02:00
Ahmed Bouhuolia
d29079a8c5 fix: one-click demo account 2024-08-20 12:51:23 +02:00
Ahmed Bouhuolia
cca596b4a9 fix: one click demo 2024-08-19 21:21:39 +02:00
Ahmed Bouhuolia
fed620505d feat: add one click endpoint 2024-08-19 12:10:38 +02:00
Ahmed Bouhuolia
a008aea3f3 feat(ee): one-click demo account 2024-08-19 12:08:58 +02:00
Ahmed Bouhuolia
df8b68fda6 feat(banking): Filter uncategorized bank transactions 2024-08-11 18:34:45 +02:00
192 changed files with 4029 additions and 515 deletions

View File

@@ -89,3 +89,7 @@ S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY= S3_SECRET_ACCESS_KEY=
S3_ENDPOINT= S3_ENDPOINT=
S3_BUCKET= S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=

View File

@@ -37,6 +37,7 @@
"agendash": "^3.1.0", "agendash": "^3.1.0",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"async": "^3.2.0", "async": "^3.2.0",
"async-mutex": "^0.5.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
@@ -98,6 +99,7 @@
"objection-unique": "^1.2.2", "objection-unique": "^1.2.2",
"plaid": "^10.3.0", "plaid": "^10.3.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"posthog-node": "^4.2.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"puppeteer": "^10.2.0", "puppeteer": "^10.2.0",
"qim": "0.0.52", "qim": "0.0.52",

View File

@@ -46,6 +46,10 @@ export class ExcludeBankTransactionsController extends BaseController {
query('account_id').optional().isNumeric().toInt(), query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
], ],
this.validationResult, this.validationResult,
this.getExcludedBankTransactions.bind(this) this.getExcludedBankTransactions.bind(this)

View File

@@ -21,6 +21,10 @@ export class RecognizedTransactionsController extends BaseController {
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(), query('account_id').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().isFloat(),
], ],
this.validationResult, this.validationResult,
this.getRecognizedTransactions.bind(this) this.getRecognizedTransactions.bind(this)

View File

@@ -84,6 +84,10 @@ export default class NewCashflowTransactionController extends BaseController {
param('id').exists().isNumeric().toInt(), param('id').exists().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
]; ];
} }

View File

@@ -0,0 +1,87 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication';
import config from '@/config';
@Service()
export class OneClickDemoController extends BaseController {
@Inject()
private oneClickDemoApp: OneClickDemoApplication;
/**
* Router constructor method.
*/
router() {
const router = Router();
// Protects the endpoints if the feature is not enabled.
const protectMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Add your protection logic here
if (config.oneClickDemoAccounts) {
next();
} else {
res.status(403).send({ message: 'Forbidden' });
}
};
router.post(
'/one_click',
protectMiddleware,
asyncMiddleware(this.oneClickDemo.bind(this))
);
router.post(
'/one_click_signin',
[body('demo_id').exists()],
this.validationResult,
protectMiddleware,
asyncMiddleware(this.oneClickSignIn.bind(this))
);
return router;
}
/**
* One-click demo application.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async oneClickDemo(req: Request, res: Response, next: NextFunction) {
try {
const data = await this.oneClickDemoApp.createOneClick();
return res.status(200).send({
data,
message: 'The one-click demo has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Sign-in to one-click demo account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async oneClickSignIn(
req: Request,
res: Response,
next: NextFunction
) {
const { demoId } = this.matchedBodyData(req);
try {
const data = await this.oneClickDemoApp.autoSignIn(demoId);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
}

View File

@@ -113,7 +113,7 @@ export class SubscriptionController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.subscriptionApp.cancelSubscription(tenantId, '455610'); await this.subscriptionApp.cancelSubscription(tenantId);
return res.status(200).send({ return res.status(200).send({
status: 200, status: 200,

View File

@@ -35,7 +35,7 @@ export class Webhooks extends BaseController {
*/ */
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) { public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
const data = req.body; const data = req.body;
const signature = req.headers['x-signature'] ?? ''; const signature = req.headers['x-signature'] as string ?? '';
const rawBody = req.rawBody; const rawBody = req.rawBody;
try { try {

View File

@@ -63,6 +63,7 @@ import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks'; import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController'; import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController'; import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
export default () => { export default () => {
const app = Router(); const app = Router();
@@ -80,6 +81,7 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router()); app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router()); app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router()); app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())
// - Dashboard routes. // - Dashboard routes.
// --------------------------- // ---------------------------

View File

@@ -1,11 +1,21 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import SettingsStore from '@/services/Settings/SettingsStore'; import SettingsStore from '@/services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => { export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user; const { tenantId } = req.user;
const Logger = Container.get('logger'); const settings = await initializeTenantSettings(tenantId);
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}
export const initializeTenantSettings = async (tenantId: number) => {
const tenantContainer = Container.of(`tenant-${tenantId}`); const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) { if (tenantContainer && !tenantContainer.has('settings')) {
@@ -18,10 +28,5 @@ export default async (req: Request, res: Response, next: NextFunction) => {
await settings.load(); await settings.load();
req.settings = settings; return settings;
res.on('finish', async () => {
await settings.save();
});
next();
} }

View File

@@ -1,6 +1,8 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
const SupportedMethods = ['POST', 'PUT'];
export default (subscriptionSlug = 'main') => export default (subscriptionSlug = 'main') =>
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const { tenant, tenantId } = req; const { tenant, tenantId } = req;
@@ -19,8 +21,10 @@ export default (subscriptionSlug = 'main') =>
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }], errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
}); });
} }
// Validate in case the subscription is inactive. const isMethodSupported = SupportedMethods.includes(req.method);
else if (subscription.inactive()) { const isSubscriptionInactive = subscription.inactive();
if (isMethodSupported && isSubscriptionInactive) {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }], errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
}); });

View File

@@ -4,6 +4,7 @@ import { Request } from 'express';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager'; import TenantsManagerService from '@/services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect'; import rtlDetect from 'rtl-detect';
import { Tenant } from '@/system/models';
export default (req: Request, tenant: ITenant) => { export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant; const { id: tenantId, organizationId } = tenant;
@@ -16,7 +17,7 @@ export default (req: Request, tenant: ITenant) => {
const tenantContainer = tenantServices.tenantContainer(tenantId); const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils(req)); tenantContainer.set('i18n', injectI18nUtils());
const knexInstance = tenantServices.knex(tenantId); const knexInstance = tenantServices.knex(tenantId);
const models = tenantServices.models(tenantId); const models = tenantServices.models(tenantId);
@@ -33,14 +34,35 @@ export default (req: Request, tenant: ITenant) => {
}; };
export const injectI18nUtils = (req) => { export const injectI18nUtils = (req) => {
const locale = req.getLocale(); const globalI18n = Container.get('i18n');
const locale = globalI18n.getLocale();
const direction = rtlDetect.getLangDir(locale); const direction = rtlDetect.getLangDir(locale);
return { return {
locale, locale,
__: req.__, __: globalI18n.__,
direction, direction,
isRtl: direction === 'rtl', isRtl: direction === 'rtl',
isLtr: direction === 'ltr', isLtr: direction === 'ltr',
}; };
}; };
export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);
// Initialize the knex instance.
tenantsManager.setupKnexInstance(tenant);
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils());
tenantServices.knex(tenantId);
tenantServices.models(tenantId);
tenantServices.repositories(tenantId);
tenantServices.cache(tenantId);
};

View File

@@ -245,4 +245,20 @@ module.exports = {
loops: { loops: {
apiKey: process.env.LOOPS_API_KEY, apiKey: process.env.LOOPS_API_KEY,
}, },
/**
* One-click demo accounts.
*/
oneClickDemoAccounts: {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
/**
* PostHog
*/
posthog: {
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST
}
}; };

View File

@@ -0,0 +1,43 @@
export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice d';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited';
export const BILL_DELETED = 'Bill deleted';
export const PAYMENT_MADE_CREATED = 'Payment made created';
export const PAYMENT_MADE_EDITED = 'Payment made edited';
export const PAYMENT_MADE_DELETED = 'Payment made deleted';
export const EXPENSE_CREATED = 'Expense created';
export const EXPENSE_EDITED = 'Expense edited';
export const EXPENSE_DELETED = 'Expense deleted';
export const ACCOUNT_CREATED = 'Account created';
export const ACCOUNT_EDITED = 'Account Edited';
export const ACCOUNT_DELETED = 'Account deleted';
export const ITEM_EVENT_CREATED = 'Item created';
export const ITEM_EVENT_EDITED = 'Item edited';
export const ITEM_EVENT_DELETED = 'Item deleted';
export const AUTH_SIGNED_UP = 'Auth Signed-up';
export const AUTH_RESET_PASSWORD = 'Auth reset password';
export const ACCOUNT_GROUP = 'Account';
export const ITEM_GROUP = 'Item';
export const AUTH_GROUP = 'Auth';
export const SALE_GROUP = 'Sale';
export const PAYMENT_GROUP = 'Payment';
export const BILL_GROUP = 'Bill';
export const EXPENSE_GROUP = 'Expense';

View File

@@ -15,6 +15,7 @@ export default class SeedSettings extends TenantSeeder {
// Manual journals settings. // Manual journals settings.
{ group: 'manual_journals', key: 'next_number', value: '00001' }, { group: 'manual_journals', key: 'next_number', value: '00001' },
{ group: 'manual_journals', key: 'number_prefix', value: 'J-' },
{ group: 'manual_journals', key: 'auto_increment', value: true }, { group: 'manual_journals', key: 'auto_increment', value: true },
// Sale invoices settings. // Sale invoices settings.

View File

@@ -7,7 +7,6 @@ export interface IRegisterDTO {
lastName: string; lastName: string;
email: string; email: string;
password: string; password: string;
organizationName: string;
} }
export interface ILoginDTO { export interface ILoginDTO {
@@ -77,6 +76,10 @@ export interface IAuthSendedResetPassword {
export interface IAuthGetMetaPOJO { export interface IAuthGetMetaPOJO {
signupDisabled: boolean; signupDisabled: boolean;
oneClickDemo: {
enable: boolean;
demoUrl: string;
};
} }
export interface IAuthSignUpVerifingEventPayload { export interface IAuthSignUpVerifingEventPayload {

View File

@@ -167,11 +167,18 @@ export interface CategorizeTransactionAsExpenseDTO {
export interface IGetUncategorizedTransactionsQuery { export interface IGetUncategorizedTransactionsQuery {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
} }
export interface IGetRecognizedTransactionsQuery { export interface IGetRecognizedTransactionsQuery {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
accountId?: number; accountId?: number;
} minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}

View File

@@ -1,10 +1,10 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IDynamicListFilter, IItemEntry, IVendorCredit } from '@/interfaces'; import { IDynamicListFilter, IItemEntry } from '@/interfaces';
import { ILedgerEntry } from './Ledger'; import { ILedgerEntry } from './Ledger';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
export interface ICreditNoteEntryNewDTO { export interface ICreditNoteEntryNewDTO {
index: number; index?: number;
itemId: number; itemId: number;
rate: number; rate: number;
quantity: number; quantity: number;
@@ -22,7 +22,7 @@ export interface ICreditNoteNewDTO {
entries: ICreditNoteEntryNewDTO[]; entries: ICreditNoteEntryNewDTO[];
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[] attachments?: AttachmentLinkDTO[];
} }
export interface ICreditNoteEditDTO { export interface ICreditNoteEditDTO {
@@ -35,7 +35,7 @@ export interface ICreditNoteEditDTO {
entries: ICreditNoteEntryNewDTO[]; entries: ICreditNoteEntryNewDTO[];
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[] attachments?: AttachmentLinkDTO[];
} }
export interface ICreditNoteEntry extends IItemEntry {} export interface ICreditNoteEntry extends IItemEntry {}
@@ -61,7 +61,7 @@ export interface ICreditNote {
localAmount?: number; localAmount?: number;
branchId?: number; branchId?: number;
warehouseId: number; warehouseId: number;
createdAt?: Date, createdAt?: Date;
} }
export enum CreditNoteAction { export enum CreditNoteAction {

View File

@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
export interface IImportFileCommitedEventPayload { export interface IImportFileCommitedEventPayload {
tenantId: number; tenantId: number;
importId: number; importId: string;
meta: ImportFilePreviewPOJO; meta: ImportFilePreviewPOJO;
} }

View File

@@ -48,6 +48,7 @@ export interface IItemEntry {
export interface IItemEntryDTO { export interface IItemEntryDTO {
id?: number; id?: number;
index?: number;
itemId: number; itemId: number;
landedCost?: boolean; landedCost?: boolean;
warehouseId?: number; warehouseId?: number;

View File

@@ -66,7 +66,7 @@ export interface IPaymentReceivedEntry {
export interface IPaymentReceivedEntryDTO { export interface IPaymentReceivedEntryDTO {
id?: number; id?: number;
index: number; index?: number;
paymentReceiveId?: number; paymentReceiveId?: number;
invoiceId: number; invoiceId: number;
paymentAmount: number; paymentAmount: number;
@@ -74,6 +74,7 @@ export interface IPaymentReceivedEntryDTO {
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO { export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
filterQuery?: (trx: Knex.Transaction) => void;
} }
export interface IPaymentReceivePageEntry { export interface IPaymentReceivePageEntry {

View File

@@ -33,3 +33,7 @@ export interface IOrganizationBuildEventPayload {
buildDTO: IOrganizationBuildDTO; buildDTO: IOrganizationBuildDTO;
systemUser: ISystemUser; systemUser: ISystemUser;
} }
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}

View File

@@ -0,0 +1,8 @@
export interface SubscriptionPayload {
lemonSqueezyId?: string;
}
export enum SubscriptionPaymentStatus {
Succeed = 'succeed',
Failed = 'failed',
}

View File

@@ -75,6 +75,7 @@ export * from './Times';
export * from './ProjectProfitabilitySummary'; export * from './ProjectProfitabilitySummary';
export * from './TaxRate'; export * from './TaxRate';
export * from './Plaid'; export * from './Plaid';
export * from './Subscription';
export interface I18nService { export interface I18nService {
__: (input: string) => string; __: (input: string) => string;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash'; import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber, sortObjectKeysAlphabetically } from 'utils'; import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';
export class Transformer { export class Transformer {
public context: any; public context: any;
@@ -156,22 +157,34 @@ export class Transformer {
} }
/** /**
* * Format date.
* @param date * @param {} date
* @returns * @param {string} format -
* @returns {}
*/ */
protected formatDate(date) { protected formatDate(date, format?: string) {
return date ? moment(date).format(this.dateFormat) : ''; // Use the export date format if the async operation is in exporting,
// otherwise use the given or default format.
const _format = this.context.exportAls.isExport
? EXPORT_DTE_FORMAT
: format || this.dateFormat;
return date ? moment(date).format(_format) : '';
} }
protected formatDateFromNow(date){ /**
*
* @param date
* @returns {}
*/
protected formatDateFromNow(date) {
return date ? moment(date).fromNow(true) : ''; return date ? moment(date).fromNow(true) : '';
} }
/** /**
* *
* @param number * @param number
* @returns * @returns {}
*/ */
protected formatNumber(number, props?) { protected formatNumber(number, props?) {
return formatNumber(number, { money: false, ...props }); return formatNumber(number, { money: false, ...props });
@@ -181,7 +194,7 @@ export class Transformer {
* *
* @param money * @param money
* @param options * @param options
* @returns * @returns {}
*/ */
protected formatMoney(money, options?) { protected formatMoney(money, options?) {
return formatNumber(money, { return formatNumber(money, {

View File

@@ -3,12 +3,17 @@ import { isNull } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models'; import { TenantMetadata } from '@/system/models';
import { Transformer } from './Transformer'; import { Transformer } from './Transformer';
import { ImportAls } from '@/services/Import/ImportALS';
import { ExportAls } from '@/services/Export/ExportAls';
@Service() @Service()
export class TransformerInjectable { export class TransformerInjectable {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject()
private exportAls: ExportAls;
/** /**
* Retrieves the application context of all tenant transformers. * Retrieves the application context of all tenant transformers.
* @param {number} tenantId * @param {number} tenantId
@@ -17,10 +22,12 @@ export class TransformerInjectable {
async getApplicationContext(tenantId: number) { async getApplicationContext(tenantId: number) {
const i18n = this.tenancy.i18n(tenantId); const i18n = this.tenancy.i18n(tenantId);
const organization = await TenantMetadata.query().findOne({ tenantId }); const organization = await TenantMetadata.query().findOne({ tenantId });
const exportAls = this.exportAls;
return { return {
organization, organization,
i18n, i18n,
exportAls,
}; };
} }

View File

@@ -116,6 +116,9 @@ import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashfl
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted'; import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber'; import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting'; import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -246,8 +249,10 @@ export const susbcribers = () => {
DeleteCashflowTransactionOnUncategorize, DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete, PreventDeleteTransactionOnDelete,
// Subscription
SubscribeFreeOnSignupCommunity, SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp, SendVerfiyMailOnSignUp,
TriggerInvalidateCacheOnSubscriptionChange,
// Attachments // Attachments
AttachmentsOnSaleInvoiceCreated, AttachmentsOnSaleInvoiceCreated,
@@ -281,6 +286,11 @@ export const susbcribers = () => {
DeleteUncategorizedTransactionsOnAccountDeleting, DeleteUncategorizedTransactionsOnAccountDeleting,
// Loops // Loops
LoopsEventsSubscriber LoopsEventsSubscriber,
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
...EventsTrackerListeners
]; ];
}; };

View File

@@ -2,6 +2,7 @@ import { I18n } from 'i18n';
export default () => new I18n({ export default () => new I18n({
locales: ['en', 'ar'], locales: ['en', 'ar'],
defaultLocale: 'en',
register: global, register: global,
directory: global.__locales_dir, directory: global.__locales_dir,
updateFiles: false, updateFiles: false,

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'bill_date', sortField: 'bill_date',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -77,7 +79,7 @@ export default {
paymentDate: { paymentDate: {
name: 'bill_payment.field.payment_date', name: 'bill_payment.field.payment_date',
type: 'date', type: 'date',
accessor: 'formattedPaymentDate' accessor: 'formattedPaymentDate',
}, },
paymentNumber: { paymentNumber: {
name: 'bill_payment.field.payment_number', name: 'bill_payment.field.payment_number',
@@ -111,6 +113,34 @@ export default {
name: 'bill_payment.field.reference', name: 'bill_payment.field.reference',
type: 'text', type: 'text',
}, },
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Bill date',
accessor: 'bill.formattedBillDate',
},
billNo: {
name: 'Bill No.',
accessor: 'bill.billNo',
},
billRefNo: {
name: 'Bill Reference No.',
accessor: 'bill.referenceNo',
},
billAmount: {
name: 'Bill Amount',
accessor: 'bill.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
}, },
fields2: { fields2: {
vendorId: { vendorId: {

View File

@@ -95,6 +95,11 @@ export default {
}, },
}, },
columns: { columns: {
customerType: {
name: 'Customer Type',
type: 'text',
accessor: 'formattedCustomerType',
},
firstName: { firstName: {
name: 'vendor.field.first_name', name: 'vendor.field.first_name',
type: 'text', type: 'text',
@@ -135,116 +140,117 @@ export default {
openingBalance: { openingBalance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
type: 'number', type: 'number',
printable: false printable: false,
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
type: 'date', type: 'date',
printable: false printable: false,
accessor: 'formattedOpeningBalanceAt'
}, },
currencyCode: { currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
type: 'text', type: 'text',
printable: false printable: false,
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
printable: false printable: false,
}, },
note: { note: {
name: 'vendor.field.note', name: 'vendor.field.note',
printable: false printable: false,
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
name: 'Billing Address 1', name: 'Billing Address 1',
column: 'billing_address1', column: 'billing_address1',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
type: 'text', type: 'text',
printable: false printable: false,
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
name: 'Shipping Address 1', name: 'Shipping Address 1',
column: 'shipping_address1', column: 'shipping_address1',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
type: 'text', type: 'text',
printable: false printable: false,
}, },
createdAt: { createdAt: {
name: 'vendor.field.created_at', name: 'vendor.field.created_at',
type: 'date', type: 'date',
printable: false printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -41,17 +41,17 @@ export default {
fieldType: 'boolean', fieldType: 'boolean',
}, },
sell_price: { sell_price: {
name: 'item.field.cost_price', name: 'item.field.sell_price',
column: 'sell_price', column: 'sell_price',
fieldType: 'number', fieldType: 'number',
}, },
cost_price: { cost_price: {
name: 'item.field.cost_account', name: 'item.field.cost_price',
column: 'cost_price', column: 'cost_price',
fieldType: 'number', fieldType: 'number',
}, },
cost_account: { cost_account: {
name: 'item.field.sell_account', name: 'item.field.cost_account',
column: 'cost_account_id', column: 'cost_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -62,7 +62,7 @@ export default {
relationEntityKey: 'slug', relationEntityKey: 'slug',
}, },
sell_account: { sell_account: {
name: 'item.field.sell_description', name: 'item.field.sell_account',
column: 'sell_account_id', column: 'sell_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -155,24 +155,24 @@ export default {
printable: false, printable: false,
}, },
sellPrice: { sellPrice: {
name: 'item.field.cost_price', name: 'item.field.sell_price',
type: 'number', type: 'number',
exportable: true, exportable: true,
}, },
costPrice: { costPrice: {
name: 'item.field.cost_account', name: 'item.field.cost_price',
type: 'number', type: 'number',
exportable: true, exportable: true,
}, },
costAccount: { costAccount: {
name: 'item.field.sell_account', name: 'item.field.cost_account',
type: 'text', type: 'text',
accessor: 'costAccount.name', accessor: 'costAccount.name',
exportable: true, exportable: true,
printable: false, printable: false,
}, },
sellAccount: { sellAccount: {
name: 'item.field.sell_description', name: 'item.field.sell_account',
type: 'text', type: 'text',
accessor: 'sellAccount.name', accessor: 'sellAccount.name',
exportable: true, exportable: true,

View File

@@ -1,6 +1,9 @@
export default { export default {
importable: true, importable: true,
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo', importAggregateBy: 'paymentReceiveNo',
@@ -72,7 +75,7 @@ export default {
amount: { amount: {
name: 'payment_receive.field.amount', name: 'payment_receive.field.amount',
type: 'number', type: 'number',
accessor: 'formattedAmount' accessor: 'formattedAmount',
}, },
referenceNo: { referenceNo: {
name: 'payment_receive.field.reference_no', name: 'payment_receive.field.reference_no',
@@ -92,6 +95,34 @@ export default {
type: 'text', type: 'text',
printable: false, printable: false,
}, },
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Invoice date',
accessor: 'invoice.invoiceDateFormatted',
},
invoiceNo: {
name: 'Invoice No.',
accessor: 'invoice.invoiceNo',
},
invoiceRefNo: {
name: 'Invoice Reference No.',
accessor: 'invoice.referenceNo',
},
invoiceAmount: {
name: 'Invoice Amount',
accessor: 'invoice.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
created_at: { created_at: {
name: 'payment_receive.field.created_at', name: 'payment_receive.field.created_at',
type: 'date', type: 'date',

View File

@@ -13,7 +13,7 @@ export default {
importAggregateBy: 'estimateNumber', importAggregateBy: 'estimateNumber',
print: { print: {
pageTitle: 'Sale Estimates' pageTitle: 'Sale Estimates',
}, },
fields: { fields: {
@@ -142,6 +142,7 @@ export default {
delivered: { delivered: {
name: 'Delivered', name: 'Delivered',
type: 'boolean', type: 'boolean',
accessor: 'isDelivered',
exportable: true, exportable: true,
printable: false, printable: false,
}, },

View File

@@ -155,6 +155,7 @@ export default {
name: 'invoice.field.delivered', name: 'invoice.field.delivered',
type: 'boolean', type: 'boolean',
printable: false, printable: false,
accessor: 'isDelivered',
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',

View File

@@ -1,8 +1,8 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
import moment from 'moment';
import { Model, mixin } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import ModelSettings from './ModelSetting'; import ModelSettings from './ModelSetting';
import Account from './Account';
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta'; import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
export default class UncategorizedCashflowTransaction extends mixin( export default class UncategorizedCashflowTransaction extends mixin(
@@ -166,6 +166,28 @@ export default class UncategorizedCashflowTransaction extends mixin(
pending(query) { pending(query) {
query.where('pending', true); query.where('pending', true);
}, },
minAmount(query, minAmount) {
query.where('amount', '>=', minAmount);
},
maxAmount(query, maxAmount) {
query.where('amount', '<=', maxAmount);
},
toDate(query, toDate) {
const dateFormat = 'YYYY-MM-DD';
const _toDate = moment(toDate).endOf('day').format(dateFormat);
query.where('date', '<=', _toDate);
},
fromDate(query, fromDate) {
const dateFormat = 'YYYY-MM-DD';
const _fromDate = moment(fromDate).startOf('day').format(dateFormat);
query.where('date', '>=', _fromDate);
},
}; };
} }

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication'; import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces'; import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class AccountsExportable extends Exportable { export class AccountsExportable extends Exportable {
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
inactiveMode: false, inactiveMode: false,
...query, ...query,
structure: IAccountsStructureType.Flat, structure: IAccountsStructureType.Flat,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
page: 1, page: 1,
} as IAccountsFilter; } as IAccountsFilter;

View File

@@ -11,6 +11,10 @@ export class GetAuthMeta {
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> { public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return { return {
signupDisabled: config.signupRestrictions.disabled, signupDisabled: config.signupRestrictions.disabled,
oneClickDemo: {
enable: config.oneClickDemoAccounts.enable,
demoUrl: config.oneClickDemoAccounts.demoUrl,
},
}; };
} }
} }

View File

@@ -1,5 +1,6 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import moment from 'moment';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ExcludedBankTransactionsQuery } from './_types'; import { ExcludedBankTransactionsQuery } from './_types';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer'; import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@@ -39,6 +40,18 @@ export class GetExcludedBankTransactionsService {
if (_query.accountId) { if (_query.accountId) {
q.where('account_id', _query.accountId); q.where('account_id', _query.accountId);
} }
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
}) })
.pagination(_query.page - 1, _query.pageSize); .pagination(_query.page - 1, _query.pageSize);

View File

@@ -4,6 +4,10 @@ export interface ExcludedBankTransactionsQuery {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
accountId?: number; accountId?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
} }
export interface IBankTransactionUnexcludingEventPayload { export interface IBankTransactionUnexcludingEventPayload {

View File

@@ -100,12 +100,14 @@ export class TriggerRecognizedTransactions {
private async triggerRecognizeTransactionsOnImportCommitted({ private async triggerRecognizeTransactionsOnImportCommitted({
tenantId, tenantId,
importId, importId,
meta,
}: IImportFileCommitedEventPayload) { }: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId }); const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch; const batch = importFile.paramsParsed.batch;
const payload = { tenantId, transactionsCriteria: { batch } }; const payload = { tenantId, transactionsCriteria: { batch } };
// Cannot continue if the imported resource is not bank account transactions.
if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
await this.agenda.now('recognize-uncategorized-transactions-job', payload); await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }
} }

View File

@@ -23,7 +23,7 @@ export class GetRecognizedTransactionsService {
) { ) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const _filter = { const _query = {
page: 1, page: 1,
pageSize: 20, pageSize: 20,
...filter, ...filter,
@@ -41,11 +41,26 @@ export class GetRecognizedTransactionsService {
// Exclude the pending transactions. // Exclude the pending transactions.
q.modify('notPending'); q.modify('notPending');
if (_filter.accountId) { if (_query.accountId) {
q.where('accountId', _filter.accountId); q.where('accountId', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
if (_query.accountId) {
q.where('accountId', _query.accountId);
} }
}) })
.pagination(_filter.page - 1, _filter.pageSize); .pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform( const data = await this.transformer.transform(
tenantId, tenantId,

View File

@@ -62,6 +62,19 @@ export class GetUncategorizedTransactions {
q.whereNull('matchedBankTransactions.id'); q.whereNull('matchedBankTransactions.id');
q.orderBy('date', 'DESC'); q.orderBy('date', 'DESC');
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
}) })
.pagination(_query.page - 1, _query.pageSize); .pagination(_query.page - 1, _query.pageSize);

View File

@@ -87,7 +87,7 @@ export class UncategorizedTransactionsImportable extends Importable {
} }
/** /**
* Transformes the import params before storing them. * Transforms the import params before storing them.
* @param {Record<string, any>} parmas * @param {Record<string, any>} parmas
*/ */
public transformParams(parmas: Record<string, any>) { public transformParams(parmas: Record<string, any>) {

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces'; import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication'; import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class CustomersExportable extends Exportable { export class CustomersExportable extends Exportable {
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
const parsedQuery = { const parsedQuery = {
sortOrder: 'DESC', sortOrder: 'DESC',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1,
...query, ...query,
pageSize: 12, page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter; } as IItemsFilter;
return this.customersApplication return this.customersApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces'; import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication'; import { VendorsApplication } from './VendorsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class VendorsExportable extends Exportable { export class VendorsExportable extends Exportable {
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
const parsedQuery = { const parsedQuery = {
sortOrder: 'DESC', sortOrder: 'DESC',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1,
...query, ...query,
pageSize: 12, page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter; } as IItemsFilter;
return this.vendorsApplication return this.vendorsApplication

View File

@@ -5,11 +5,17 @@ import * as R from 'ramda';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { ICreditNote, ICreditNoteEditDTO, ICreditNoteNewDTO } from '@/interfaces'; import {
ICreditNote,
ICreditNoteEditDTO,
ICreditNoteEntryNewDTO,
ICreditNoteNewDTO,
} from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService'; import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
@Service() @Service()
export default class BaseCreditNotes { export default class BaseCreditNotes {
@@ -43,10 +49,17 @@ export default class BaseCreditNotes {
const amount = this.itemsEntriesService.getTotalItemsEntries( const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries creditNoteDTO.entries
); );
const entries = creditNoteDTO.entries.map((entry) => ({ const entries = R.compose(
...entry, // Associate the default index to each item entry.
referenceType: 'CreditNote', assocItemEntriesDefaultIndex,
}));
// Associate the reference type to credit note entries.
R.map((entry: ICreditNoteEntryNewDTO) => ({
...entry,
referenceType: 'CreditNote',
}))
)(creditNoteDTO.entries);
// Retreive the next credit note number. // Retreive the next credit note number.
const autoNextNumber = this.getNextCreditNumber(tenantId); const autoNextNumber = this.getNextCreditNumber(tenantId);

View File

@@ -0,0 +1,24 @@
import { PostHog } from 'posthog-node';
import { Service } from 'typedi';
import { EventMessage } from 'posthog-node/src/types';
import config from '@/config';
@Service()
export class PosthogService {
public posthog: PostHog;
constructor() {
if (config.posthog.apiKey && config.posthog.host) {
this.posthog = new PostHog(config.posthog.apiKey, {
host: config.posthog.host,
});
}
}
public trackEvent(event: EventMessage) {
// Cannot continue if the Posthog not configured.
if (!this.posthog) return;
this.posthog.capture(event);
}
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IAccountEventCreatedPayload,
IAccountEventEditedPayload,
IAccountEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
ACCOUNT_CREATED,
ACCOUNT_EDITED,
ACCOUNT_DELETED,
} from '@/constants/event-tracker';
@Service()
export class AccountEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.accounts.onCreated,
this.handleTrackAccountCreatedEvent
);
bus.subscribe(events.accounts.onEdited, this.handleTrackEditedAccountEvent);
bus.subscribe(
events.accounts.onDeleted,
this.handleTrackDeletedAccountEvent
);
}
private handleTrackAccountCreatedEvent = ({
tenantId,
}: IAccountEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_CREATED,
properties: {},
});
};
private handleTrackEditedAccountEvent = ({
tenantId,
}: IAccountEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_EDITED,
properties: {},
});
};
private handleTrackDeletedAccountEvent = ({
tenantId,
}: IAccountEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { IAuthSignedUpEventPayload } from '@/interfaces';
import { PosthogService } from '../PostHog';
import { AUTH_SIGNED_UP } from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class AuthenticationEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.auth.signUp, this.handleTrackSignUpEvent);
}
private handleTrackSignUpEvent = ({
signupDTO,
user,
tenant,
}: IAuthSignedUpEventPayload) => {
this.posthog.trackEvent({
distinctId: user.email,
event: AUTH_SIGNED_UP,
properties: {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
},
});
};
}

View File

@@ -0,0 +1,59 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEventEditedPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BILL_CREATED,
BILL_EDITED,
BILL_DELETED,
} from '@/constants/event-tracker';
@Service()
export class BillEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.bill.onCreated, this.handleTrackBillCreatedEvent);
bus.subscribe(events.bill.onEdited, this.handleTrackEditedBillEvent);
bus.subscribe(events.bill.onDeleted, this.handleTrackDeletedBillEvent);
}
private handleTrackBillCreatedEvent = ({
tenantId,
}: IBillPaymentEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_CREATED,
properties: {},
});
};
private handleTrackEditedBillEvent = ({
tenantId,
}: IBillPaymentEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_EDITED,
properties: {},
});
};
private handleTrackDeletedBillEvent = ({
tenantId,
}: IBillPaymentEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IExpenseCreatedPayload,
IExpenseEventEditPayload,
IExpenseEventDeletePayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
EXPENSE_CREATED,
EXPENSE_EDITED,
EXPENSE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ExpenseEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.expenses.onCreated,
this.handleTrackExpenseCreatedEvent
);
bus.subscribe(events.expenses.onEdited, this.handleTrackEditedExpenseEvent);
bus.subscribe(
events.expenses.onDeleted,
this.handleTrackDeletedExpenseEvent
);
}
private handleTrackExpenseCreatedEvent = ({
tenantId,
}: IExpenseCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_CREATED,
properties: {},
});
};
private handleTrackEditedExpenseEvent = ({
tenantId,
}: IExpenseEventEditPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_EDITED,
properties: {},
});
};
private handleTrackDeletedExpenseEvent = ({
tenantId,
}: IExpenseEventDeletePayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,59 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IItemEventCreatedPayload,
IItemEventEditedPayload,
IItemEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
ITEM_EVENT_CREATED,
ITEM_EVENT_EDITED,
ITEM_EVENT_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ItemEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
}
private handleTrackItemCreatedEvent = ({
tenantId,
}: IItemEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_CREATED,
properties: {},
});
};
private handleTrackEditedItemEvent = ({
tenantId,
}: IItemEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_EDITED,
properties: {},
});
};
private handleTrackDeletedItemEvent = ({
tenantId,
}: IItemEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEditingPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_MADE_CREATED,
PAYMENT_MADE_EDITED,
PAYMENT_MADE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentMadeEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.billPayment.onCreated,
this.handleTrackPaymentMadeCreatedEvent
);
bus.subscribe(
events.billPayment.onEdited,
this.handleTrackEditedPaymentMadeEvent
);
bus.subscribe(
events.billPayment.onDeleted,
this.handleTrackDeletedPaymentMadeEvent
);
}
private handleTrackPaymentMadeCreatedEvent = ({
tenantId,
}: IBillPaymentEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_CREATED,
properties: {},
});
};
private handleTrackEditedPaymentMadeEvent = ({
tenantId,
}: IBillPaymentEditingPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_EDITED,
properties: {},
});
};
private handleTrackDeletedPaymentMadeEvent = ({
tenantId,
}: IBillPaymentEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IPaymentReceivedCreatedPayload,
IPaymentReceivedEditedPayload,
IPaymentReceivedDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_RECEIVED_CREATED,
PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentReceivedEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentReceive.onCreated,
this.handleTrackPaymentReceivedCreatedEvent
);
bus.subscribe(
events.paymentReceive.onEdited,
this.handleTrackEditedPaymentReceivedEvent
);
bus.subscribe(
events.paymentReceive.onDeleted,
this.handleTrackDeletedPaymentReceivedEvent
);
}
private handleTrackPaymentReceivedCreatedEvent = ({
tenantId,
}: IPaymentReceivedCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_CREATED,
properties: {},
});
};
private handleTrackEditedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_EDITED,
properties: {},
});
};
private handleTrackDeletedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleEstimateCreatedPayload,
ISaleEstimateEditedPayload,
ISaleEstimateDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
SALE_ESTIMATE_CREATED,
SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class SaleEstimateEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleEstimate.onCreated,
this.handleTrackEstimateCreatedEvent
);
bus.subscribe(
events.saleEstimate.onEdited,
this.handleTrackEditedEstimateEvent
);
bus.subscribe(
events.saleEstimate.onDeleted,
this.handleTrackDeletedEstimateEvent
);
}
private handleTrackEstimateCreatedEvent = ({
tenantId,
}: ISaleEstimateCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_CREATED,
properties: {},
});
};
private handleTrackEditedEstimateEvent = ({
tenantId,
}: ISaleEstimateEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_EDITED,
properties: {},
});
};
private handleTrackDeletedEstimateEvent = ({
tenantId,
}: ISaleEstimateDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED,
} from '@/constants/event-tracker';
@Service()
export class SaleInvoiceEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleTrackInvoiceCreatedEvent
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleTrackEditedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onDeleted,
this.handleTrackDeletedInvoiceEvent
);
}
private handleTrackInvoiceCreatedEvent = ({
tenantId,
}: ISaleInvoiceCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_CREATED,
properties: {},
});
};
private handleTrackEditedInvoiceEvent = ({
tenantId,
}: ISaleInvoiceEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_EDITED,
properties: {},
});
};
private handleTrackDeletedInvoiceEvent = ({
tenantId,
}: ISaleInvoiceEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,21 @@
import { SaleInvoiceEventsTracker } from './SaleInvoicesEventsTracker';
import { SaleEstimateEventsTracker } from './SaleEstimateEventsTracker';
import { PaymentMadeEventsTracker } from './PaymentMadeEventsTracker';
import { PaymentReceivedEventsTracker } from './PaymentReceivedEventsTracker';
import { BillEventsTracker } from './BillEventsTracker';
import { ExpenseEventsTracker } from './ExpenseEventsTracker';
import { AccountEventsTracker } from './AccountEventsTracker';
import { AuthenticationEventsTracker } from './AuthenticationEventsTracker';
import { ItemEventsTracker } from './ItemEventsTracker';
export const EventsTrackerListeners = [
SaleInvoiceEventsTracker,
SaleEstimateEventsTracker,
PaymentMadeEventsTracker,
PaymentReceivedEventsTracker,
BillEventsTracker,
AccountEventsTracker,
ExpenseEventsTracker,
AuthenticationEventsTracker,
ItemEventsTracker
];

View File

@@ -11,6 +11,7 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TenantMetadata } from '@/system/models'; import { TenantMetadata } from '@/system/models';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export class ExpenseDTOTransformer { export class ExpenseDTOTransformer {
@@ -40,8 +41,8 @@ export class ExpenseDTOTransformer {
/** /**
* Mapping expense DTO to model. * Mapping expense DTO to model.
* @param {IExpenseDTO} expenseDTO * @param {IExpenseDTO} expenseDTO
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @return {IExpense} * @return {IExpense}
*/ */
private expenseDTOToModel( private expenseDTOToModel(
@@ -52,9 +53,14 @@ export class ExpenseDTOTransformer {
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO); const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories); const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
const categories = R.compose(
// Associate the default index to categories lines.
assocItemEntriesDefaultIndex
)(expenseDTO.categories || []);
const initialDTO = { const initialDTO = {
categories: [],
...omit(expenseDTO, ['publish', 'attachments']), ...omit(expenseDTO, ['publish', 'attachments']),
categories,
totalAmount, totalAmount,
landedCostAmount, landedCostAmount,
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(), paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces'; import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication'; import { ExpensesApplication } from './ExpensesApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class ExpensesExportable extends Exportable { export class ExpensesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as IExpensesFilter; } as IExpensesFilter;
return this.expensesApplication return this.expensesApplication

View File

@@ -0,0 +1,48 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ExportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), () => {
this.markAsExport();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an export operation.
* @param flag Boolean flag to set or unset the export status. Defaults to true.
*/
public markAsExport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isExport', flag);
}
/**
* Checks if the current context is an export operation.
* @returns {boolean} True if the context is an export operation, false otherwise.
*/
public get isExport(): boolean {
return !!this.getStore()?.get('isExport');
}
}

View File

@@ -10,6 +10,7 @@ import { Errors, ExportFormat } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces'; import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils'; import { flatDataCollections, getDataAccessor } from './utils';
import { ExportPdf } from './ExportPdf'; import { ExportPdf } from './ExportPdf';
import { ExportAls } from './ExportAls';
@Service() @Service()
export class ExportResourceService { export class ExportResourceService {
@@ -22,13 +23,33 @@ export class ExportResourceService {
@Inject() @Inject()
private exportPdf: ExportPdf; private exportPdf: ExportPdf;
@Inject()
private exportAls: ExportAls;
/**
*
* @param {number} tenantId
* @param {string} resourceName
* @param {ExportFormat} format
* @returns
*/
public async export(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv
) {
return this.exportAls.run(() =>
this.exportAlsRun(tenantId, resourceName, format)
);
}
/** /**
* Exports the given resource data through csv, xlsx or pdf. * Exports the given resource data through csv, xlsx or pdf.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {string} resourceName - Resource name. * @param {string} resourceName - Resource name.
* @param {ExportFormat} format - File format. * @param {ExportFormat} format - File format.
*/ */
public async export( public async exportAlsRun(
tenantId: number, tenantId: number,
resourceName: string, resourceName: string,
format: ExportFormat = ExportFormat.Csv format: ExportFormat = ExportFormat.Csv

View File

@@ -0,0 +1,2 @@
export const EXPORT_SIZE_LIMIT = 9999999;
export const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';

View File

@@ -0,0 +1,105 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ImportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), callback);
}
/**
* Runs a callback function in preview mode within the AsyncLocalStorage context.
* @param callback The function to be executed in preview mode.
* @returns The result of the callback function.
*/
public runPreview<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportPreview();
return callback();
});
}
/**
* Runs a callback function in commit mode within the AsyncLocalStorage context.
* @param {() => T} callback - The function to be executed in commit mode.
* @returns {T} The result of the callback function.
*/
public runCommit<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportCommit();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an import operation.
* @param flag Boolean flag to set or unset the import status. Defaults to true.
*/
public markAsImport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImport', flag);
}
/**
* Marks the current context as an import commit operation.
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
*/
public markAsImportCommit(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportCommit', flag);
}
/**
* Marks the current context as an import preview operation.
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
*/
public markAsImportPreview(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportPreview', flag);
}
/**
* Checks if the current context is an import operation.
* @returns {boolean} True if the context is an import operation, false otherwise.
*/
public get isImport(): boolean {
return !!this.getStore()?.get('isImport');
}
/**
* Checks if the current context is an import commit operation.
* @returns {boolean} True if the context is an import commit operation, false otherwise.
*/
public get isImportCommit(): boolean {
return !!this.getStore()?.get('isImportCommit');
}
/**
* Checks if the current context is an import preview operation.
* @returns {boolean} True if the context is an import preview operation, false otherwise.
*/
public get isImportPreview(): boolean {
return !!this.getStore()?.get('isImportPreview');
}
}

View File

@@ -1,4 +1,3 @@
import XLSX from 'xlsx';
import bluebird from 'bluebird'; import bluebird from 'bluebird';
import * as R from 'ramda'; import * as R from 'ramda';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
@@ -27,23 +26,7 @@ export class ImportFileCommon {
@Inject() @Inject()
private resource: ResourceService; private resource: ResourceService;
/**
* Maps the columns of the imported data based on the provided mapping attributes.
* @param {Record<string, any>[]} body - The array of data objects to map.
* @param {ImportMappingAttr[]} map - The mapping attributes.
* @returns {Record<string, any>[]} - The mapped data objects.
*/
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return XLSX.utils.sheet_to_json(worksheet, {});
}
/** /**
* Imports the given parsed data to the resource storage through registered importable service. * Imports the given parsed data to the resource storage through registered importable service.
* @param {number} tenantId - * @param {number} tenantId -

View File

@@ -23,7 +23,7 @@ export class ImportFileMapping {
*/ */
public async mapping( public async mapping(
tenantId: number, tenantId: number,
importId: number, importId: string,
maps: ImportMappingAttr[] maps: ImportMappingAttr[]
): Promise<ImportFileMapPOJO> { ): Promise<ImportFileMapPOJO> {
const importFile = await Import.query() const importFile = await Import.query()

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { ImportFilePreviewPOJO } from './interfaces'; import { ImportFilePreviewPOJO } from './interfaces';
import { ImportFileProcess } from './ImportFileProcess'; import { ImportFileProcess } from './ImportFileProcess';
import { ImportAls } from './ImportALS';
@Service() @Service()
export class ImportFilePreview { export class ImportFilePreview {
@@ -11,15 +12,33 @@ export class ImportFilePreview {
@Inject() @Inject()
private importFile: ImportFileProcess; private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId -
* @param {string} importId -
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
this.previewAlsRun(tenantId, importId)
);
}
/** /**
* Preview the imported file results before commiting the transactions. * Preview the imported file results before commiting the transactions.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async preview( public async previewAlsRun(
tenantId: number, tenantId: number,
importId: number importId: string
): Promise<ImportFilePreviewPOJO> { ): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId); const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' }); const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
import { chain } from 'lodash'; import { chain } from 'lodash';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
ERRORS,
getSheetColumns,
getUnmappedSheetColumns,
readImportFile,
} from './_utils';
import { ImportFileCommon } from './ImportFileCommon'; import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataTransformer } from './ImportFileDataTransformer'; import { ImportFileDataTransformer } from './ImportFileDataTransformer';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import UnitOfWork from '../UnitOfWork'; import UnitOfWork from '../UnitOfWork';
import { ImportFilePreviewPOJO } from './interfaces'; import { ImportFilePreviewPOJO } from './interfaces';
import { Import } from '@/system/models'; import { Import } from '@/system/models';
import { parseSheetData } from './sheet_utils';
@Service() @Service()
export class ImportFileProcess { export class ImportFileProcess {
@@ -37,7 +33,7 @@ export class ImportFileProcess {
*/ */
public async import( public async import(
tenantId: number, tenantId: number,
importId: number, importId: string,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> { ): Promise<ImportFilePreviewPOJO> {
const importFile = await Import.query() const importFile = await Import.query()
@@ -49,10 +45,10 @@ export class ImportFileProcess {
if (!importFile.isMapped) { if (!importFile.isMapped) {
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED); throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
} }
// Read the imported file. // Read the imported file and parse the given buffer to get columns
// and sheet data in json format.
const buffer = await readImportFile(importFile.filename); const buffer = await readImportFile(importFile.filename);
const sheetData = this.importCommon.parseXlsxSheet(buffer); const [sheetData, sheetColumns] = parseSheetData(buffer);
const header = getSheetColumns(sheetData);
const resource = importFile.resource; const resource = importFile.resource;
const resourceFields = this.resource.getResourceFields2(tenantId, resource); const resourceFields = this.resource.getResourceFields2(tenantId, resource);
@@ -87,7 +83,7 @@ export class ImportFileProcess {
.flatten() .flatten()
.value(); .value();
const unmappedColumns = getUnmappedSheetColumns(header, mapping); const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
const totalCount = allData.length; const totalCount = allData.length;
const createdCount = successedImport.length; const createdCount = successedImport.length;

View File

@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { IImportFileCommitedEventPayload } from '@/interfaces/Import'; import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
import { ImportAls } from './ImportALS';
@Service() @Service()
export class ImportFileProcessCommit { export class ImportFileProcessCommit {
@@ -14,18 +15,36 @@ export class ImportFileProcessCommit {
@Inject() @Inject()
private importFile: ImportFileProcess; private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
/**
* Commits the imported file under ALS.
* @param {number} tenantId
* @param {string} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public commit(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
this.commitAlsRun(tenantId, importId)
);
}
/** /**
* Commits the imported file. * Commits the imported file.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async commit( public async commitAlsRun(
tenantId: number, tenantId: number,
importId: number importId: string
): Promise<ImportFilePreviewPOJO> { ): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId); const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' }); const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator'; import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces'; import { ImportFileUploadPOJO } from './interfaces';
import { Import } from '@/system/models'; import { Import } from '@/system/models';
import { parseSheetData } from './sheet_utils';
@Service() @Service()
export class ImportFileUploadService { export class ImportFileUploadService {
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
const buffer = await readImportFile(filename); const buffer = await readImportFile(filename);
// Parse the buffer file to array data. // Parse the buffer file to array data.
const sheetData = this.importFileCommon.parseXlsxSheet(buffer); const [sheetData, sheetColumns] = parseSheetData(buffer);
const coumnsStringified = JSON.stringify(sheetColumns);
// Throws service error if the sheet data is empty. // Throws service error if the sheet data is empty.
validateSheetEmpty(sheetData); validateSheetEmpty(sheetData);
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
const coumnsStringified = JSON.stringify(sheetColumns);
try { try {
// Validates the params Yup schema. // Validates the params Yup schema.
await this.importFileCommon.validateParamsSchema(resource, params); await this.importFileCommon.validateParamsSchema(resource, params);

View File

@@ -55,7 +55,7 @@ export class ImportResourceApplication {
*/ */
public async mapping( public async mapping(
tenantId: number, tenantId: number,
importId: number, importId: string,
maps: ImportMappingAttr[] maps: ImportMappingAttr[]
) { ) {
return this.importMappingService.mapping(tenantId, importId, maps); return this.importMappingService.mapping(tenantId, importId, maps);
@@ -67,7 +67,7 @@ export class ImportResourceApplication {
* @param {number} importId - Import id. * @param {number} importId - Import id.
* @returns {Promise<ImportFilePreviewPOJO>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async preview(tenantId: number, importId: number) { public async preview(tenantId: number, importId: string) {
return this.ImportFilePreviewService.preview(tenantId, importId); return this.ImportFilePreviewService.preview(tenantId, importId);
} }
@@ -77,7 +77,7 @@ export class ImportResourceApplication {
* @param {number} importId * @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>} * @returns {Promise<ImportFilePreviewPOJO>}
*/ */
public async process(tenantId: number, importId: number) { public async process(tenantId: number, importId: string) {
return this.importProcessCommit.commit(tenantId, importId); return this.importProcessCommit.commit(tenantId, importId);
} }

View File

@@ -0,0 +1,56 @@
import XLSX from 'xlsx';
import { first } from 'lodash';
/**
* Parses the given sheet buffer to worksheet.
* @param {Buffer} buffer
* @returns {XLSX.WorkSheet}
*/
export function parseFirstSheet(buffer: Buffer): XLSX.WorkSheet {
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return worksheet;
}
/**
* Extracts the given worksheet to columns.
* @param {XLSX.WorkSheet} worksheet
* @returns {Array<string>}
*/
export function extractSheetColumns(worksheet: XLSX.WorkSheet): Array<string> {
// By default, sheet_to_json scans the first row and uses the values as headers.
// With the header: 1 option, the function exports an array of arrays of values.
const sheetCells = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const sheetCols = first(sheetCells) as Array<string>;
return sheetCols.filter((col) => col);
}
/**
* Parses the given worksheet to json values. the keys are columns labels.
* @param {XLSX.WorkSheet} worksheet
* @returns {Array<Record<string, string>>}
*/
export function parseSheetToJson(
worksheet: XLSX.WorkSheet
): Array<Record<string, string>> {
return XLSX.utils.sheet_to_json(worksheet, {});
}
/**
* Parses the given sheet buffer then retrieves the sheet data and columns.
* @param {Buffer} buffer
*/
export function parseSheetData(
buffer: Buffer
): [Array<Record<string, string>>, string[]] {
const worksheet = parseFirstSheet(buffer);
const columns = extractSheetColumns(worksheet);
const data = parseSheetToJson(worksheet);
return [data, columns];
}

View File

@@ -139,24 +139,25 @@ export default class InventoryService {
) { ) {
const agenda = Container.get('agenda'); const agenda = Container.get('agenda');
const commonJobsQuery = {
name: 'compute-item-cost',
lastRunAt: { $exists: false },
'data.tenantId': tenantId,
'data.itemId': itemId,
};
// Cancel any `compute-item-cost` in the queue has upper starting date // Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item. // with the same given item.
await agenda.cancel({ await agenda.cancel({
name: 'compute-item-cost', ...commonJobsQuery,
nextRunAt: { $ne: null }, 'data.startingDate': { $lte: startingDate },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $gt: startingDate },
}); });
// Retrieve any `compute-item-cost` in the queue has lower starting date // Retrieve any `compute-item-cost` in the queue has lower starting date
// with the same given item. // with the same given item.
const dependsJobs = await agenda.jobs({ const dependsJobs = await agenda.jobs({
name: 'compute-item-cost', ...commonJobsQuery,
nextRunAt: { $ne: null }, 'data.startingDate': { $gte: startingDate },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $lte: startingDate },
}); });
// If the depends jobs cleared.
if (dependsJobs.length === 0) { if (dependsJobs.length === 0) {
await agenda.schedule( await agenda.schedule(
config.scheduleComputeItemCost, config.scheduleComputeItemCost,
@@ -172,6 +173,13 @@ export default class InventoryService {
events.inventory.onComputeItemCostJobScheduled, events.inventory.onComputeItemCostJobScheduled,
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload { startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
); );
} else {
// Re-schedule the jobs that have higher date from current moment.
await Promise.all(
dependsJobs.map((job) =>
job.schedule(config.scheduleComputeItemCost).save()
)
);
} }
} }

View File

@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntry } from '@/models'; import { ItemEntry } from '@/models';
import { entriesAmountDiff } from 'utils'; import { entriesAmountDiff } from 'utils';
import { Knex } from 'knex';
const ERRORS = { const ERRORS = {
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
@@ -58,13 +59,14 @@ export default class ItemsEntriesService {
*/ */
public async filterInventoryEntries( public async filterInventoryEntries(
tenantId: number, tenantId: number,
entries: IItemEntry[] entries: IItemEntry[],
trx?: Knex.Transaction
): Promise<IItemEntry[]> { ): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId); const entriesItemsIds = entries.map((e) => e.itemId);
// Retrieve entries inventory items. // Retrieve entries inventory items.
const inventoryItems = await Item.query() const inventoryItems = await Item.query(trx)
.whereIn('id', entriesItemsIds) .whereIn('id', entriesItemsIds)
.where('type', 'inventory'); .where('type', 'inventory');

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { IItemsFilter } from '@/interfaces'; import { IItemsFilter } from '@/interfaces';
import { ItemsApplication } from './ItemsApplication'; import { ItemsApplication } from './ItemsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class ItemsExportable extends Exportable { export class ItemsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ItemsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
...query, ...query,
pageSize: 12, pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter; } as IItemsFilter;
return this.itemsApplication return this.itemsApplication

View File

@@ -0,0 +1,16 @@
import { IItemEntry } from '@/interfaces';
import { isNull, isUndefined } from 'lodash';
export function assocItemEntriesDefaultIndex<T>(
entries: Array<T & { index?: number }>
): Array<T & { index: number }> {
return entries.map((entry, index) => {
return {
index:
isUndefined(entry.index) || isNull(entry.index)
? index + 1
: entry.index,
...entry,
};
});
}

View File

@@ -18,6 +18,8 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandManualJournalValidators } from './CommandManualJournalValidators'; import { CommandManualJournalValidators } from './CommandManualJournalValidators';
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal'; import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer'; import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
@Service() @Service()
export class CreateManualJournalService { export class CreateManualJournalService {
@Inject() @Inject()
@@ -58,16 +60,22 @@ export class CreateManualJournalService {
// The manual or auto-increment journal number. // The manual or auto-increment journal number.
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber; const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex
)(manualJournalDTO.entries);
const initialDTO = { const initialDTO = {
...omit(manualJournalDTO, ['publish', 'attachments']), ...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish ...(manualJournalDTO.publish
? { publishedAt: moment().toMySqlDateTime() } ? { publishedAt: moment().toMySqlDateTime() }
: {}), : {}),
amount, amount,
date,
currencyCode: manualJournalDTO.currencyCode || baseCurrency, currencyCode: manualJournalDTO.currencyCode || baseCurrency,
exchangeRate: manualJournalDTO.exchangeRate || 1, exchangeRate: manualJournalDTO.exchangeRate || 1,
date,
journalNumber, journalNumber,
entries,
userId: authorizedUser.id, userId: authorizedUser.id,
}; };
return R.compose( return R.compose(

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IManualJournalsFilter } from '@/interfaces'; import { IManualJournalsFilter } from '@/interfaces';
import { Exportable } from '../Export/Exportable'; import { Exportable } from '../Export/Exportable';
import { ManualJournalsApplication } from './ManualJournalsApplication'; import { ManualJournalsApplication } from './ManualJournalsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service() @Service()
export class ManualJournalsExportable extends Exportable { export class ManualJournalsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ManualJournalsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as IManualJournalsFilter; } as IManualJournalsFilter;
return this.manualJournalsApplication return this.manualJournalsApplication

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import { faker } from '@faker-js/faker';
import uniqid from 'uniqid';
import AuthenticationApplication from '../Authentication/AuthApplication';
import OrganizationService from '../Organization/OrganizationService';
import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SystemUser } from '@/system/models';
import { IAuthSignInPOJO } from '@/interfaces';
import { ICreateOneClickDemoPOJO } from './interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { defaultDemoOrganizationDTO } from './_constants';
@Service()
export class CreateOneClickDemo {
@Inject()
private authApp: AuthenticationApplication;
@Inject()
private organizationService: OrganizationService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
*/
public async createOneClickDemo(): Promise<ICreateOneClickDemoPOJO> {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const email = faker.internet.email();
const password = '123123123';
const demoId = uniqid();
await this.authApp.signUp({ firstName, lastName, email, password });
const signedIn = await this.authApp.signIn(email, password);
const tenantId = signedIn.tenant.id;
const userId = signedIn.user.id;
// Creates a new one-click demo.
await OneClickDemo.query().insert({ key: demoId, tenantId, userId });
const buildJob = await this.organizationService.buildRunJob(
tenantId,
defaultDemoOrganizationDTO,
signedIn.user
);
return { email, demoId, signedIn, buildJob };
}
/**
* Sign-in automicatlly using the demo id one creating an account finish.
* @param {string} oneClickDemoId -
* @returns {Promise<IAuthSignInPOJO>}
*/
async autoSignIn(oneClickDemoId: string): Promise<IAuthSignInPOJO> {
const foundOneclickDemo = await OneClickDemo.query()
.findOne('key', oneClickDemoId)
.throwIfNotFound();
const userId = foundOneclickDemo.userId;
const user = await SystemUser.query().findById(userId);
const email = user.email;
const password = '123123123';
const signedIn = await this.authApp.signIn(email, password);
return signedIn;
}
}

View File

@@ -0,0 +1,17 @@
export class SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
* @returns {Array<>}
*/
get mapping() {
return [];
}
/**
* Retireves the seeder file import params.
* @returns {Record<string, any>}
*/
get importParams() {
return {};
}
}

View File

@@ -0,0 +1,35 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoBankTransactions extends SeedDemoAbstract {
get mapping() {
return [
{ from: 'Date', to: 'date' },
{ from: 'Payee', to: 'payee' },
{ from: 'Description', to: 'description' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Amount', to: 'amount' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `bank-transactions.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'UncategorizedCashflowTransaction';
}
get importParams() {
return {
accountId: 1001,
};
}
}

View File

@@ -0,0 +1,37 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountCustomers extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Customer Type', to: 'customerType' },
{ from: 'First Name', to: 'firstName' },
{ from: 'Last Name', to: 'lastName' },
{ from: 'Display Name', to: 'displayName' },
{ from: 'Email', to: 'email' },
{ from: 'Work Phone Number', to: 'workPhone' },
{ from: 'Personal Phone Number', to: 'personalPhone' },
{ from: 'Company Name', to: 'companyName' },
{ from: 'Website', to: 'website' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `customers.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Customer';
}
}

View File

@@ -0,0 +1,39 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountExpenses extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Payment Account', to: 'paymentAccountId' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Payment Date', to: 'paymentDate' },
{ from: 'Description', to: 'description' },
{ from: 'Publish', to: 'publish' },
{
from: 'Expense Account',
to: 'expenseAccountId',
group: 'categories',
},
{ from: 'Amount', to: 'amount', group: 'categories' },
{ from: 'Line Description', to: 'description', group: 'categories' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `Expenses.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Expense';
}
}

View File

@@ -0,0 +1,45 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountItems extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Item Type', to: 'type' },
{ from: 'Item Name', to: 'name' },
{ from: 'Item Code', to: 'code' },
{ from: 'Sellable', to: 'sellable' },
{ from: 'Purchasable', to: 'purchasable' },
{ from: 'Sell Price', to: 'sellPrice' },
{ from: 'Cost Price', to: 'costPrice' },
{ from: 'Cost Account', to: 'costAccountId' },
{ from: 'Sell Account', to: 'sellAccountId' },
{ from: 'Inventory Account', to: 'inventoryAccountId' },
{ from: 'Sell Description', to: 'sellDescription' },
{
from: 'Purchase Description',
to: 'purchaseDescription',
},
{ from: 'Note', to: 'note' },
{ from: 'Category', to: 'category' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `items.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Item';
}
}

View File

@@ -0,0 +1,36 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountManualJournals extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Date', to: 'date' },
{ from: 'Journal No', to: 'journalNumber' },
{ from: 'Reference No.', to: 'reference' },
{ from: 'Description', to: 'description' },
{ from: 'Publish', to: 'publish' },
{ from: 'Credit', to: 'credit', group: 'entries' },
{ from: 'Debit', to: 'debit', group: 'entries' },
{ from: 'Account', to: 'accountId', group: 'entries' },
{ from: 'Note', to: 'note', group: 'entries' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `manual-journals.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'ManualJournal';
}
}

View File

@@ -0,0 +1,37 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoSaleInvoices extends SeedDemoAbstract {
get mapping() {
return [
{ from: 'Invoice Date', to: 'invoiceDate' },
{ from: 'Due Date', to: 'dueDate' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Invoice No.', to: 'invoiceNo' },
{ from: 'Customer', to: 'customerId' },
{ from: 'Exchange Rate', to: 'exchangeRate' },
{ from: 'Invoice Message', to: 'invoiceMessage' },
{ from: 'Terms & Conditions', to: 'termsConditions' },
{ from: 'Delivered', to: 'delivered' },
{ from: 'Item', to: 'itemId', group: 'entries' },
{ from: 'Rate', to: 'rate', group: 'entries' },
{ from: 'Quantity', to: 'quantity', group: 'entries' },
{ from: 'Description', to: 'description', group: 'entries' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `sale-invoices.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'SaleInvoice';
}
}

View File

@@ -0,0 +1,36 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountVendors extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'First Name', to: 'firstName' },
{ from: 'Last Name', to: 'lastName' },
{ from: 'Display Name', to: 'displayName' },
{ from: 'Email', to: 'email' },
{ from: 'Work Phone Number', to: 'workPhone' },
{ from: 'Personal Phone Number', to: 'personalPhone' },
{ from: 'Company Name', to: 'companyName' },
{ from: 'Website', to: 'website' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `vendors.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Vendor';
}
}

View File

@@ -0,0 +1,25 @@
import { Inject, Service } from 'typedi';
import { CreateOneClickDemo } from './CreateOneClickDemo';
@Service()
export class OneClickDemoApplication {
@Inject()
private createOneClickDemoService: CreateOneClickDemo;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
*/
public createOneClick() {
return this.createOneClickDemoService.createOneClickDemo();
}
/**
* Auto-sign-in to created demo account.
* @param {string} demoId -
* @returns {Promise<IAuthSignInPOJO>}
*/
public autoSignIn(demoId: string) {
return this.createOneClickDemoService.autoSignIn(demoId);
}
}

View File

@@ -0,0 +1,12 @@
export const defaultDemoOrganizationDTO = {
name: 'BIGCAPITAL, INC',
baseCurrency: 'USD',
location: 'US',
language: 'en',
industry: 'Technology',
fiscalYear: 'march',
timezone: 'US/Central',
dateFormat: 'MM/DD/yyyy',
}

View File

@@ -0,0 +1,113 @@
import { Inject } from 'typedi';
import { promises as fs } from 'fs';
import path from 'path';
import uniqid from 'uniqid';
import { isEmpty } from 'lodash';
import events from '@/subscribers/events';
import { PromisePool } from '@supercharge/promise-pool';
import { IOrganizationBuiltEventPayload } from '@/interfaces';
import { SeedDemoAccountItems } from '../DemoSeeders/SeedDemoItems';
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
import { getImportsStoragePath } from '@/services/Import/_utils';
import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SeedDemoAccountCustomers } from '../DemoSeeders/SeedDemoCustomers';
import { SeedDemoAccountVendors } from '../DemoSeeders/SeedDemoVendors';
import { SeedDemoAccountManualJournals } from '../DemoSeeders/SeedDemoManualJournals';
import { SeedDemoAccountExpenses } from '../DemoSeeders/SeedDemoExpenses';
import { SeedDemoBankTransactions } from '../DemoSeeders/SeedDemoBankTransactions';
import { SeedDemoSaleInvoices } from '../DemoSeeders/SeedDemoSaleInvoices';
export class SeedInitialDemoAccountDataOnOrgBuild {
@Inject()
private importApp: ImportResourceApplication;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.organization.built,
this.seedInitialDemoAccountDataOnOrgBuild.bind(this)
);
};
/**
* Demo account seeder.
*/
get seedDemoAccountSeeders() {
return [
SeedDemoAccountItems,
SeedDemoBankTransactions,
SeedDemoAccountCustomers,
SeedDemoAccountVendors,
SeedDemoAccountManualJournals,
SeedDemoSaleInvoices,
SeedDemoAccountExpenses,
];
}
/**
* Initialize the seeder sheet file to the import storage first.
* @param {string} fileName -
* @returns {Promise<void>}
*/
async initiateSeederFile(fileName: string) {
const destFileName = uniqid();
const source = path.join(global.__views_dir, `/demo-sheets`, fileName);
const destination = path.join(getImportsStoragePath(), destFileName);
// Use the fs.promises.copyFile method to copy the file
await fs.copyFile(source, destination);
return destFileName;
}
/**
* Seeds initial demo account data on organization build
* @param {IOrganizationBuildEventPayload}
*/
async seedInitialDemoAccountDataOnOrgBuild({
tenantId,
}: IOrganizationBuiltEventPayload) {
const foundDemo = await OneClickDemo.query().findOne('tenantId', tenantId);
// Can't continue if the found demo is not exists.
// Means that account is not demo account.
if (!foundDemo) {
return null;
}
const results = await PromisePool.for(this.seedDemoAccountSeeders)
.withConcurrency(1)
.process(async (SeedDemoAccountSeeder) => {
const seederInstance = new SeedDemoAccountSeeder();
// Initialize the seeder sheet file before importing.
const importFileName = await this.initiateSeederFile(seederInstance.importFileName);
// Import the given seeder file.
const importedFile = await this.importApp.import(
tenantId,
seederInstance.resource,
importFileName,
seederInstance.importParams
);
// Mapping the columns with resource fields.
await this.importApp.mapping(
tenantId,
importedFile.import.importId,
seederInstance.mapping
);
await this.importApp.preview(tenantId, importedFile.import.importId);
// Commit the imported file.
await this.importApp.process(
tenantId,
importedFile.import.importId
);
});
if (!isEmpty(results.errors)) {
throw results.errors;
}
}
}

View File

@@ -0,0 +1,8 @@
import { IAuthSignInPOJO } from '@/interfaces';
export interface ICreateOneClickDemoPOJO {
email: string;
demoId: string;
signedIn: IAuthSignInPOJO;
buildJob: any;
}

View File

@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
import { import {
IOrganizationBuildDTO, IOrganizationBuildDTO,
IOrganizationBuildEventPayload, IOrganizationBuildEventPayload,
IOrganizationBuiltEventPayload,
IOrganizationUpdateDTO, IOrganizationUpdateDTO,
ISystemUser, ISystemUser,
ITenant, ITenant,
@@ -17,6 +18,8 @@ import { Tenant } from '@/system/models';
import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking'; import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
@Service() @Service()
export default class OrganizationService { export default class OrganizationService {
@@ -62,6 +65,10 @@ export default class OrganizationService {
// Migrated tenant. // Migrated tenant.
const migratedTenant = await tenant.$query().withGraphFetched('metadata'); const migratedTenant = await tenant.$query().withGraphFetched('metadata');
// Injects the given tenant IoC services.
await initalizeTenantServices(tenantId);
await initializeTenantSettings(tenantId);
// Creates a tenancy object from given tenant model. // Creates a tenancy object from given tenant model.
const tenancyContext = const tenancyContext =
this.tenantsManager.getSeedMigrationContext(migratedTenant); this.tenantsManager.getSeedMigrationContext(migratedTenant);
@@ -82,6 +89,11 @@ export default class OrganizationService {
// //
await this.flagTenantDBBatch(tenantId); await this.flagTenantDBBatch(tenantId);
// Triggers the organization built event.
await this.eventPublisher.emitAsync(events.organization.built, {
tenantId: tenant.id,
} as IOrganizationBuiltEventPayload)
} }
/** /**

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { BillPaymentsApplication } from './BillPaymentsApplication'; import { BillPaymentsApplication } from './BillPaymentsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class BillPaymentExportable extends Exportable { export class BillPaymentExportable extends Exportable {
@@ -13,12 +14,16 @@ export class BillPaymentExportable extends Exportable {
* @returns * @returns
*/ */
public exportable(tenantId: number, query: any) { public exportable(tenantId: number, query: any) {
const filterQuery = (builder) => {
builder.withGraphFetched('entries.bill');
};
const parsedQuery = { const parsedQuery = {
page: 1,
pageSize: 12,
...query,
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
filterQuery
} as any; } as any;
return this.billPaymentsApplication return this.billPaymentsApplication

View File

@@ -3,8 +3,8 @@ import * as R from 'ramda';
import { omit, sumBy } from 'lodash'; import { omit, sumBy } from 'lodash';
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces'; import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { formatDateFields } from '@/utils'; import { formatDateFields } from '@/utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export class CommandBillPaymentDTOTransformer { export class CommandBillPaymentDTOTransformer {
@@ -27,6 +27,12 @@ export class CommandBillPaymentDTOTransformer {
const amount = const amount =
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount'); billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
// Associate the default index to each item entry.
const entries = R.compose(
// Associate the default index to payment entries.
assocItemEntriesDefaultIndex
)(billPaymentDTO.entries);
const initialDTO = { const initialDTO = {
...formatDateFields(omit(billPaymentDTO, ['attachments']), [ ...formatDateFields(omit(billPaymentDTO, ['attachments']), [
'paymentDate', 'paymentDate',
@@ -34,7 +40,7 @@ export class CommandBillPaymentDTOTransformer {
amount, amount,
currencyCode: vendor.currencyCode, currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1, exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries, entries,
}; };
return R.compose( return R.compose(
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId) this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)

View File

@@ -52,6 +52,7 @@ export class GetBillPayments {
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
filter?.filterQuery && filter?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -3,7 +3,7 @@ import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import composeAsync from 'async/compose'; import composeAsync from 'async/compose';
import { formatDateFields } from 'utils'; import { assocDepthLevelToObjectTree, formatDateFields } from 'utils';
import { import {
IBillDTO, IBillDTO,
IBill, IBill,
@@ -15,6 +15,7 @@ import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export class BillDTOTransformer { export class BillDTOTransformer {
@@ -54,9 +55,9 @@ export class BillDTOTransformer {
/** /**
* Converts create bill DTO to model. * Converts create bill DTO to model.
* @param {number} tenantId * @param {number} tenantId
* @param {IBillDTO} billDTO * @param {IBillDTO} billDTO
* @param {IBill} oldBill * @param {IBill} oldBill
* @returns {IBill} * @returns {IBill}
*/ */
public async billDTOToModel( public async billDTOToModel(
@@ -92,7 +93,9 @@ export class BillDTOTransformer {
const entries = R.compose( const entries = R.compose(
// Remove tax code from entries. // Remove tax code from entries.
R.map(R.omit(['taxCode'])) R.map(R.omit(['taxCode'])),
// Associate the default index to each item entry line.
assocItemEntriesDefaultIndex
)(asyncEntries); )(asyncEntries);
const initialDTO = { const initialDTO = {

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IBillsFilter } from '@/interfaces'; import { IBillsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { BillsApplication } from './BillsApplication'; import { BillsApplication } from './BillsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class BillsExportable extends Exportable { export class BillsExportable extends Exportable {
@@ -19,7 +20,7 @@ export class BillsExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as IBillsFilter; } as IBillsFilter;
return this.billsApplication return this.billsApplication

View File

@@ -9,11 +9,13 @@ import {
IVendorCredit, IVendorCredit,
IVendorCreditCreateDTO, IVendorCreditCreateDTO,
IVendorCreditEditDTO, IVendorCreditEditDTO,
IVendorCreditEntryDTO,
} from '@/interfaces'; } from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService'; import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export default class BaseVendorCredit { export default class BaseVendorCredit {
@@ -50,10 +52,18 @@ export default class BaseVendorCredit {
const amount = this.itemsEntriesService.getTotalItemsEntries( const amount = this.itemsEntriesService.getTotalItemsEntries(
vendorCreditDTO.entries vendorCreditDTO.entries
); );
const entries = vendorCreditDTO.entries.map((entry) => ({
...entry, const entries = R.compose(
referenceType: 'VendorCredit', // Associate the default index to each item entry.
})); assocItemEntriesDefaultIndex,
// Associate the reference type to item entries.
R.map((entry: IVendorCreditEntryDTO) => ({
referenceType: 'VendorCredit',
...entry,
}))
)(vendorCreditDTO.entries);
// Retreive the next vendor credit number. // Retreive the next vendor credit number.
const autoNextNumber = this.getNextCreditNumber(tenantId); const autoNextNumber = this.getNextCreditNumber(tenantId);

View File

@@ -9,6 +9,7 @@ import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrat
import { formatDateFields } from '@/utils'; import { formatDateFields } from '@/utils';
import moment from 'moment'; import moment from 'moment';
import { SaleEstimateIncrement } from './SaleEstimateIncrement'; import { SaleEstimateIncrement } from './SaleEstimateIncrement';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export class SaleEstimateDTOTransformer { export class SaleEstimateDTOTransformer {
@@ -56,6 +57,14 @@ export class SaleEstimateDTOTransformer {
// Validate the sale estimate number require. // Validate the sale estimate number require.
this.validators.validateEstimateNoRequire(estimateNumber); this.validators.validateEstimateNoRequire(estimateNumber);
const entries = R.compose(
// Associate the reference type to item entries.
R.map((entry) => R.assoc('reference_type', 'SaleEstimate', entry)),
// Associate default index to item entries.
assocItemEntriesDefaultIndex
)(estimateDTO.entries);
const initialDTO = { const initialDTO = {
amount, amount,
...formatDateFields( ...formatDateFields(
@@ -65,10 +74,7 @@ export class SaleEstimateDTOTransformer {
currencyCode: paymentCustomer.currencyCode, currencyCode: paymentCustomer.currencyCode,
exchangeRate: estimateDTO.exchangeRate || 1, exchangeRate: estimateDTO.exchangeRate || 1,
...(estimateNumber ? { estimateNumber } : {}), ...(estimateNumber ? { estimateNumber } : {}),
entries: estimateDTO.entries.map((entry) => ({ entries,
reference_type: 'SaleEstimate',
...entry,
})),
// Avoid rewrite the deliver date in edit mode when already published. // Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered && ...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && { !oldSaleEstimate?.deliveredAt && {

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces'; import { ISalesInvoicesFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { SaleEstimatesApplication } from './SaleEstimatesApplication'; import { SaleEstimatesApplication } from './SaleEstimatesApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service() @Service()
export class SaleEstimatesExportable extends Exportable { export class SaleEstimatesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class SaleEstimatesExportable extends Exportable {
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: EXPORT_SIZE_LIMIT,
} as ISalesInvoicesFilter; } as ISalesInvoicesFilter;
return this.saleEstimatesApplication return this.saleEstimatesApplication

View File

@@ -17,6 +17,7 @@ import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement'; import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { ItemEntry } from '@/models'; import { ItemEntry } from '@/models';
@Service() @Service()
@@ -81,7 +82,10 @@ export class CommandSaleInvoiceDTOTransformer {
const entries = R.compose( const entries = R.compose(
// Remove tax code from entries. // Remove tax code from entries.
R.map(R.omit(['taxCode'])) R.map(R.omit(['taxCode'])),
// Associate the default index for each item entry lin.
assocItemEntriesDefaultIndex
)(asyncEntries); )(asyncEntries);
const initialDTO = { const initialDTO = {

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