Compare commits

...

61 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
1372a1f0a8 hotfix: fix the subscription plan when subscribe on cloud (#422) 2024-04-24 15:30:36 +02:00
Ahmed Bouhuolia
571a332658 Merge pull request #410 from bigcapitalhq/seed-free-subscription-to-tenants
feat: seed free subscription to tenants that have no subscription.
2024-04-19 09:36:35 +02:00
Ahmed Bouhuolia
b44c318a5d feat: seed free subscription to tenants that have no subscription. 2024-04-19 09:34:47 +02:00
Ahmed Bouhuolia
bd9717f4dc chore: Add Bigcapital Cloud link on README.md file 2024-04-17 19:51:05 +02:00
Ahmed Bouhuolia
f48aea8e5a feat: add the new env variables to docker compose 2024-04-17 19:21:35 +02:00
Ahmed Bouhuolia
0ac3a5dea9 fix: add /imports directory to storage dir 2024-04-17 18:25:17 +02:00
Ahmed Bouhuolia
56b40ad4cb fix: TS and linit errors 2024-04-17 17:51:21 +02:00
Ahmed Bouhuolia
9b6f934990 fix: add @lemonsqueezy/lemonsqueezy package dependencies. 2024-04-17 17:44:35 +02:00
Ahmed Bouhuolia
80e3522f8a Merge pull request #408 from bigcapitalhq/fix-import-store-absolute-path
fix: absolute storage imports path.
2024-04-17 17:37:39 +02:00
Ahmed Bouhuolia
7975643765 fix: absolute storage imports path. 2024-04-17 17:36:35 +02:00
Ahmed Bouhuolia
2ac7f86bdb Merge pull request #407 from bigcapitalhq/auto-subscribe-free
chore: add default value to env variable
2024-04-16 21:24:49 +02:00
Ahmed Bouhuolia
956b9b6812 chore: add default value to env variable 2024-04-16 21:22:40 +02:00
Ahmed Bouhuolia
60248ec3f6 Merge pull request #406 from bigcapitalhq/auto-subscribe-free
feat: auto subscribe to free plan once signup on community version.
2024-04-16 20:58:29 +02:00
Ahmed Bouhuolia
9d3f1541eb feat: auto subscribe to free plan once signup on community version. 2024-04-16 20:57:05 +02:00
Ahmed Bouhuolia
9b5f1a36ab Merge branch 'main' into develop 2024-04-16 12:58:22 +02:00
Ahmed Bouhuolia
8ee691e1ed Merge pull request #405 from bigcapitalhq/subscription-page-content
feat: subscription page content
2024-04-16 12:55:57 +02:00
Ahmed Bouhuolia
f9cb14da9e feat: subscription page content 2024-04-16 12:54:36 +02:00
Ahmed Bouhuolia
5e87581f4e hotfix: creating a vendor 2024-04-15 22:48:54 +02:00
Ahmed Bouhuolia
8ca9cf39da Merge pull request #404 from bigcapitalhq/optimize-ui-onboarding
feat: optimize the onboarding subscription experience.
2024-04-15 14:53:52 +02:00
Ahmed Bouhuolia
9001fea524 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-04-15 14:51:19 +02:00
Ahmed Bouhuolia
dea0d71732 Merge pull request #402 from bigcapitalhq/lemon-squeezy-payment
feat: Integrate Lemon Squeezy payment
2024-04-15 14:49:39 +02:00
Ahmed Bouhuolia
c191c4bd26 feat: remove other payment methods 2024-04-15 14:49:27 +02:00
Ahmed Bouhuolia
47d82ce591 feat: optimize the onboarding subscription experience. 2024-04-15 12:48:16 +02:00
Ahmed Bouhuolia
9321db2a3a feat: sweep up lemon squeezy webhooks. 2024-04-14 12:58:53 +02:00
Ahmed Bouhuolia
e486333c96 feat: sweep up the Lemon Squeezy integration 2024-04-14 12:44:02 +02:00
Ahmed Bouhuolia
a9748b23c0 feat: listen LemonSqueezy webhooks 2024-04-14 11:55:36 +02:00
Ahmed Bouhuolia
693ae61141 feat: integrate LemonSqueezy to subscription payment 2024-04-14 10:33:29 +02:00
Ahmed Bouhuolia
9807ac04b0 Revert "feat(webapp): deprecate the subscription step in onboarding process"
This reverts commit 0c1bf302e5.
2024-04-13 15:18:59 +02:00
Ahmed Bouhuolia
bddfde4138 Revert "feat(webapp): deprecate the subscription step in onboarding process"
This reverts commit 0c1bf302e5.
2024-04-13 14:07:32 +02:00
Ahmed Bouhuolia
a39dcd00d5 Revert "feat(server): deprecated the subscription module."
This reverts commit 3b79ac66ae.
2024-04-13 11:05:53 +02:00
Ahmed Bouhuolia
4d616e9287 Revert "feat(server): deprecated the subscription module."
This reverts commit 44fc26b156.
2024-04-13 10:17:48 +02:00
Ahmed Bouhuolia
dc52fb1de5 fix: lint error 2024-04-09 22:24:03 +02:00
Ahmed Bouhuolia
21a1777424 Merge branch 'release/v0.15.2' into develop 2024-04-09 22:11:10 +02:00
Ahmed Bouhuolia
16b721db91 Merge pull request #401 from bigcapitalhq/import-fields-hints
feat: add hints to import fields
2024-04-09 22:01:16 +02:00
Ahmed Bouhuolia
079491823d feat: add hints to import fields 2024-04-09 22:00:04 +02:00
Ahmed Bouhuolia
f7a87a6e9c Merge pull request #400 from bigcapitalhq/clean-up-templ-import-files
feat: clean up the imported temp files
2024-04-09 00:16:36 +02:00
Ahmed Bouhuolia
e0cdf42980 chore: update the server .gitignore 2024-04-09 00:13:58 +02:00
Ahmed Bouhuolia
ee56653f4b fix: remove the import table from tenants dbs 2024-04-09 00:13:32 +02:00
Ahmed Bouhuolia
2310b09778 fix: delete imported file if error has thrown 2024-04-09 00:11:34 +02:00
Ahmed Bouhuolia
0684e50ebd feat: clean up the imported temp files 2024-04-09 00:09:32 +02:00
Ahmed Bouhuolia
aaa8f39e50 feat: add section label to import mapping 2024-04-08 00:39:18 +02:00
Ahmed Bouhuolia
af981ce630 Merge pull request #396 from bigcapitalhq/aggregate-rows-import
feat: Aggregate rows import
2024-04-07 23:55:06 +02:00
Ahmed Bouhuolia
a1f8417b5d feat: revert the resource columns 2024-04-07 23:48:23 +02:00
Ahmed Bouhuolia
086b060351 feat: add sample import sheet to invoices 2024-04-07 15:26:23 +02:00
Ahmed Bouhuolia
bbafdcd8bd feat: configuring import services on more resources 2024-04-06 04:06:15 +02:00
Ahmed Bouhuolia
dd9098bdc1 feat: more resources support importing 2024-04-05 02:00:12 +02:00
Ahmed Bouhuolia
3851d34ba4 feat: aggregate rows on import feature 2024-04-04 05:01:09 +02:00
Ahmed Bouhuolia
b9651f30d5 chore: remove console.log 2024-04-01 04:40:40 +02:00
Ahmed Bouhuolia
45b5fb4088 fix: syntax error 2024-04-01 03:06:01 +02:00
Ahmed Bouhuolia
aa64bcf69b Merge pull request #393 from bigcapitalhq/import-relations-mapping
feat: linking relation with id in importing
2024-04-01 03:02:04 +02:00
Ahmed Bouhuolia
cbd867b334 Merge branch 'develop' into import-relations-mapping 2024-04-01 03:01:24 +02:00
Ahmed Bouhuolia
1a8ca83786 Merge pull request #395 from bigcapitalhq/validate-imported-sheet-empty
feat: validate the given imported sheet whether is empty
2024-04-01 02:58:41 +02:00
Ahmed Bouhuolia
80c14ba1a0 Merge branch 'develop' into validate-imported-sheet-empty 2024-04-01 02:58:33 +02:00
Ahmed Bouhuolia
785045dbad feat: validate the given imported sheet whether is empty 2024-04-01 02:57:30 +02:00
Ahmed Bouhuolia
291301c1e3 Merge pull request #394 from bigcapitalhq/advanced-import-parser
feat: advanced parser for numeric and boolean import values
2024-04-01 02:27:34 +02:00
Ahmed Bouhuolia
824e4e13d1 feat: advanced parser for numeric and boolean import values 2024-04-01 02:26:08 +02:00
Ahmed Bouhuolia
74da28b464 feat: linking relation with id in importing 2024-04-01 01:13:31 +02:00
Ahmed Bouhuolia
22a016b56e Merge pull request #392 from bigcapitalhq/import-show-unique-value-preview
fix: show the unique row value in the import preview
2024-03-28 05:39:40 +02:00
Ahmed Bouhuolia
040f016273 fix: show the unique row value in the import preview 2024-03-28 05:38:24 +02:00
Ahmed Bouhuolia
8ab809fc71 Merge pull request #389 from bigcapitalhq/accounts-bank-transactions-demo-sheet
feat: add sample sheet to accounts and bank transactions
2024-03-28 00:57:58 +02:00
Ahmed Bouhuolia
2baa667c5d fix(webapp): hotfix pdf request hook 2024-03-19 05:22:15 +02:00
219 changed files with 6120 additions and 1778 deletions

View File

@@ -95,3 +95,8 @@ PLAID_LINK_WEBHOOK=
PLAID_SANDBOX_REDIRECT_URI= PLAID_SANDBOX_REDIRECT_URI=
PLAID_DEVELOPMENT_REDIRECT_URI= PLAID_DEVELOPMENT_REDIRECT_URI=
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET=

View File

@@ -25,6 +25,10 @@
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" /> <img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
</a> </a>
</p> </p>
<p align="center">
<a href="https://app.bigcapital.ly">Bigcapital Cloud</a>
</p>
</p> </p>
# What's Bigcapital? # What's Bigcapital?

View File

@@ -21,16 +21,12 @@ services:
depends_on: depends_on:
- server - server
- webapp - webapp
deploy: restart: on-failure
restart_policy:
condition: unless-stopped
webapp: webapp:
container_name: bigcapital-webapp container_name: bigcapital-webapp
image: ghcr.io/bigcapitalhq/webapp:latest image: ghcr.io/bigcapitalhq/webapp:latest
deploy: restart: on-failure
restart_policy:
condition: unless-stopped
server: server:
container_name: bigcapital-server container_name: bigcapital-server
@@ -45,9 +41,7 @@ services:
- mysql - mysql
- mongo - mongo
- redis - redis
deploy: restart: on-failure
restart_policy:
condition: unless-stopped
environment: environment:
# Mail # Mail
- MAIL_HOST=${MAIL_HOST} - MAIL_HOST=${MAIL_HOST}
@@ -92,6 +86,12 @@ services:
- GOTENBERG_URL=${GOTENBERG_URL} - GOTENBERG_URL=${GOTENBERG_URL}
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL} - GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
# Lemon Squeez
- LEMONSQUEEZY_API_KEY=${LEMONSQUEEZY_API_KEY}
- LEMONSQUEEZY_STORE_ID=${LEMONSQUEEZY_STORE_ID}
- LEMONSQUEEZY_WEBHOOK_SECRET=${LEMONSQUEEZY_WEBHOOK_SECRET}
- HOSTED_ON_BIGCAPITAL_CLOUD=${HOSTED_ON_BIGCAPITAL_CLOUD}
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration
build: build:
@@ -111,9 +111,7 @@ services:
mysql: mysql:
container_name: bigcapital-mysql container_name: bigcapital-mysql
deploy: restart: on-failure
restart_policy:
condition: unless-stopped
build: build:
context: ./docker/mariadb context: ./docker/mariadb
environment: environment:
@@ -128,9 +126,7 @@ services:
mongo: mongo:
container_name: bigcapital-mongo container_name: bigcapital-mongo
deploy: restart: on-failure
restart_policy:
condition: unless-stopped
build: ./docker/mongo build: ./docker/mongo
expose: expose:
- '27017' - '27017'
@@ -139,9 +135,7 @@ services:
redis: redis:
container_name: bigcapital-redis container_name: bigcapital-redis
deploy: restart: on-failure
restart_policy:
condition: unless-stopped
build: build:
context: ./docker/redis context: ./docker/redis
expose: expose:

View File

@@ -3,3 +3,4 @@
stdout.log stdout.log
/dist /dist
/build /build
/public/imports

View File

@@ -22,6 +22,7 @@
"dependencies": { "dependencies": {
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3", "@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@types/i18n": "^0.8.7", "@types/i18n": "^0.8.7",
"@types/knex": "^0.16.1", "@types/knex": "^0.16.1",
"@types/mathjs": "^6.0.12", "@types/mathjs": "^6.0.12",
@@ -89,17 +90,17 @@
"objection-filter": "^4.0.1", "objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7", "objection-soft-delete": "^1.0.7",
"objection-unique": "^1.2.2", "objection-unique": "^1.2.2",
"plaid": "^10.3.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"puppeteer": "^10.2.0", "puppeteer": "^10.2.0",
"plaid": "^10.3.0",
"qim": "0.0.52", "qim": "0.0.52",
"ramda": "^0.27.1", "ramda": "^0.27.1",
"rate-limiter-flexible": "^2.1.14", "rate-limiter-flexible": "^2.1.14",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rtl-detect": "^1.0.4", "rtl-detect": "^1.0.4",
"source-map-loader": "^4.0.1",
"socket.io": "^4.7.4", "socket.io": "^4.7.4",
"source-map-loader": "^4.0.1",
"tmp-promise": "^3.0.3", "tmp-promise": "^3.0.3",
"ts-transformer-keys": "^0.4.2", "ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0", "tsyringe": "^4.3.0",

BIN
packages/server/public/.DS_Store vendored Normal file

Binary file not shown.

BIN
packages/server/public/imports/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -244,26 +244,28 @@
"account.field.active": "Active", "account.field.active": "Active",
"account.field.currency": "Currency", "account.field.currency": "Currency",
"account.field.balance": "Balance", "account.field.balance": "Balance",
"account.field.parent_account": "Parent Account",
"account.field.created_at": "Created at", "account.field.created_at": "Created at",
"item.field.type": "Item type", "item.field.type": "Item Type",
"item.field.type.inventory": "Inventory", "item.field.type.inventory": "Inventory",
"item.field.type.service": "Service", "item.field.type.service": "Service",
"item.field.type.non-inventory": "Non inventory", "item.field.type.non-inventory": "Non Inventory",
"item.field.name": "Name", "item.field.name": "Item Name",
"item.field.code": "Code", "item.field.code": "Item Code",
"item.field.sellable": "Sellable", "item.field.sellable": "Sellable",
"item.field.purchasable": "Purchasable", "item.field.purchasable": "Purchasable",
"item.field.cost_price": "Cost price", "item.field.cost_price": "Cost Price",
"item.field.cost_account": "Cost account", "item.field.sell_price": "Sell Price",
"item.field.sell_account": "Sell account", "item.field.cost_account": "Cost Account",
"item.field.sell_description": "Sell description", "item.field.sell_account": "Sell Account",
"item.field.inventory_account": "Inventory account", "item.field.sell_description": "Sell Description",
"item.field.purchase_description": "Purchase description", "item.field.inventory_account": "Inventory Account",
"item.field.quantity_on_hand": "Quantity on hand", "item.field.purchase_description": "Purchase Description",
"item.field.quantity_on_hand": "Quantity on Hand",
"item.field.note": "Note", "item.field.note": "Note",
"item.field.category": "Category", "item.field.category": "Category",
"item.field.active": "Active", "item.field.active": "Active",
"item.field.created_at": "Created at", "item.field.created_at": "Created At",
"item_category.field.name": "Name", "item_category.field.name": "Name",
"item_category.field.description": "Description", "item_category.field.description": "Description",
"item_category.field.count": "Count", "item_category.field.count": "Count",
@@ -276,8 +278,14 @@
"invoice.field.invoice_message": "Invoice message", "invoice.field.invoice_message": "Invoice message",
"invoice.field.terms_conditions": "Terms & conditions", "invoice.field.terms_conditions": "Terms & conditions",
"invoice.field.amount": "Amount", "invoice.field.amount": "Amount",
"invoice.field.exchange_rate": "Exchange Rate",
"invoice.field.payment_amount": "Payment amount", "invoice.field.payment_amount": "Payment amount",
"invoice.field.due_amount": "Due amount", "invoice.field.due_amount": "Due amount",
"invoice.field.delivered": "Delivered",
"invoice.field.item_name": "Item Name",
"invoice.field.rate": "Rate",
"invoice.field.quantity": "Quantity",
"invoice.field.description": "Description",
"invoice.field.status": "Status", "invoice.field.status": "Status",
"invoice.field.status.paid": "Paid", "invoice.field.status.paid": "Paid",
"invoice.field.status.partially-paid": "Partially paid", "invoice.field.status.partially-paid": "Partially paid",
@@ -286,6 +294,8 @@
"invoice.field.status.delivered": "Delivered", "invoice.field.status.delivered": "Delivered",
"invoice.field.status.draft": "Draft", "invoice.field.status.draft": "Draft",
"invoice.field.created_at": "Created at", "invoice.field.created_at": "Created at",
"invoice.field.currency": "Currency",
"invoice.field.entries": "Entries",
"estimate.field.amount": "Amount", "estimate.field.amount": "Amount",
"estimate.field.estimate_number": "Estimate number", "estimate.field.estimate_number": "Estimate number",
"estimate.field.customer": "Customer", "estimate.field.customer": "Customer",
@@ -300,22 +310,31 @@
"estimate.field.status.approved": "Approved", "estimate.field.status.approved": "Approved",
"estimate.field.status.draft": "Draft", "estimate.field.status.draft": "Draft",
"estimate.field.created_at": "Created at", "estimate.field.created_at": "Created at",
"payment_receive.field.customer": "Customer",
"payment_receive.field.payment_date": "Payment date",
"payment_receive.field.amount": "Amount", "payment_receive.field.amount": "Amount",
"payment_receive.field.reference_no": "Reference No.",
"payment_receive.field.deposit_account": "Deposit account",
"payment_receive.field.payment_receive_no": "Payment receive No.", "payment_receive.field.payment_receive_no": "Payment receive No.",
"payment_receive.field.statement": "Statement", "payment_receive.field.statement": "Statement",
"payment_receive.field.created_at": "Created at", "payment_receive.field.created_at": "Created at",
"payment_receive.field.customer": "Customer",
"payment_receive.field.exchange_rate": "Exchange Rate",
"payment_receive.field.payment_date": "Payment Date",
"payment_receive.field.reference_no": "Reference No.",
"payment_receive.field.deposit_account": "Deposit Account",
"payment_receive.field.entries": "Entries",
"payment_receive.field.invoice": "Invoice",
"payment_receive.field.entries.payment_amount": "Payment Amount",
"bill_payment.field.vendor": "Vendor", "bill_payment.field.vendor": "Vendor",
"bill_payment.field.amount": "Amount", "bill_payment.field.amount": "Amount",
"bill_payment.field.due_amount": "Due amount", "bill_payment.field.due_amount": "Due Amount",
"bill_payment.field.payment_account": "Payment account", "bill_payment.field.payment_account": "Payment Account",
"bill_payment.field.payment_number": "Payment number", "bill_payment.field.payment_number": "Payment No.",
"bill_payment.field.payment_date": "Payment date", "bill_payment.field.payment_date": "Payment Date",
"bill_payment.field.reference_no": "Reference No.", "bill_payment.field.reference_no": "Reference No.",
"bill_payment.field.description": "Description", "bill_payment.field.description": "Description",
"bill_payment.field.exchange_rate": "Exchange Rate",
"bill_payment.field.statement": "Statement",
"bill_payment.field.entries.bill": "Bill No.",
"bill_payment.field.entries.payment_amount": "Payment Amount",
"bill_payment.field.reference": "Reference No.",
"bill_payment.field.created_at": "Created at", "bill_payment.field.created_at": "Created at",
"bill.field.vendor": "Vendor", "bill.field.vendor": "Vendor",
"bill.field.bill_number": "Bill number", "bill.field.bill_number": "Bill number",
@@ -343,22 +362,30 @@
"inventory_adjustment.field.description": "Description", "inventory_adjustment.field.description": "Description",
"inventory_adjustment.field.published_at": "Published at", "inventory_adjustment.field.published_at": "Published at",
"inventory_adjustment.field.created_at": "Created at", "inventory_adjustment.field.created_at": "Created at",
"expense.field.payment_date": "Payment date", "expense.field.payment_date": "Payment Date",
"expense.field.payment_account": "Payment account", "expense.field.payment_account": "Payment Account",
"expense.field.amount": "Amount", "expense.field.amount": "Amount",
"expense.field.currency_code": "Currency",
"expense.field.exchange_rate": "Exchange Rate",
"expense.field.reference_no": "Reference No.", "expense.field.reference_no": "Reference No.",
"expense.field.description": "Description", "expense.field.description": "Description",
"expense.field.line_description": "Line Description",
"expense.field.published": "Published", "expense.field.published": "Published",
"expense.field.categories": "Categories",
"expense.field.expense_account": "Expense Account",
"expense.field.publish": "Publish",
"expense.field.status": "Status", "expense.field.status": "Status",
"expense.field.status.draft": "Draft", "expense.field.status.draft": "Draft",
"expense.field.status.published": "Published", "expense.field.status.published": "Published",
"expense.field.created_at": "Created at", "expense.field.created_at": "Created at",
"manual_journal.field.date": "Date", "manual_journal.field.date": "Date",
"manual_journal.field.journal_number": "Journal number", "manual_journal.field.journal_number": "Journal No.",
"manual_journal.field.reference": "Reference No.", "manual_journal.field.reference": "Reference No.",
"manual_journal.field.journal_type": "Journal type", "manual_journal.field.journal_type": "Journal Type",
"manual_journal.field.amount": "Amount", "manual_journal.field.amount": "Amount",
"manual_journal.field.description": "Description", "manual_journal.field.description": "Description",
"manual_journal.field.currency": "Currency",
"manual_journal.field.exchange_rate": "Exchange Rate",
"manual_journal.field.status": "Status", "manual_journal.field.status": "Status",
"manual_journal.field.created_at": "Created at", "manual_journal.field.created_at": "Created at",
"receipt.field.amount": "Amount", "receipt.field.amount": "Amount",
@@ -411,6 +438,8 @@
"vendor.field.status.unpaid": "Unpaid", "vendor.field.status.unpaid": "Unpaid",
"Invoice write-off": "Invoice write-off", "Invoice write-off": "Invoice write-off",
"transaction_type.credit_note": "Credit note", "transaction_type.credit_note": "Credit note",
"transaction_type.refund_credit_note": "Refund credit note", "transaction_type.refund_credit_note": "Refund credit note",
"transaction_type.vendor_credit": "Vendor credit", "transaction_type.vendor_credit": "Vendor credit",

View File

@@ -349,7 +349,7 @@ export default class AccountsController extends BaseController {
// Filter query. // Filter query.
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'createdAt', columnSortBy: 'created_at',
inactiveMode: false, inactiveMode: false,
structure: IAccountsStructureType.Tree, structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req), ...this.matchedQueryData(req),

View File

@@ -289,7 +289,7 @@ export default class CustomersController extends ContactsController {
const filter = { const filter = {
inactiveMode: false, inactiveMode: false,
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'createdAt', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),

View File

@@ -144,10 +144,8 @@ export default class VendorsController extends ContactsController {
try { try {
const vendor = await this.vendorsApplication.createVendor( const vendor = await this.vendorsApplication.createVendor(
tenantId, tenantId,
contactDTO, contactDTO
user
); );
return res.status(200).send({ return res.status(200).send({
id: vendor.id, id: vendor.id,
message: 'The vendor has been created successfully.', message: 'The vendor has been created successfully.',
@@ -272,7 +270,7 @@ export default class VendorsController extends ContactsController {
const vendorsFilter: IVendorsFilter = { const vendorsFilter: IVendorsFilter = {
inactiveMode: false, inactiveMode: false,
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'createdAt', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),

View File

@@ -37,6 +37,7 @@ export class ImportController extends BaseController {
[ [
param('import_id').exists().isString(), param('import_id').exists().isString(),
body('mapping').exists().isArray({ min: 1 }), body('mapping').exists().isArray({ min: 1 }),
body('mapping.*.group').optional(),
body('mapping.*.from').exists(), body('mapping.*.from').exists(),
body('mapping.*.to').exists(), body('mapping.*.to').exists(),
], ],
@@ -47,6 +48,7 @@ export class ImportController extends BaseController {
router.get( router.get(
'/sample', '/sample',
[query('resource').exists(), query('format').optional()], [query('resource').exists(), query('format').optional()],
this.validationResult,
this.downloadImportSample.bind(this), this.downloadImportSample.bind(this),
this.catchServiceErrors this.catchServiceErrors
); );

View File

@@ -1,21 +1,34 @@
import Multer from 'multer'; import Multer from 'multer';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { getImportsStoragePath } from '@/services/Import/_utils';
export function allowSheetExtensions(req, file, cb) { export function allowSheetExtensions(req, file, cb) {
if ( if (
file.mimetype !== 'text/csv' && file.mimetype !== 'text/csv' &&
file.mimetype !== 'application/vnd.ms-excel' && file.mimetype !== 'application/vnd.ms-excel' &&
file.mimetype !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' file.mimetype !==
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) { ) {
cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID')); cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID'));
return; return;
} }
cb(null, true); cb(null, true);
} }
const storage = Multer.diskStorage({
destination: function (req, file, cb) {
const path = getImportsStoragePath();
cb(null, path);
},
filename: function (req, file, cb) {
// Add the creation timestamp to clean up temp files later.
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix);
},
});
export const uploadImportFile = Multer({ export const uploadImportFile = Multer({
dest: './public/imports', storage,
limits: { fileSize: 5 * 1024 * 1024 }, limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: allowSheetExtensions, fileFilter: allowSheetExtensions,
}); });

View File

@@ -6,7 +6,7 @@ import ItemTransactionsController from './ItemsTransactions';
@Service() @Service()
export default class ItemsBaseController { export default class ItemsBaseController {
router() { public router() {
const router = Router(); const router = Router();
router.use('/', Container.get(ItemsController).router()); router.use('/', Container.get(ItemsController).router());

View File

@@ -6,6 +6,7 @@ import { check, ValidationChain } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import JWTAuth from '@/api/middleware/jwtAuth'; import JWTAuth from '@/api/middleware/jwtAuth';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware'; import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser'; import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import OrganizationService from '@/services/Organization/OrganizationService'; import OrganizationService from '@/services/Organization/OrganizationService';
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants'; import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
@@ -17,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
@Service() @Service()
export default class OrganizationController extends BaseController { export default class OrganizationController extends BaseController {
@Inject() @Inject()
private organizationService: OrganizationService; organizationService: OrganizationService;
/** /**
* Router constructor. * Router constructor.
@@ -25,10 +26,13 @@ export default class OrganizationController extends BaseController {
router() { router() {
const router = Router(); const router = Router();
// Should before build tenant database the user be authorized and
// most important than that, should be subscribed to any plan.
router.use(JWTAuth); router.use(JWTAuth);
router.use(AttachCurrentTenantUser); router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware); router.use(TenancyMiddleware);
router.use('/build', SubscriptionMiddleware('main'));
router.post( router.post(
'/build', '/build',
this.buildOrganizationValidationSchema, this.buildOrganizationValidationSchema,

View File

@@ -297,8 +297,7 @@ export default class VendorCreditController extends BaseController {
try { try {
const vendorCredit = await this.createVendorCreditService.newVendorCredit( const vendorCredit = await this.createVendorCreditService.newVendorCredit(
tenantId, tenantId,
vendorCreditCreateDTO, vendorCreditCreateDTO
user
); );
return res.status(200).send({ return res.status(200).send({

View File

@@ -338,8 +338,7 @@ export default class PaymentReceivesController extends BaseController {
try { try {
const creditNote = await this.createCreditNoteService.newCreditNote( const creditNote = await this.createCreditNoteService.newCreditNote(
tenantId, tenantId,
creditNoteDTO, creditNoteDTO
user
); );
return res.status(200).send({ return res.status(200).send({
id: creditNote.id, id: creditNote.id,

View File

@@ -0,0 +1,88 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import JWTAuth from '@/api/middleware/jwtAuth';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import SubscriptionService from '@/services/Subscription/SubscriptionService';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '../BaseController';
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
@Service()
export class SubscriptionController extends BaseController {
@Inject()
private subscriptionService: SubscriptionService;
@Inject()
private lemonSqueezyService: LemonSqueezyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.use(JWTAuth);
router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware);
router.post(
'/lemon/checkout_url',
[body('variantId').exists().trim()],
this.validationResult,
this.getCheckoutUrl.bind(this)
);
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
return router;
}
/**
* Retrieve all subscriptions of the authenticated user's tenant.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getSubscriptions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const subscriptions = await this.subscriptionService.getSubscriptions(
tenantId
);
return res.status(200).send({ subscriptions });
} catch (error) {
next(error);
}
}
/**
* Retrieves the LemonSqueezy checkout url.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getCheckoutUrl(
req: Request,
res: Response,
next: NextFunction
) {
const { variantId } = this.matchedBodyData(req);
const { user } = req;
try {
const checkout = await this.lemonSqueezyService.getCheckout(
variantId,
user
);
return res.status(200).send(checkout);
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1 @@
export * from './SubscriptionController';

View File

@@ -3,6 +3,7 @@ import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware'; import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
@Service() @Service()
@@ -10,18 +11,39 @@ export class Webhooks extends BaseController {
@Inject() @Inject()
private plaidApp: PlaidApplication; private plaidApp: PlaidApplication;
@Inject()
private lemonWebhooksService: LemonSqueezyWebhooks;
/** /**
* Router constructor. * Router constructor.
*/ */
router() { router() {
const router = Router(); const router = Router();
router.use(PlaidWebhookTenantBootMiddleware); router.use('/plaid', PlaidWebhookTenantBootMiddleware);
router.post('/plaid', this.plaidWebhooks.bind(this)); router.post('/plaid', this.plaidWebhooks.bind(this));
router.post('/lemon', this.lemonWebhooks.bind(this));
return router; return router;
} }
/**
* Listens to Lemon Squeezy webhooks events.
* @param {Request} req
* @param {Response} res
* @returns {Response}
*/
public async lemonWebhooks(req: Request, res: Response) {
const data = req.body;
const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody;
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature);
return res.status(200).send();
}
/** /**
* Listens to Plaid webhooks. * Listens to Plaid webhooks.
* @param {Request} req * @param {Request} req

View File

@@ -4,6 +4,7 @@ import { Container } from 'typedi';
// Middlewares // Middlewares
import JWTAuth from '@/api/middleware/jwtAuth'; import JWTAuth from '@/api/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser'; import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware'; import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized'; import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware'; import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
@@ -36,6 +37,7 @@ import Resources from './controllers/Resources';
import ExchangeRates from '@/api/controllers/ExchangeRates'; import ExchangeRates from '@/api/controllers/ExchangeRates';
import Media from '@/api/controllers/Media'; import Media from '@/api/controllers/Media';
import Ping from '@/api/controllers/Ping'; import Ping from '@/api/controllers/Ping';
import { SubscriptionController } from '@/api/controllers/Subscription';
import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments'; import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware'; import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
import Jobs from './controllers/Jobs'; import Jobs from './controllers/Jobs';
@@ -70,6 +72,7 @@ export default () => {
app.use('/auth', Container.get(Authentication).router()); app.use('/auth', Container.get(Authentication).router());
app.use('/invite', Container.get(InviteUsers).nonAuthRouter()); app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
app.use('/subscription', Container.get(SubscriptionController).router());
app.use('/organization', Container.get(Organization).router()); app.use('/organization', Container.get(Organization).router());
app.use('/ping', Container.get(Ping).router()); app.use('/ping', Container.get(Ping).router());
app.use('/jobs', Container.get(Jobs).router()); app.use('/jobs', Container.get(Jobs).router());
@@ -83,6 +86,7 @@ export default () => {
dashboard.use(JWTAuth); dashboard.use(JWTAuth);
dashboard.use(AttachCurrentTenantUser); dashboard.use(AttachCurrentTenantUser);
dashboard.use(TenancyMiddleware); dashboard.use(TenancyMiddleware);
dashboard.use(SubscriptionMiddleware('main'));
dashboard.use(EnsureTenantIsInitialized); dashboard.use(EnsureTenantIsInitialized);
dashboard.use(SettingsMiddleware); dashboard.use(SettingsMiddleware);
dashboard.use(I18nAuthenticatedMiddlware); dashboard.use(I18nAuthenticatedMiddlware);
@@ -136,12 +140,10 @@ export default () => {
dashboard.use('/warehouses', Container.get(WarehousesController).router()); dashboard.use('/warehouses', Container.get(WarehousesController).router());
dashboard.use('/projects', Container.get(ProjectsController).router()); dashboard.use('/projects', Container.get(ProjectsController).router());
dashboard.use('/tax-rates', Container.get(TaxRatesController).router()); dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
dashboard.use('/import', Container.get(ImportController).router()); dashboard.use('/import', Container.get(ImportController).router());
dashboard.use('/', Container.get(ProjectTasksController).router()); dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router()); dashboard.use('/', Container.get(ProjectTimesController).router());
dashboard.use('/', Container.get(WarehousesItemController).router()); dashboard.use('/', Container.get(WarehousesItemController).router());
dashboard.use('/dashboard', Container.get(DashboardController).router()); dashboard.use('/dashboard', Container.get(DashboardController).router());

View File

@@ -0,0 +1,29 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
export default (subscriptionSlug = 'main') =>
async (req: Request, res: Response, next: NextFunction) => {
const { tenant, tenantId } = req;
const { subscriptionRepository } = Container.get('repositories');
if (!tenant) {
throw new Error('Should load `TenancyMiddlware` before this middleware.');
}
const subscription = await subscriptionRepository.getBySlugInTenant(
subscriptionSlug,
tenantId
);
// Validate in case there is no any already subscription.
if (!subscription) {
return res.boom.badRequest('Tenant has no subscription.', {
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
}
// Validate in case the subscription is inactive.
else if (subscription.inactive()) {
return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
});
}
next();
};

View File

@@ -1,6 +1,6 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { toInteger } from 'lodash'; import { defaultTo, toInteger } from 'lodash';
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils'; import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
dotenv.config(); dotenv.config();
@@ -190,6 +190,24 @@ module.exports = {
secretSandbox: process.env.PLAID_SECRET_SANDBOX, secretSandbox: process.env.PLAID_SECRET_SANDBOX,
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI, redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI, redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
linkWebhook: process.env.PLAID_LINK_WEBHOOK linkWebhook: process.env.PLAID_LINK_WEBHOOK,
}, },
/**
* Lemon Squeezy.
*/
lemonSqueezy: {
key: process.env.LEMONSQUEEZY_API_KEY,
storeId: process.env.LEMONSQUEEZY_STORE_ID,
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
},
/**
* Bigcapital (Cloud).
* NOTE: DO NOT CHANGE THIS OPTION OR ADD THIS ENV VAR.
*/
hostedOnBigcapitalCloud: parseBoolean(
defaultTo(process.env.HOSTED_ON_BIGCAPITAL_CLOUD, false),
false
),
}; };

View File

@@ -0,0 +1,8 @@
export default class NotAllowedChangeSubscriptionPlan {
constructor() {
this.name = "NotAllowedChangeSubscriptionPlan";
}
}

View File

@@ -1,3 +1,4 @@
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
import ServiceError from './ServiceError'; import ServiceError from './ServiceError';
import ServiceErrors from './ServiceErrors'; import ServiceErrors from './ServiceErrors';
import TenantAlreadyInitialized from './TenantAlreadyInitialized'; import TenantAlreadyInitialized from './TenantAlreadyInitialized';
@@ -6,6 +7,7 @@ import TenantDBAlreadyExists from './TenantDBAlreadyExists';
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt'; import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
export { export {
NotAllowedChangeSubscriptionPlan,
ServiceError, ServiceError,
ServiceErrors, ServiceErrors,
TenantAlreadyInitialized, TenantAlreadyInitialized,

View File

@@ -32,11 +32,13 @@ export interface IModelMetaFieldCommon {
name: string; name: string;
column: string; column: string;
columnable?: boolean; columnable?: boolean;
fieldType: IModelColumnType;
customQuery?: Function; customQuery?: Function;
required?: boolean; required?: boolean;
importHint?: string; importHint?: string;
importableRelationLabel?: string;
order?: number; order?: number;
unique?: number;
dataTransferObjectKey?: string;
} }
export interface IModelMetaFieldText { export interface IModelMetaFieldText {
@@ -67,6 +69,7 @@ export type IModelMetaField = IModelMetaFieldCommon &
| IModelMetaFieldUrl | IModelMetaFieldUrl
| IModelMetaEnumerationField | IModelMetaEnumerationField
| IModelMetaRelationField | IModelMetaRelationField
| IModelMetaCollectionField
); );
export interface IModelMetaEnumerationOption { export interface IModelMetaEnumerationOption {
@@ -90,12 +93,71 @@ export interface IModelMetaRelationEnumerationField {
relationEntityKey: string; relationEntityKey: string;
} }
export interface IModelMetaFieldWithFields {
fields: IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
);
}
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
collectionOf: 'object';
}
export interface IModelMetaCollectionFieldCommon {
fieldType: 'collection';
collectionMinLength?: number;
collectionMaxLength?: number;
}
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
IModelMetaCollectionObjectField;
export type IModelMetaRelationField = IModelMetaRelationFieldCommon & export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
IModelMetaRelationEnumerationField; IModelMetaRelationEnumerationField;
export interface IModelMeta { export interface IModelMeta {
defaultFilterField: string; defaultFilterField: string;
defaultSort: IModelMetaDefaultSort; defaultSort: IModelMetaDefaultSort;
importable?: boolean; importable?: boolean;
importAggregator?: string;
importAggregateOn?: string;
importAggregateBy?: string;
fields: { [key: string]: IModelMetaField }; fields: { [key: string]: IModelMetaField };
} }
// ----
export interface IModelMetaFieldCommon2 {
name: string;
required?: boolean;
importHint?: string;
order?: number;
unique?: number;
}
export interface IModelMetaRelationField2 {
fieldType: 'relation';
relationModel: string;
importableRelationLabel: string | string[];
}
export type IModelMetaField2 = IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField2
| IModelMetaCollectionField
);

View File

@@ -89,7 +89,9 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber'; import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent'; import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize'; import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; } import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -218,6 +220,8 @@ export const susbcribers = () => {
// Cashflow // Cashflow
DeleteCashflowTransactionOnUncategorize, DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete PreventDeleteTransactionOnDelete,
SubscribeFreeOnSignupCommunity
]; ];
}; };

View File

@@ -36,7 +36,13 @@ export default ({ app }) => {
// Boom response objects. // Boom response objects.
app.use(boom()); app.use(boom());
app.use(bodyParser.json()); app.use(
bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
// Parses both json and urlencoded. // Parses both json and urlencoded.
app.use(json()); app.use(json());

View File

@@ -11,6 +11,7 @@ import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEsti
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob'; import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob'; import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob'; import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
export default ({ agenda }: { agenda: Agenda }) => { export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda); new ResetPasswordMailJob(agenda);
@@ -25,6 +26,9 @@ export default ({ agenda }: { agenda: Agenda }) => {
new SaleReceiptMailNotificationJob(agenda); new SaleReceiptMailNotificationJob(agenda);
new PaymentReceiveMailNotificationJob(agenda); new PaymentReceiveMailNotificationJob(agenda);
new PlaidFetchTransactionsJob(agenda); new PlaidFetchTransactionsJob(agenda);
new ImportDeleteExpiredFilesJobs(agenda);
agenda.start(); agenda.start().then(() => {
agenda.every('1 hours', 'delete-expired-imported-files', {});
});
}; };

View File

@@ -1,6 +1,7 @@
import Container from 'typedi'; import Container from 'typedi';
import { import {
SystemUserRepository, SystemUserRepository,
SubscriptionRepository,
TenantRepository, TenantRepository,
} from '@/system/repositories'; } from '@/system/repositories';
@@ -10,6 +11,7 @@ export default () => {
return { return {
systemUserRepository: new SystemUserRepository(knex, cache), systemUserRepository: new SystemUserRepository(knex, cache),
subscriptionRepository: new SubscriptionRepository(knex, cache),
tenantRepository: new TenantRepository(knex, cache), tenantRepository: new TenantRepository(knex, cache),
}; };
} }

View File

@@ -61,7 +61,6 @@ import Task from 'models/Task';
import TaxRate from 'models/TaxRate'; import TaxRate from 'models/TaxRate';
import TaxRateTransaction from 'models/TaxRateTransaction'; import TaxRateTransaction from 'models/TaxRateTransaction';
import Attachment from 'models/Attachment'; import Attachment from 'models/Attachment';
import Import from 'models/Import';
import PlaidItem from 'models/PlaidItem'; import PlaidItem from 'models/PlaidItem';
import UncategorizedCashflowTransaction from 'models/UncategorizedCashflowTransaction'; import UncategorizedCashflowTransaction from 'models/UncategorizedCashflowTransaction';
@@ -128,7 +127,6 @@ export default (knex) => {
TaxRate, TaxRate,
TaxRateTransaction, TaxRateTransaction,
Attachment, Attachment,
Import,
PlaidItem, PlaidItem,
UncategorizedCashflowTransaction UncategorizedCashflowTransaction
}; };

View File

@@ -12,18 +12,11 @@ export default {
name: 'account.field.name', name: 'account.field.name',
column: 'name', column: 'name',
fieldType: 'text', fieldType: 'text',
unique: true,
required: true,
importable: true,
exportable: true,
order: 1,
}, },
description: { description: {
name: 'account.field.description', name: 'account.field.description',
column: 'description', column: 'description',
fieldType: 'text', fieldType: 'text',
importable: true,
exportable: true,
}, },
slug: { slug: {
name: 'account.field.slug', name: 'account.field.slug',
@@ -31,19 +24,13 @@ export default {
fieldType: 'text', fieldType: 'text',
columnable: false, columnable: false,
filterable: false, filterable: false,
importable: false,
}, },
code: { code: {
name: 'account.field.code', name: 'account.field.code',
column: 'code', column: 'code',
fieldType: 'text', fieldType: 'text',
exportable: true,
importable: true,
minLength: 3,
maxLength: 6,
importHint: 'Unique number to identify the account.',
}, },
rootType: { root_type: {
name: 'account.field.root_type', name: 'account.field.root_type',
fieldType: 'enumeration', fieldType: 'enumeration',
options: [ options: [
@@ -55,7 +42,6 @@ export default {
], ],
filterCustomQuery: RootTypeFieldFilterQuery, filterCustomQuery: RootTypeFieldFilterQuery,
sortable: false, sortable: false,
importable: false,
}, },
normal: { normal: {
name: 'account.field.normal', name: 'account.field.normal',
@@ -66,9 +52,8 @@ export default {
], ],
filterCustomQuery: NormalTypeFieldFilterQuery, filterCustomQuery: NormalTypeFieldFilterQuery,
sortable: false, sortable: false,
importable: false,
}, },
accountType: { type: {
name: 'account.field.type', name: 'account.field.type',
column: 'account_type', column: 'account_type',
fieldType: 'enumeration', fieldType: 'enumeration',
@@ -76,46 +61,71 @@ export default {
label: accountType.label, label: accountType.label,
key: accountType.key, key: accountType.key,
})), })),
required: true,
importable: true,
exportable: true,
order: 2,
}, },
active: { active: {
name: 'account.field.active', name: 'account.field.active',
column: 'active', column: 'active',
fieldType: 'boolean', fieldType: 'boolean',
filterable: false, filterable: false,
exportable: true,
importable: true,
}, },
balance: { balance: {
name: 'account.field.balance', name: 'account.field.balance',
column: 'amount', column: 'amount',
fieldType: 'number', fieldType: 'number',
importable: false,
}, },
currencyCode: { currency: {
name: 'account.field.currency', name: 'account.field.currency',
column: 'currency_code', column: 'currency_code',
fieldType: 'text', fieldType: 'text',
filterable: false, filterable: false,
importable: true,
exportable: true,
}, },
parentAccount: { created_at: {
name: 'account.field.parent_account',
column: 'parent_account_id',
fieldType: 'relation',
to: { model: 'Account', to: 'id' },
importable: false,
},
createdAt: {
name: 'account.field.created_at', name: 'account.field.created_at',
column: 'created_at', column: 'created_at',
fieldType: 'date', fieldType: 'date',
importable: false, },
exportable: true, },
fields2: {
name: {
name: 'account.field.name',
fieldType: 'text',
unique: true,
required: true,
},
description: {
name: 'account.field.description',
fieldType: 'text',
},
code: {
name: 'account.field.code',
fieldType: 'text',
minLength: 3,
maxLength: 6,
unique: true,
importHint: 'Unique number to identify the account.',
},
accountType: {
name: 'account.field.type',
fieldType: 'enumeration',
options: ACCOUNT_TYPES.map((accountType) => ({
label: accountType.label,
key: accountType.key,
})),
required: true,
},
active: {
name: 'account.field.active',
fieldType: 'boolean',
},
currencyCode: {
name: 'account.field.currency',
fieldType: 'text',
},
parentAccountId: {
name: 'account.field.parent_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
}, },
}, },
}; };

View File

@@ -1,10 +1,13 @@
export default { export default {
defaultFilterField: 'vendor', defaultFilterField: 'vendor',
defaultSort: { defaultSort: {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'bill_date', sortField: 'bill_date',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'billNumber',
fields: { fields: {
vendor: { vendor: {
name: 'bill.field.vendor', name: 'bill.field.vendor',
@@ -77,6 +80,77 @@ export default {
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
billNumber: {
name: 'Bill No.',
fieldType: 'text',
required: true,
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
billDate: {
name: 'Date',
fieldType: 'date',
required: true,
},
dueDate: {
name: 'Due Date',
fieldType: 'date',
required: true,
},
vendorId: {
name: 'Vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
note: {
name: 'Note',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'Item',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Line Description',
fieldType: 'text',
},
},
},
},
}; };
/** /**

View File

@@ -4,6 +4,10 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'bill_date', sortField: 'bill_date',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentNumber',
fields: { fields: {
vendor: { vendor: {
name: 'bill_payment.field.vendor', name: 'bill_payment.field.vendor',
@@ -63,4 +67,67 @@ export default {
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
vendorId: {
name: 'bill_payment.field.vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
payment_date: {
name: 'bill_payment.field.payment_date',
fieldType: 'date',
required: true,
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
fieldType: 'text',
unique: true,
importHint: "The payment number should be unique."
},
paymentAccountId: {
name: 'bill_payment.field.payment_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
},
exchangeRate: {
name: 'bill_payment.field.exchange_rate',
fieldType: 'number',
},
statement: {
name: 'bill_payment.field.statement',
fieldType: 'text',
},
reference: {
name: 'bill_payment.field.reference',
fieldType: 'text',
},
entries: {
name: 'bill_payment.field.entries',
column: 'entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
billId: {
name: 'bill_payment.field.entries.bill',
fieldType: 'relation',
relationModel: 'Bill',
relationImportMatch: 'billNumber',
required: true,
importHint: "Matches the bill number."
},
paymentAmount: {
name: 'bill_payment.field.entries.payment_amount',
fieldType: 'number',
required: true,
},
},
},
},
}; };

View File

@@ -187,18 +187,4 @@ export default class Contact extends TenantModel {
}, },
}; };
} }
static get fields() {
return {
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
created_at: {
column: 'created_at',
},
};
}
} }

View File

@@ -12,6 +12,10 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'name', sortField: 'name',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'creditNoteNumber',
fields: { fields: {
customer: { customer: {
name: 'credit_note.field.customer', name: 'credit_note.field.customer',
@@ -77,4 +81,72 @@ export default {
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
creditNoteDate: {
name: 'Credit Note Date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
note: {
name: 'Note',
fieldType: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
fieldType: 'text',
},
creditNoteNumber: {
name: 'Credit Note Number',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
fields: {
itemId: {
name: 'Item',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
},
},
},
}; };

View File

@@ -3,213 +3,246 @@ export default {
defaultFilterField: 'displayName', defaultFilterField: 'displayName',
defaultSort: { defaultSort: {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'createdAt', sortField: 'created_at',
}, },
fields: { fields: {
first_name: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
},
last_name: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
},
display_name: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
},
work_phone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
},
personal_phone: {
name: 'vendor.field.personal_pone',
column: 'personal_phone',
fieldType: 'text',
},
company_name: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
},
created_at: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'vendor.field.balance',
column: 'balance',
fieldType: 'number',
},
opening_balance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
},
opening_balance_at: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
},
currency_code: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
},
status: {
name: 'vendor.field.status',
type: 'enumeration',
options: [
{ key: 'overdue', label: 'vendor.field.status.overdue' },
{ key: 'unpaid', label: 'vendor.field.status.unpaid' },
],
filterCustomQuery: (query, role) => {
switch (role.value) {
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
},
fields2: {
customerType: { customerType: {
name: 'Customer Type', name: 'Customer Type',
column: 'contact_type',
fieldType: 'enumeration', fieldType: 'enumeration',
options: [ options: [
{ key: 'business', label: 'Business' }, { key: 'business', label: 'Business' },
{ key: 'individual', label: 'Individual' }, { key: 'individual', label: 'Individual' },
], ],
importable: true,
required: true, required: true,
}, },
firstName: { firstName: {
name: 'customer.field.first_name', name: 'customer.field.first_name',
column: 'first_name', column: 'first_name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
lastName: { lastName: {
name: 'customer.field.last_name', name: 'customer.field.last_name',
column: 'last_name', column: 'last_name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
displayName: { displayName: {
name: 'customer.field.display_name', name: 'customer.field.display_name',
column: 'display_name', column: 'display_name',
fieldType: 'text', fieldType: 'text',
required: true, required: true,
importable: true,
}, },
email: { email: {
name: 'customer.field.email', name: 'customer.field.email',
column: 'email', column: 'email',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
workPhone: { workPhone: {
name: 'customer.field.work_phone', name: 'customer.field.work_phone',
column: 'work_phone', column: 'work_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
personalPhone: { personalPhone: {
name: 'customer.field.personal_phone', name: 'customer.field.personal_phone',
column: 'personal_phone', column: 'personal_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
companyName: { companyName: {
name: 'customer.field.company_name', name: 'customer.field.company_name',
column: 'company_name', column: 'company_name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
website: { website: {
name: 'customer.field.website', name: 'customer.field.website',
column: 'website', column: 'website',
fieldType: 'url', fieldType: 'url',
importable: true,
},
balance: {
name: 'customer.field.balance',
column: 'balance',
fieldType: 'number',
}, },
openingBalance: { openingBalance: {
name: 'customer.field.opening_balance', name: 'customer.field.opening_balance',
column: 'opening_balance', column: 'opening_balance',
fieldType: 'number', fieldType: 'number',
importable: true,
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'customer.field.opening_balance_at', name: 'customer.field.opening_balance_at',
column: 'opening_balance_at', column: 'opening_balance_at',
filterable: false, filterable: false,
fieldType: 'date', fieldType: 'date',
importable: true,
}, },
openingBalanceExchangeRate: { openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate', name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate', column: 'opening_balance_exchange_rate',
fieldType: 'number', fieldType: 'number',
importable: true,
}, },
currencyCode: { currencyCode: {
name: 'customer.field.currency', name: 'customer.field.currency',
column: 'currency_code', column: 'currency_code',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
note: { note: {
name: 'Note', name: 'Note',
column: 'note', column: 'note',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
active: { active: {
name: 'Active', name: 'Active',
column: 'active', column: 'active',
fieldType: 'boolean', fieldType: 'boolean',
importable: true,
},
status: {
name: 'customer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'active', label: 'customer.field.status.active' },
{ key: 'inactive', label: 'customer.field.status.inactive' },
{ key: 'overdue', label: 'customer.field.status.overdue' },
{ key: 'unpaid', label: 'customer.field.status.unpaid' },
],
filterCustomQuery: statusFieldFilterQuery,
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
name: 'Billing Address 1', name: 'Billing Address 1',
column: 'billing_address1', column: 'billing_address1',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
name: 'Shipping Address 1', name: 'Shipping Address 1',
column: 'shipping_address1', column: 'shipping_address1',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
fieldType: 'text', fieldType: 'text',
importable: true,
},
//
createdAt: {
name: 'customer.field.created_at',
column: 'created_at',
fieldType: 'date',
}, },
}, },
}; };

View File

@@ -7,13 +7,14 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'name', sortField: 'name',
}, },
importable: true,
fields: { fields: {
'payment_date': { payment_date: {
name: 'expense.field.payment_date', name: 'expense.field.payment_date',
column: 'payment_date', column: 'payment_date',
fieldType: 'date', fieldType: 'date',
}, },
'payment_account': { payment_account: {
name: 'expense.field.payment_account', name: 'expense.field.payment_account',
column: 'payment_account_id', column: 'payment_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -24,27 +25,27 @@ export default {
relationEntityLabel: 'name', relationEntityLabel: 'name',
relationEntityKey: 'slug', relationEntityKey: 'slug',
}, },
'amount': { amount: {
name: 'expense.field.amount', name: 'expense.field.amount',
column: 'total_amount', column: 'total_amount',
fieldType: 'number', fieldType: 'number',
}, },
'reference_no': { reference_no: {
name: 'expense.field.reference_no', name: 'expense.field.reference_no',
column: 'reference_no', column: 'reference_no',
fieldType: 'text', fieldType: 'text',
}, },
'description': { description: {
name: 'expense.field.description', name: 'expense.field.description',
column: 'description', column: 'description',
fieldType: 'text', fieldType: 'text',
}, },
'published': { published: {
name: 'expense.field.published', name: 'expense.field.published',
column: 'published_at', column: 'published_at',
fieldType: 'date', fieldType: 'date',
}, },
'status': { status: {
name: 'expense.field.status', name: 'expense.field.status',
fieldType: 'enumeration', fieldType: 'enumeration',
options: [ options: [
@@ -54,12 +55,71 @@ export default {
filterCustomQuery: StatusFieldFilterQuery, filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery, sortCustomQuery: StatusFieldSortQuery,
}, },
'created_at': { created_at: {
name: 'expense.field.created_at', name: 'expense.field.created_at',
column: 'created_at', column: 'created_at',
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
paymentAccountId: {
name: 'expense.field.payment_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
},
referenceNo: {
name: 'expense.field.reference_no',
fieldType: 'text',
},
paymentDate: {
name: 'expense.field.payment_date',
fieldType: 'date',
required: true,
},
currencyCode: {
name: 'expense.field.currency_code',
fieldType: 'text',
},
exchangeRate: {
name: 'expense.field.exchange_rate',
fieldType: 'number',
},
description: {
name: 'expense.field.description',
fieldType: 'text',
},
categories: {
name: 'expense.field.categories',
fieldType: 'collection',
collectionOf: 'object',
fields: {
expenseAccountId: {
name: 'expense.field.expense_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
},
amount: {
name: 'expense.field.amount',
fieldType: 'number',
required: true,
},
description: {
name: 'expense.field.line_description',
fieldType: 'text',
},
},
},
publish: {
name: 'expense.field.publish',
fieldType: 'boolean',
},
},
}; };
function StatusFieldFilterQuery(query, role) { function StatusFieldFilterQuery(query, role) {

View File

@@ -15,45 +15,38 @@ export default {
{ key: 'service', label: 'item.field.type.service' }, { key: 'service', label: 'item.field.type.service' },
{ key: 'non-inventory', label: 'item.field.type.non-inventory' }, { key: 'non-inventory', label: 'item.field.type.non-inventory' },
], ],
importable: true,
}, },
name: { name: {
name: 'item.field.name', name: 'item.field.name',
column: 'name', column: 'name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
code: { code: {
name: 'item.field.code', name: 'item.field.code',
column: 'code', column: 'code',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
sellable: { sellable: {
name: 'item.field.sellable', name: 'item.field.sellable',
column: 'sellable', column: 'sellable',
fieldType: 'boolean', fieldType: 'boolean',
importable: true,
}, },
purchasable: { purchasable: {
name: 'item.field.purchasable', name: 'item.field.purchasable',
column: 'purchasable', column: 'purchasable',
fieldType: 'boolean', fieldType: 'boolean',
importable: true,
}, },
sellPrice: { sell_price: {
name: 'item.field.cost_price', name: 'item.field.cost_price',
column: 'sell_price', column: 'sell_price',
fieldType: 'number', fieldType: 'number',
importable: true,
}, },
costPrice: { cost_price: {
name: 'item.field.cost_account', name: 'item.field.cost_account',
column: 'cost_price', column: 'cost_price',
fieldType: 'number', fieldType: 'number',
importable: true,
}, },
costAccount: { cost_account: {
name: 'item.field.sell_account', name: 'item.field.sell_account',
column: 'cost_account_id', column: 'cost_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -63,10 +56,8 @@ export default {
relationEntityLabel: 'name', relationEntityLabel: 'name',
relationEntityKey: 'slug', relationEntityKey: 'slug',
importable: true,
}, },
sellAccount: { sell_account: {
name: 'item.field.sell_description', name: 'item.field.sell_description',
column: 'sell_account_id', column: 'sell_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -76,10 +67,8 @@ export default {
relationEntityLabel: 'name', relationEntityLabel: 'name',
relationEntityKey: 'slug', relationEntityKey: 'slug',
importable: true,
}, },
inventoryAccount: { inventory_account: {
name: 'item.field.inventory_account', name: 'item.field.inventory_account',
column: 'inventory_account_id', column: 'inventory_account_id',
@@ -88,32 +77,26 @@ export default {
relationEntityLabel: 'name', relationEntityLabel: 'name',
relationEntityKey: 'slug', relationEntityKey: 'slug',
importable: true,
}, },
sellDescription: { sell_description: {
name: 'Sell description', name: 'Sell description',
column: 'sell_description', column: 'sell_description',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
purchaseDescription: { purchase_description: {
name: 'Purchase description', name: 'Purchase description',
column: 'purchase_description', column: 'purchase_description',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
quantityOnHand: { quantity_on_hand: {
name: 'item.field.quantity_on_hand', name: 'item.field.quantity_on_hand',
column: 'quantity_on_hand', column: 'quantity_on_hand',
fieldType: 'number', fieldType: 'number',
importable: true,
}, },
note: { note: {
name: 'item.field.note', name: 'item.field.note',
column: 'note', column: 'note',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
category: { category: {
name: 'item.field.category', name: 'item.field.category',
@@ -124,19 +107,99 @@ export default {
relationEntityLabel: 'name', relationEntityLabel: 'name',
relationEntityKey: 'id', relationEntityKey: 'id',
importable: true,
}, },
active: { active: {
name: 'item.field.active', name: 'item.field.active',
column: 'active', column: 'active',
fieldType: 'boolean', fieldType: 'boolean',
importable: true, filterable: false,
}, },
createdAt: { created_at: {
name: 'item.field.created_at', name: 'item.field.created_at',
column: 'created_at', column: 'created_at',
columnType: 'date', columnType: 'date',
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
type: {
name: 'item.field.type',
fieldType: 'enumeration',
options: [
{ key: 'inventory', label: 'item.field.type.inventory' },
{ key: 'service', label: 'item.field.type.service' },
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
],
required: true,
},
name: {
name: 'item.field.name',
fieldType: 'text',
required: true,
},
code: {
name: 'item.field.code',
fieldType: 'text',
},
sellable: {
name: 'item.field.sellable',
fieldType: 'boolean',
},
purchasable: {
name: 'item.field.purchasable',
fieldType: 'boolean',
},
sellPrice: {
name: 'item.field.sell_price',
fieldType: 'number',
},
cost_price: {
name: 'item.field.cost_price',
fieldType: 'number',
},
costAccount: {
name: 'item.field.cost_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellAccount: {
name: 'item.field.sell_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
inventoryAccount: {
name: 'item.field.inventory_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellDescription: {
name: 'Sell Description',
fieldType: 'text',
},
purchaseDescription: {
name: 'Purchase Description',
fieldType: 'text',
},
note: {
name: 'item.field.note',
fieldType: 'text',
},
category: {
name: 'item.field.category',
fieldType: 'relation',
relationModel: 'ItemCategory',
relationImportMatch: ['name'],
importHint: "Matches the category name."
},
active: {
name: 'item.field.active',
fieldType: 'boolean',
},
},
}; };

View File

@@ -4,6 +4,7 @@ export default {
sortField: 'name', sortField: 'name',
sortOrder: 'DESC', sortOrder: 'DESC',
}, },
importable: true,
fields: { fields: {
name: { name: {
name: 'item_category.field.name', name: 'item_category.field.name',
@@ -27,4 +28,16 @@ export default {
columnType: 'date', columnType: 'date',
}, },
}, },
fields2: {
name: {
name: 'item_category.field.name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'item_category.field.description',
column: 'description',
fieldType: 'text',
},
},
}; };

View File

@@ -4,54 +4,130 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'name', sortField: 'name',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'journalNumber',
fields: { fields: {
'date': { date: {
name: 'manual_journal.field.date', name: 'manual_journal.field.date',
column: 'date', column: 'date',
fieldType: 'date', fieldType: 'date',
}, },
'journal_number': { journal_number: {
name: 'manual_journal.field.journal_number', name: 'manual_journal.field.journal_number',
column: 'journal_number', column: 'journal_number',
fieldType: 'text', fieldType: 'text',
}, },
'reference': { reference: {
name: 'manual_journal.field.reference', name: 'manual_journal.field.reference',
column: 'reference', column: 'reference',
fieldType: 'text', fieldType: 'text',
}, },
'journal_type': { journal_type: {
name: 'manual_journal.field.journal_type', name: 'manual_journal.field.journal_type',
column: 'journal_type', column: 'journal_type',
fieldType: 'text', fieldType: 'text',
}, },
'amount': { amount: {
name: 'manual_journal.field.amount', name: 'manual_journal.field.amount',
column: 'amount', column: 'amount',
fieldType: 'number', fieldType: 'number',
}, },
'description': { description: {
name: 'manual_journal.field.description', name: 'manual_journal.field.description',
column: 'description', column: 'description',
fieldType: 'text', fieldType: 'text',
}, },
'status': { status: {
name: 'manual_journal.field.status', name: 'manual_journal.field.status',
column: 'status', column: 'status',
fieldType: 'enumeration', fieldType: 'enumeration',
options: [ options: [
{ key: 'draft', label: 'Draft' }, { key: 'draft', label: 'Draft' },
{ key: 'published', label: 'published' } { key: 'published', label: 'published' },
], ],
filterCustomQuery: StatusFieldFilterQuery, filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery, sortCustomQuery: StatusFieldSortQuery,
}, },
'created_at': { created_at: {
name: 'manual_journal.field.created_at', name: 'manual_journal.field.created_at',
column: 'created_at', column: 'created_at',
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
date: {
name: 'manual_journal.field.date',
fieldType: 'date',
required: true,
},
journalNumber: {
name: 'manual_journal.field.journal_number',
fieldType: 'text',
required: true,
},
reference: {
name: 'manual_journal.field.reference',
fieldType: 'text',
},
journalType: {
name: 'manual_journal.field.journal_type',
fieldType: 'text',
},
currencyCode: {
name: 'manual_journal.field.currency',
fieldType: 'text',
},
exchange_rate: {
name: 'manual_journal.field.exchange_rate',
fieldType: 'number',
},
description: {
name: 'manual_journal.field.description',
fieldType: 'text',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 2,
required: true,
fields: {
credit: {
name: 'Credit',
fieldType: 'number',
required: true,
},
debit: {
name: 'Debit',
fieldType: 'number',
required: true,
},
accountId: {
name: 'Account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
contact: {
name: 'Contact',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
},
note: {
name: 'Note',
fieldType: 'text',
},
},
},
publish: {
name: 'Publish',
fieldType: 'boolean',
},
},
}; };
/** /**
@@ -64,6 +140,6 @@ function StatusFieldSortQuery(query, role) {
/** /**
* Status field filter custom query. * Status field filter custom query.
*/ */
function StatusFieldFilterQuery(query, role) { function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value); query.modify('filterByStatus', role.value);
} }

View File

@@ -1,33 +1,54 @@
import { get } from 'lodash'; import { get } from 'lodash';
import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from '@/interfaces'; import {
IModelMeta,
IModelMetaField,
IModelMetaDefaultSort,
} from '@/interfaces';
const defaultModelMeta = {
fields: {},
fields2: {},
};
export default (Model) => export default (Model) =>
class ModelSettings extends Model { class ModelSettings extends Model {
/** /**
* *
* @returns {IModelMeta}
*/ */
static get meta(): IModelMeta { static get meta(): IModelMeta {
throw new Error(''); throw new Error('');
} }
/**
* Parsed meta merged with default emta.
* @returns {IModelMeta}
*/
static get parsedMeta(): IModelMeta {
return {
...defaultModelMeta,
...this.meta,
};
}
/** /**
* Retrieve specific model field meta of the given field key. * Retrieve specific model field meta of the given field key.
* @param {string} key * @param {string} key
* @returns {IModelMetaField} * @returns {IModelMetaField}
*/ */
public static getField(key: string, attribute?:string): IModelMetaField { public static getField(key: string, attribute?: string): IModelMetaField {
const field = get(this.meta.fields, key); const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field; return attribute ? get(field, attribute) : field;
} }
/** /**
* Retrieve the specific model meta. * Retrieves the specific model meta.
* @param {string} key * @param {string} key
* @returns * @returns
*/ */
public static getMeta(key?: string) { public static getMeta(key?: string) {
return key ? get(this.meta, key): this.meta; return key ? get(this.parsedMeta, key) : this.parsedMeta;
} }
/** /**

View File

@@ -1,5 +1,8 @@
export default { export default {
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo',
fields: { fields: {
customer: { customer: {
name: 'payment_receive.field.customer', name: 'payment_receive.field.customer',
@@ -54,4 +57,65 @@ export default {
fieldDate: 'date', fieldDate: 'date',
}, },
}, },
fields2: {
customerId: {
name: 'payment_receive.field.customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
exchangeRate: {
name: 'payment_receive.field.exchange_rate',
fieldType: 'number',
},
paymentDate: {
name: 'payment_receive.field.payment_date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'payment_receive.field.reference_no',
fieldType: 'text',
},
depositAccountId: {
name: 'payment_receive.field.deposit_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
},
paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no',
fieldType: 'text',
importHint: "The payment number should be unique."
},
statement: {
name: 'payment_receive.field.statement',
fieldType: 'text',
},
entries: {
name: 'payment_receive.field.entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
invoiceId: {
name: 'payment_receive.field.invoice',
fieldType: 'relation',
relationModel: 'SaleInvoice',
relationImportMatch: 'invoiceNo',
required: true,
importHint: "Matches the invoice number."
},
paymentAmount: {
name: 'payment_receive.field.entries.payment_amount',
fieldType: 'number',
required: true,
},
},
},
},
}; };

View File

@@ -4,18 +4,22 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'estimate_date', sortField: 'estimate_date',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'estimateNumber',
fields: { fields: {
'amount': { amount: {
name: 'estimate.field.amount', name: 'estimate.field.amount',
column: 'amount', column: 'amount',
fieldType: 'number', fieldType: 'number',
}, },
'estimate_number': { estimate_number: {
name: 'estimate.field.estimate_number', name: 'estimate.field.estimate_number',
column: 'estimate_number', column: 'estimate_number',
fieldType: 'text', fieldType: 'text',
}, },
'customer': { customer: {
name: 'estimate.field.customer', name: 'estimate.field.customer',
column: 'customer_id', column: 'customer_id',
fieldType: 'relation', fieldType: 'relation',
@@ -26,32 +30,32 @@ export default {
relationEntityLabel: 'display_name', relationEntityLabel: 'display_name',
relationEntityKey: 'id', relationEntityKey: 'id',
}, },
'estimate_date': { estimate_date: {
name: 'estimate.field.estimate_date', name: 'estimate.field.estimate_date',
column: 'estimate_date', column: 'estimate_date',
fieldType: 'date', fieldType: 'date',
}, },
'expiration_date': { expiration_date: {
name: 'estimate.field.expiration_date', name: 'estimate.field.expiration_date',
column: 'expiration_date', column: 'expiration_date',
fieldType: 'date', fieldType: 'date',
}, },
'reference_no': { reference_no: {
name: 'estimate.field.reference_no', name: 'estimate.field.reference_no',
column: 'reference', column: 'reference',
fieldType: 'text', fieldType: 'text',
}, },
'note': { note: {
name: 'estimate.field.note', name: 'estimate.field.note',
column: 'note', column: 'note',
fieldType: 'text', fieldType: 'text',
}, },
'terms_conditions': { terms_conditions: {
name: 'estimate.field.terms_conditions', name: 'estimate.field.terms_conditions',
column: 'terms_conditions', column: 'terms_conditions',
fieldType: 'text', fieldType: 'text',
}, },
'status': { status: {
name: 'estimate.field.status', name: 'estimate.field.status',
fieldType: 'enumeration', fieldType: 'enumeration',
options: [ options: [
@@ -63,12 +67,90 @@ export default {
filterCustomQuery: StatusFieldFilterQuery, filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery, sortCustomQuery: StatusFieldSortQuery,
}, },
'created_at': { created_at: {
name: 'estimate.field.created_at', name: 'estimate.field.created_at',
column: 'created_at', column: 'created_at',
columnType: 'date', columnType: 'date',
}, },
}, },
fields2: {
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
estimateDate: {
name: 'Estimate Date',
fieldType: 'date',
required: true,
},
expirationDate: {
name: 'Expiration Date',
fieldType: 'date',
required: true,
},
estimateNumber: {
name: 'Estimate No.',
fieldType: 'text',
},
reference: {
name: 'Reference No.',
fieldType: 'text',
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
currencyCode: {
name: 'Currency',
fieldType: 'text',
},
note: {
name: 'Note',
fieldType: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
fieldType: 'text',
},
delivered: {
name: 'Delivered',
type: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'invoice.field.item_name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Line Description',
fieldType: 'text',
},
},
},
},
}; };
function StatusFieldSortQuery(query, role) { function StatusFieldSortQuery(query, role) {

View File

@@ -4,6 +4,10 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'created_at', sortField: 'created_at',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'invoiceNo',
fields: { fields: {
customer: { customer: {
name: 'invoice.field.customer', name: 'invoice.field.customer',
@@ -83,6 +87,84 @@ export default {
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
invoiceDate: {
name: 'invoice.field.invoice_date',
fieldType: 'date',
required: true,
},
dueDate: {
name: 'invoice.field.due_date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'invoice.field.reference_no',
fieldType: 'text',
},
invoiceNo: {
name: 'invoice.field.invoice_no',
fieldType: 'text',
},
customerId: {
name: 'invoice.field.customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'invoice.field.exchange_rate',
fieldType: 'number',
},
currencyCode: {
name: 'invoice.field.currency',
fieldType: 'text',
},
invoiceMessage: {
name: 'invoice.field.invoice_message',
fieldType: 'text',
},
termsConditions: {
name: 'invoice.field.terms_conditions',
fieldType: 'text',
},
entries: {
name: 'invoice.field.entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'invoice.field.item_name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'invoice.field.description',
fieldType: 'text',
},
},
},
delivered: {
name: 'invoice.field.delivered',
fieldType: 'boolean',
},
},
}; };
/** /**

View File

@@ -4,13 +4,17 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'created_at', sortField: 'created_at',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'receiptNumber',
fields: { fields: {
'amount': { amount: {
name: 'receipt.field.amount', name: 'receipt.field.amount',
column: 'amount', column: 'amount',
fieldType: 'number', fieldType: 'number',
}, },
'deposit_account': { deposit_account: {
column: 'deposit_account_id', column: 'deposit_account_id',
name: 'receipt.field.deposit_account', name: 'receipt.field.deposit_account',
fieldType: 'relation', fieldType: 'relation',
@@ -21,7 +25,7 @@ export default {
relationEntityLabel: 'name', relationEntityLabel: 'name',
relationEntityKey: 'slug', relationEntityKey: 'slug',
}, },
'customer': { customer: {
name: 'receipt.field.customer', name: 'receipt.field.customer',
column: 'customer_id', column: 'customer_id',
fieldType: 'relation', fieldType: 'relation',
@@ -32,38 +36,37 @@ export default {
relationEntityLabel: 'display_name', relationEntityLabel: 'display_name',
relationEntityKey: 'id', relationEntityKey: 'id',
}, },
'receipt_date': { receipt_date: {
name: 'receipt.field.receipt_date', name: 'receipt.field.receipt_date',
column: 'receipt_date', column: 'receipt_date',
fieldType: 'date', fieldType: 'date',
}, },
'receipt_number': { receipt_number: {
name: 'receipt.field.receipt_number', name: 'receipt.field.receipt_number',
column: 'receipt_number', column: 'receipt_number',
fieldType: 'text', fieldType: 'text',
}, },
'reference_no': { reference_no: {
name: 'receipt.field.reference_no', name: 'receipt.field.reference_no',
column: 'reference_no', column: 'reference_no',
fieldType: 'text', fieldType: 'text',
}, },
'receipt_message': { receipt_message: {
name: 'receipt.field.receipt_message', name: 'receipt.field.receipt_message',
column: 'receipt_message', column: 'receipt_message',
fieldType: 'text', fieldType: 'text',
}, },
'statement': { statement: {
name: 'receipt.field.statement', name: 'receipt.field.statement',
column: 'statement', column: 'statement',
fieldType: 'text', fieldType: 'text',
}, },
'created_at': { created_at: {
name: 'receipt.field.created_at', name: 'receipt.field.created_at',
column: 'created_at', column: 'created_at',
fieldType: 'date', fieldType: 'date',
}, },
'status': { status: {
name: 'receipt.field.status', name: 'receipt.field.status',
fieldType: 'enumeration', fieldType: 'enumeration',
options: [ options: [
@@ -74,6 +77,82 @@ export default {
sortCustomQuery: StatusFieldSortQuery, sortCustomQuery: StatusFieldSortQuery,
}, },
}, },
fields2: {
receiptDate: {
name: 'Receipt Date',
fieldType: 'date',
required: true,
},
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
depositAccountId: {
name: 'Deposit Account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
receiptNumber: {
name: 'Receipt Number',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
closed: {
name: 'Closed',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'invoice.field.item_name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'invoice.field.description',
fieldType: 'text',
},
},
},
statement: {
name: 'Statement',
fieldType: 'text',
},
receiptMessage: {
name: 'Receipt Message',
fieldType: 'text',
},
},
}; };
function StatusFieldFilterQuery(query, role) { function StatusFieldFilterQuery(query, role) {

View File

@@ -2,7 +2,7 @@ export default {
defaultFilterField: 'createdAt', defaultFilterField: 'createdAt',
defaultSort: { defaultSort: {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'createdAt', sortField: 'created_at',
}, },
importable: true, importable: true,
fields: { fields: {
@@ -10,33 +10,27 @@ export default {
name: 'Date', name: 'Date',
column: 'date', column: 'date',
fieldType: 'date', fieldType: 'date',
importable: true,
required: true,
}, },
payee: { payee: {
name: 'Payee', name: 'Payee',
column: 'payee', column: 'payee',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
description: { description: {
name: 'Description', name: 'Description',
column: 'description', column: 'description',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
referenceNo: { referenceNo: {
name: 'Reference No.', name: 'Reference No.',
column: 'reference_no', column: 'reference_no',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
amount: { amount: {
name: 'Amount', name: 'Amount',
column: 'Amount', column: 'Amount',
fieldType: 'numeric', fieldType: 'numeric',
required: true, required: true,
importable: true,
}, },
account: { account: {
name: 'Account', name: 'Account',
@@ -51,4 +45,28 @@ export default {
importable: false, importable: false,
}, },
}, },
fields2: {
date: {
name: 'Date',
fieldType: 'date',
required: true,
},
payee: {
name: 'Payee',
fieldType: 'text',
},
description: {
name: 'Description',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
amount: {
name: 'Amount',
fieldType: 'number',
required: true,
},
},
}; };

View File

@@ -2,99 +2,74 @@ export default {
defaultFilterField: 'displayName', defaultFilterField: 'displayName',
defaultSort: { defaultSort: {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'createdAt', sortField: 'created_at',
}, },
importable: true, importable: true,
fields: { fields: {
firstName: { first_name: {
name: 'vendor.field.first_name', name: 'vendor.field.first_name',
column: 'first_name', column: 'first_name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
lastName: { last_name: {
name: 'vendor.field.last_name', name: 'vendor.field.last_name',
column: 'last_name', column: 'last_name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
displayName: { display_name: {
name: 'vendor.field.display_name', name: 'vendor.field.display_name',
column: 'display_name', column: 'display_name',
fieldType: 'text', fieldType: 'text',
required: true,
importable: true,
}, },
email: { email: {
name: 'vendor.field.email', name: 'vendor.field.email',
column: 'email', column: 'email',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
workPhone: { work_phone: {
name: 'vendor.field.work_phone', name: 'vendor.field.work_phone',
column: 'work_phone', column: 'work_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
personalPhone: { personal_phone: {
name: 'vendor.field.personal_phone', name: 'vendor.field.personal_pone',
column: 'personal_phone', column: 'personal_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
companyName: { company_name: {
name: 'vendor.field.company_name', name: 'vendor.field.company_name',
column: 'company_name', column: 'company_name',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
website: { website: {
name: 'vendor.field.website', name: 'vendor.field.website',
column: 'website', column: 'website',
fieldType: 'text', fieldType: 'text',
importable: true, },
created_at: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
}, },
balance: { balance: {
name: 'vendor.field.balance', name: 'vendor.field.balance',
column: 'balance', column: 'balance',
fieldType: 'number', fieldType: 'number',
}, },
openingBalance: { opening_balance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
column: 'opening_balance', column: 'opening_balance',
fieldType: 'number', fieldType: 'number',
importable: true,
}, },
openingBalanceAt: { opening_balance_at: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at', column: 'opening_balance_at',
fieldType: 'date', fieldType: 'date',
importable: true,
}, },
openingBalanceExchangeRate: { currency_code: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
importable: true,
},
currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
column: 'currency_code', column: 'currency_code',
fieldType: 'text', fieldType: 'text',
importable: true,
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
importable: true,
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
importable: true,
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
@@ -114,96 +89,150 @@ export default {
} }
}, },
}, },
},
fields2: {
firstName: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
},
lastName: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
},
displayName: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
required: true,
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
},
workPhone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
},
personalPhone: {
name: 'vendor.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
},
companyName: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
},
openingBalance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
},
openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
},
currencyCode: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
},
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
name: 'Billing Address 1', name: 'Billing Address 1',
column: 'billing_address1', column: 'billing_address1',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
name: 'Shipping Address 1', name: 'Shipping Address 1',
column: 'shipping_address1', column: 'shipping_address1',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
fieldType: 'text', fieldType: 'text',
importable: true,
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
fieldType: 'text', fieldType: 'text',
importable: true,
},
createdAt: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
}, },
}, },
}; };

View File

@@ -12,6 +12,10 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'name', sortField: 'name',
}, },
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'vendorCreditNumber',
fields: { fields: {
vendor: { vendor: {
name: 'vendor_credit.field.vendor', name: 'vendor_credit.field.vendor',
@@ -72,4 +76,69 @@ export default {
fieldType: 'date', fieldType: 'date',
}, },
}, },
fields2: {
vendorId: {
name: 'Vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Echange Rate',
fieldType: 'text',
},
vendorCreditNumber: {
name: 'Vendor Credit No.',
fieldType: 'text',
},
referenceNo: {
name: 'Refernece No.',
fieldType: 'text',
},
vendorCreditDate: {
name: 'Vendor Credit Date',
fieldType: 'date',
required: true,
},
note: {
name: 'Note',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'Item Name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
},
},
},
}; };

View File

@@ -97,9 +97,11 @@ export class CommandAccountValidators {
query.whereNot('id', notAccountId); query.whereNot('id', notAccountId);
} }
}); });
if (account.length > 0) { if (account.length > 0) {
throw new ServiceError(ERRORS.ACCOUNT_CODE_NOT_UNIQUE); throw new ServiceError(
ERRORS.ACCOUNT_CODE_NOT_UNIQUE,
'Account code is not unique.'
);
} }
} }
@@ -124,7 +126,10 @@ export class CommandAccountValidators {
} }
}); });
if (foundAccount) { if (foundAccount) {
throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE); throw new ServiceError(
ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
'Account name is not unique.'
);
} }
} }

View File

@@ -6,7 +6,7 @@ import { ERRORS } from './constants';
@Service() @Service()
export default class CashflowDeleteAccount { export default class CashflowDeleteAccount {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Validate the account has no associated cashflow transactions. * Validate the account has no associated cashflow transactions.

View File

@@ -79,27 +79,25 @@ export interface ICashflowTransactionTypeMeta {
} }
export const BankTransactionsSampleData = [ export const BankTransactionsSampleData = [
[ {
{ Amount: '6,410.19',
Amount: '6,410.19', Date: '2024-03-26',
Date: '2024-03-26', Payee: 'MacGyver and Sons',
Payee: 'MacGyver and Sons', 'Reference No.': 'REF-1',
'Reference No.': 'REF-1', Description: 'Commodi quo labore.',
Description: 'Commodi quo labore.', },
}, {
{ Amount: '8,914.17',
Amount: '8,914.17', Date: '2024-01-05',
Date: '2024-01-05', Payee: 'Eichmann - Bergnaum',
Payee: 'Eichmann - Bergnaum', 'Reference No.': 'REF-1',
'Reference No.': 'REF-1', Description: 'Quia enim et.',
Description: 'Quia enim et.', },
}, {
{ Amount: '6,200.88',
Amount: '6,200.88', Date: '2024-02-17',
Date: '2024-02-17', Payee: 'Luettgen, Mraz and Legros',
Payee: 'Luettgen, Mraz and Legros', 'Reference No.': 'REF-1',
'Reference No.': 'REF-1', Description: 'Occaecati consequuntur cum impedit illo.',
Description: 'Occaecati consequuntur cum impedit illo.', },
},
],
]; ];

View File

@@ -50,10 +50,7 @@ export class CustomersApplication {
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @returns {Promise<ICustomer>} * @returns {Promise<ICustomer>}
*/ */
public createCustomer = ( public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
tenantId: number,
customerDTO: ICustomerNewDTO,
) => {
return this.createCustomerService.createCustomer(tenantId, customerDTO); return this.createCustomerService.createCustomer(tenantId, customerDTO);
}; };

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { import {
ISystemUser, ISystemUser,
IVendorEditDTO, IVendorEditDTO,
@@ -42,13 +43,9 @@ export class VendorsApplication {
public createVendor = ( public createVendor = (
tenantId: number, tenantId: number,
vendorDTO: IVendorNewDTO, vendorDTO: IVendorNewDTO,
authorizedUser: ISystemUser trx?: Knex.Transaction
) => { ) => {
return this.createVendorService.createVendor( return this.createVendorService.createVendor(tenantId, vendorDTO, trx);
tenantId,
vendorDTO,
authorizedUser
);
}; };
/** /**

View File

@@ -12,7 +12,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02", "Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2, "Opening Balance Ex. Rate": 2,
"Currency": "LYD", "Currency": "LYD",
"Active": "F", "Active": "T",
"Note": "Doloribus autem optio temporibus dolores mollitia sit.", "Note": "Doloribus autem optio temporibus dolores mollitia sit.",
"Billing Address 1": "862 Jessika Well", "Billing Address 1": "862 Jessika Well",
"Billing Address 2": "1091 Dorthy Mount", "Billing Address 2": "1091 Dorthy Mount",
@@ -42,7 +42,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02", "Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2, "Opening Balance Ex. Rate": 2,
"Currency": "LYD", "Currency": "LYD",
"Active": "F", "Active": "T",
"Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.", "Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.",
"Billing Address 1": "532 Simonis Spring", "Billing Address 1": "532 Simonis Spring",
"Billing Address 2": "3122 Nicolas Inlet", "Billing Address 2": "3122 Nicolas Inlet",
@@ -72,7 +72,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02", "Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2, "Opening Balance Ex. Rate": 2,
"Currency": "LYD", "Currency": "LYD",
"Active": "F", "Active": "T",
"Note": "Vero quibusdam rem fugit aperiam est modi.", "Note": "Vero quibusdam rem fugit aperiam est modi.",
"Billing Address 1": "214 Sauer Villages", "Billing Address 1": "214 Sauer Villages",
"Billing Address 2": "30687 Kacey Square", "Billing Address 2": "30687 Kacey Square",
@@ -102,7 +102,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02", "Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2, "Opening Balance Ex. Rate": 2,
"Currency": "LYD", "Currency": "LYD",
"Active": "F", "Active": "T",
"Note": "Quis cumque molestias rerum.", "Note": "Quis cumque molestias rerum.",
"Billing Address 1": "22590 Cathy Harbor", "Billing Address 1": "22590 Cathy Harbor",
"Billing Address 2": "24493 Brycen Brooks", "Billing Address 2": "24493 Brycen Brooks",

View File

@@ -4,7 +4,6 @@ import {
ICreditNoteCreatedPayload, ICreditNoteCreatedPayload,
ICreditNoteCreatingPayload, ICreditNoteCreatingPayload,
ICreditNoteNewDTO, ICreditNoteNewDTO,
ISystemUser,
} from '@/interfaces'; } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@@ -34,7 +33,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
public newCreditNote = async ( public newCreditNote = async (
tenantId: number, tenantId: number,
creditNoteDTO: ICreditNoteNewDTO, creditNoteDTO: ICreditNoteNewDTO,
authorizedUser: ISystemUser trx?: Knex.Transaction
) => { ) => {
const { CreditNote, Contact } = this.tenancy.models(tenantId); const { CreditNote, Contact } = this.tenancy.models(tenantId);
@@ -66,28 +65,32 @@ export default class CreateCreditNote extends BaseCreditNotes {
customer.currencyCode customer.currencyCode
); );
// Creates a new credit card transactions under unit-of-work envirement. // Creates a new credit card transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onCreditNoteCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.creditNote.onCreating, { async (trx: Knex.Transaction) => {
tenantId, // Triggers `onCreditNoteCreating` event.
creditNoteDTO, await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
trx, tenantId,
} as ICreditNoteCreatingPayload); creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
// Upsert the credit note graph. // Upsert the credit note graph.
const creditNote = await CreditNote.query(trx).upsertGraph({ const creditNote = await CreditNote.query(trx).upsertGraph({
...creditNoteModel, ...creditNoteModel,
}); });
// Triggers `onCreditNoteCreated` event. // Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, { await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
tenantId, tenantId,
creditNoteDTO, creditNoteDTO,
creditNote, creditNote,
creditNoteId: creditNote.id, creditNoteId: creditNote.id,
trx, trx,
} as ICreditNoteCreatedPayload); } as ICreditNoteCreatedPayload);
return creditNote; return creditNote;
}); },
trx
);
}; };
} }

View File

@@ -0,0 +1,44 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ICreditNoteNewDTO } from '@/interfaces';
import { Importable } from '../Import/Importable';
import CreateCreditNote from './CreateCreditNote';
@Service()
export class CreditNotesImportable extends Importable {
@Inject()
private createCreditNoteImportable: CreateCreditNote;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: ICreditNoteNewDTO,
trx?: Knex.Transaction
) {
return this.createCreditNoteImportable.newCreditNote(
tenantId,
createAccountDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return [];
}
}

View File

@@ -88,7 +88,8 @@ export class CreateExpense {
public newExpense = async ( public newExpense = async (
tenantId: number, tenantId: number,
expenseDTO: IExpenseCreateDTO, expenseDTO: IExpenseCreateDTO,
authorizedUser: ISystemUser authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<IExpense> => { ): Promise<IExpense> => {
const { Expense } = await this.tenancy.models(tenantId); const { Expense } = await this.tenancy.models(tenantId);
@@ -103,28 +104,32 @@ export class CreateExpense {
); );
// Writes the expense transaction with associated transactions under // Writes the expense transaction with associated transactions under
// unit-of-work envirement. // unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onExpenseCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.expenses.onCreating, { async (trx: Knex.Transaction) => {
trx, // Triggers `onExpenseCreating` event.
tenantId, await this.eventPublisher.emitAsync(events.expenses.onCreating, {
expenseDTO, trx,
} as IExpenseCreatingPayload); tenantId,
expenseDTO,
} as IExpenseCreatingPayload);
// Creates a new expense transaction graph. // Creates a new expense transaction graph.
const expense: IExpense = await Expense.query(trx).upsertGraph( const expense: IExpense = await Expense.query(trx).upsertGraph(
expenseObj expenseObj
); );
// Triggers `onExpenseCreated` event. // Triggers `onExpenseCreated` event.
await this.eventPublisher.emitAsync(events.expenses.onCreated, { await this.eventPublisher.emitAsync(events.expenses.onCreated, {
tenantId, tenantId,
expenseId: expense.id, expenseId: expense.id,
authorizedUser, authorizedUser,
expense, expense,
trx, trx,
} as IExpenseCreatedPayload); } as IExpenseCreatedPayload);
return expense; return expense;
}); },
trx
);
}; };
} }

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IExpenseCreateDTO } from '@/interfaces';
import { Importable } from '../Import/Importable';
import { CreateExpense } from './CRUD/CreateExpense';
import { ExpensesSampleData } from './constants';
@Service()
export class ExpensesImportable extends Importable {
@Inject()
private createExpenseService: CreateExpense;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: IExpenseCreateDTO,
trx?: Knex.Transaction
) {
return this.createExpenseService.newExpense(
tenantId,
createAccountDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return ExpensesSampleData;
}
}

View File

@@ -36,3 +36,43 @@ export const ERRORS = {
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published', EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST',
}; };
export const ExpensesSampleData = [
{
'Payment Date': '2024-03-01',
'Reference No.': 'REF-1',
'Payment Account': 'Petty Cash',
Description: 'Vel et dolorem architecto veniam.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description': 'Voluptates voluptas corporis vel.',
Publish: 'T',
},
{
'Payment Date': '2024-03-02',
'Reference No.': 'REF-2',
'Payment Account': 'Petty Cash',
Description: 'Id est molestias.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description': 'Eos voluptatem cumque et voluptate reiciendis.',
Publish: 'T',
},
{
'Payment Date': '2024-03-03',
'Reference No.': 'REF-3',
'Payment Account': 'Petty Cash',
Description: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description':
'Hic alias rerum sed commodi dolores sint animi perferendis.',
Publish: 'T',
},
];

View File

@@ -1,4 +1,3 @@
import fs from 'fs/promises';
import XLSX from 'xlsx'; import XLSX from 'xlsx';
import bluebird from 'bluebird'; import bluebird from 'bluebird';
import * as R from 'ramda'; import * as R from 'ramda';
@@ -7,16 +6,17 @@ import { first } from 'lodash';
import { ImportFileDataValidator } from './ImportFileDataValidator'; import { ImportFileDataValidator } from './ImportFileDataValidator';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
ImportInsertError,
ImportOperError, ImportOperError,
ImportOperSuccess, ImportOperSuccess,
ImportableContext, ImportableContext,
} from './interfaces'; } from './interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { trimObject } from './_utils'; import { getUniqueImportableValue, trimObject } from './_utils';
import { ImportableResources } from './ImportableResources'; import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import Import from '@/models/Import'; import { Import } from '@/system/models';
@Service() @Service()
export class ImportFileCommon { export class ImportFileCommon {
@@ -47,14 +47,6 @@ export class ImportFileCommon {
return XLSX.utils.sheet_to_json(worksheet, {}); return XLSX.utils.sheet_to_json(worksheet, {});
} }
/**
* Reads the import file.
* @param {string} filename
* @returns {Promise<Buffer>}
*/
public readImportFile(filename: string) {
return fs.readFile(`public/imports/${filename}`);
}
/** /**
* 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.
@@ -70,7 +62,7 @@ export class ImportFileCommon {
parsedData: Record<string, any>[], parsedData: Record<string, any>[],
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<[ImportOperSuccess[], ImportOperError[]]> { ): Promise<[ImportOperSuccess[], ImportOperError[]]> {
const importableFields = this.resource.getResourceImportableFields( const resourceFields = this.resource.getResourceFields2(
tenantId, tenantId,
importFile.resource importFile.resource
); );
@@ -88,11 +80,16 @@ export class ImportFileCommon {
import: importFile, import: importFile,
}; };
const transformedDTO = importable.transform(objectDTO, context); const transformedDTO = importable.transform(objectDTO, context);
const rowNumber = index + 1;
const uniqueValue = getUniqueImportableValue(resourceFields, objectDTO);
const errorContext = {
rowNumber,
uniqueValue,
};
try { try {
// Validate the DTO object before passing it to the service layer. // Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData( await this.importFileValidator.validateData(
importableFields, resourceFields,
transformedDTO transformedDTO
); );
try { try {
@@ -105,18 +102,27 @@ export class ImportFileCommon {
success.push({ index, data }); success.push({ index, data });
} catch (err) { } catch (err) {
if (err instanceof ServiceError) { if (err instanceof ServiceError) {
const error = [ const error: ImportInsertError[] = [
{ {
errorCode: 'ValidationError', errorCode: 'ServiceError',
errorMessage: err.message || err.errorType, errorMessage: err.message || err.errorType,
rowNumber: index + 1, ...errorContext,
},
];
failed.push({ index, error });
} else {
const error: ImportInsertError[] = [
{
errorCode: 'UnknownError',
errorMessage: 'Unknown error occurred',
...errorContext,
}, },
]; ];
failed.push({ index, error }); failed.push({ index, error });
} }
} }
} catch (errors) { } catch (errors) {
const error = errors.map((er) => ({ ...er, rowNumber: index + 1 })); const error = errors.map((er) => ({ ...er, ...errorContext }));
failed.push({ index, error }); failed.push({ index, error });
} }
}; };
@@ -187,19 +193,4 @@ export class ImportFileCommon {
public parseSheetColumns(json: unknown[]): string[] { public parseSheetColumns(json: unknown[]): string[] {
return R.compose(Object.keys, trimObject, first)(json); return R.compose(Object.keys, trimObject, first)(json);
} }
/**
* Deletes the imported file from the storage and database.
* @param {number} tenantId
* @param {} importFile
*/
public async deleteImportFile(tenantId: number, importFile: any) {
const { Import } = this.tenancy.models(tenantId);
// Deletes the import row.
await Import.query().findById(importFile.id).delete();
// Deletes the imported file.
await fs.unlink(`public/imports/${importFile.filename}`);
}
} }

View File

@@ -1,24 +1,43 @@
import { Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import bluebird from 'bluebird';
import { isUndefined, get, chain } from 'lodash'; import { isUndefined, pickBy, set } from 'lodash';
import { Knex } from 'knex';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces'; import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { parseBoolean } from '@/utils'; import {
import { trimObject } from './_utils'; valueParser,
parseKey,
getFieldKey,
aggregate,
sanitizeSheetData,
getMapToPath,
} from './_utils';
import ResourceService from '../Resource/ResourceService';
import HasTenancyService from '../Tenancy/TenancyService';
import { CurrencyParsingDTOs } from './_constants';
@Service() @Service()
export class ImportFileDataTransformer { export class ImportFileDataTransformer {
@Inject()
private resource: ResourceService;
@Inject()
private tenancy: HasTenancyService;
/** /**
* * Parses the given sheet data before passing to the service layer.
* based on the mapped fields and the each field type.
* @param {number} tenantId - * @param {number} tenantId -
* @param {} * @param {}
*/ */
public parseSheetData( public async parseSheetData(
tenantId: number,
importFile: any, importFile: any,
importableFields: any, importableFields: ResourceMetaFieldsMap,
data: Record<string, unknown>[] data: Record<string, unknown>[],
) { trx?: Knex.Transaction
): Promise<Record<string, any>[]> {
// Sanitize the sheet data. // Sanitize the sheet data.
const sanitizedData = this.sanitizeSheetData(data); const sanitizedData = sanitizeSheetData(data);
// Map the sheet columns key with the given map. // Map the sheet columns key with the given map.
const mappedDTOs = this.mapSheetColumns( const mappedDTOs = this.mapSheetColumns(
@@ -26,19 +45,44 @@ export class ImportFileDataTransformer {
importFile.mappingParsed importFile.mappingParsed
); );
// Parse the mapped sheet values. // Parse the mapped sheet values.
const parsedValues = this.parseExcelValues(importableFields, mappedDTOs); const parsedValues = await this.parseExcelValues(
tenantId,
return parsedValues; importableFields,
mappedDTOs,
trx
);
const aggregateValues = this.aggregateParsedValues(
tenantId,
importFile.resource,
parsedValues
);
return aggregateValues;
} }
/** /**
* Sanitizes the data in the imported sheet by trimming object keys. * Aggregates parsed data based on resource metadata configuration.
* @param json - The JSON data representing the imported sheet. * @param {number} tenantId
* @returns {string[][]} - The sanitized data with trimmed object keys. * @param {string} resourceName
* @param {Record<string, any>} parsedData
* @returns {Record<string, any>[]}
*/ */
public sanitizeSheetData(json) { public aggregateParsedValues = (
return R.compose(R.map(trimObject))(json); tenantId: number,
} resourceName: string,
parsedData: Record<string, any>[]
): Record<string, any>[] => {
let _value = parsedData;
const meta = this.resource.getResourceMeta(tenantId, resourceName);
if (meta.importAggregator === 'group') {
_value = aggregate(
_value,
meta.importAggregateBy,
meta.importAggregateOn
);
}
return _value;
};
/** /**
* Maps the columns of the imported data based on the provided mapping attributes. * Maps the columns of the imported data based on the provided mapping attributes.
@@ -55,7 +99,8 @@ export class ImportFileDataTransformer {
map map
.filter((mapping) => !isUndefined(item[mapping.from])) .filter((mapping) => !isUndefined(item[mapping.from]))
.forEach((mapping) => { .forEach((mapping) => {
newItem[mapping.to] = item[mapping.from]; const toPath = getMapToPath(mapping.to, mapping.group);
newItem[toPath] = item[mapping.from];
}); });
return newItem; return newItem;
}); });
@@ -67,35 +112,40 @@ export class ImportFileDataTransformer {
* @param {Record<string, any>} valueDTOS - * @param {Record<string, any>} valueDTOS -
* @returns {Record<string, any>} * @returns {Record<string, any>}
*/ */
public parseExcelValues( public async parseExcelValues(
tenantId: number,
fields: ResourceMetaFieldsMap, fields: ResourceMetaFieldsMap,
valueDTOs: Record<string, any>[] valueDTOs: Record<string, any>[],
): Record<string, any> { trx?: Knex.Transaction
const parser = (value, key) => { ): Promise<Record<string, any>[]> {
let _value = value; const tenantModels = this.tenancy.models(tenantId);
const _valueParser = valueParser(fields, tenantModels, trx);
const _keyParser = parseKey(fields);
// Parses the boolean value. const parseAsync = async (valueDTO) => {
if (fields[key].fieldType === 'boolean') { // Clean up the undefined keys that not exist in resource fields.
_value = parseBoolean(value, false); const _valueDTO = pickBy(
valueDTO,
(value, key) => !isUndefined(fields[getFieldKey(key)])
);
// Keys of mapped values. key structure: `group.key` or `key`.
const keys = Object.keys(_valueDTO);
// Parses the enumeration value. // Map the object values.
} else if (fields[key].fieldType === 'enumeration') { return bluebird.reduce(
const field = fields[key]; keys,
const option = get(field, 'options', []).find( async (acc, key) => {
(option) => option.label === value const parsedValue = await _valueParser(_valueDTO[key], key);
); const parsedKey = await _keyParser(key);
_value = get(option, 'key');
// Prases the numeric value. set(acc, parsedKey, parsedValue);
} else if (fields[key].fieldType === 'number') { return acc;
_value = parseFloat(value); },
} {}
return _value; );
}; };
return valueDTOs.map((DTO) => { return bluebird.map(valueDTOs, parseAsync, {
return chain(DTO) concurrency: CurrencyParsingDTOs,
.pickBy((value, key) => !isUndefined(fields[key]))
.mapValues(parser)
.value();
}); });
} }
} }

View File

@@ -32,10 +32,14 @@ export class ImportFileDataValidator {
try { try {
await YupSchema.validate(_data, { abortEarly: false }); await YupSchema.validate(_data, { abortEarly: false });
} catch (validationError) { } catch (validationError) {
const errors = validationError.inner.map((error) => ({ const errors = validationError.inner.reduce((errors, error) => {
errorCode: 'ValidationError', const newErrors = error.errors.map((errMsg) => ({
errorMessage: error.errors, errorCode: 'ValidationError',
})); errorMessage: errMsg,
}));
return [...errors, ...newErrors];
}, []);
throw errors; throw errors;
} }
} }

View File

@@ -1,6 +1,5 @@
import { fromPairs } from 'lodash'; import { fromPairs, isUndefined } from 'lodash';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { import {
ImportDateFormats, ImportDateFormats,
ImportFileMapPOJO, ImportFileMapPOJO,
@@ -9,12 +8,10 @@ import {
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { ERRORS } from './_utils'; import { ERRORS } from './_utils';
import { Import } from '@/system/models';
@Service() @Service()
export class ImportFileMapping { export class ImportFileMapping {
@Inject()
private tenancy: HasTenancyService;
@Inject() @Inject()
private resource: ResourceService; private resource: ResourceService;
@@ -29,8 +26,6 @@ export class ImportFileMapping {
importId: number, importId: number,
maps: ImportMappingAttr[] maps: ImportMappingAttr[]
): Promise<ImportFileMapPOJO> { ): Promise<ImportFileMapPOJO> {
const { Import } = this.tenancy.models(tenantId);
const importFile = await Import.query() const importFile = await Import.query()
.findOne('filename', importId) .findOne('filename', importId)
.throwIfNotFound(); .throwIfNotFound();
@@ -69,7 +64,7 @@ export class ImportFileMapping {
importFile: any, importFile: any,
maps: ImportMappingAttr[] maps: ImportMappingAttr[]
) { ) {
const fields = this.resource.getResourceImportableFields( const fields = this.resource.getResourceFields2(
tenantId, tenantId,
importFile.resource importFile.resource
); );
@@ -78,11 +73,20 @@ export class ImportFileMapping {
); );
const invalid = []; const invalid = [];
// is not empty, is not undefined or map.group
maps.forEach((map) => { maps.forEach((map) => {
if ( let _invalid = true;
'undefined' === typeof fields[map.to] ||
'undefined' === typeof columnsMap[map.from] if (!map.group && fields[map.to]) {
) { _invalid = false;
}
if (map.group && fields[map.group] && fields[map.group]?.fields[map.to]) {
_invalid = false;
}
if (columnsMap[map.from]) {
_invalid = false;
}
if (_invalid) {
invalid.push(map); invalid.push(map);
} }
}); });
@@ -105,10 +109,14 @@ export class ImportFileMapping {
} else { } else {
fromMap[map.from] = true; fromMap[map.from] = true;
} }
if (toMap[map.to]) { const toPath = !isUndefined(map?.group)
? `${map.group}.${map.to}`
: map.to;
if (toMap[toPath]) {
throw new ServiceError(ERRORS.DUPLICATED_TO_MAP_ATTR); throw new ServiceError(ERRORS.DUPLICATED_TO_MAP_ATTR);
} else { } else {
toMap[map.to] = true; toMap[toPath] = true;
} }
}); });
} }
@@ -128,6 +136,7 @@ export class ImportFileMapping {
tenantId, tenantId,
resource resource
); );
// @todo Validate date type of the nested fields.
maps.forEach((map) => { maps.forEach((map) => {
if ( if (
typeof fields[map.to] !== 'undefined' && typeof fields[map.to] !== 'undefined' &&

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { ImportFileMetaTransformer } from './ImportFileMetaTransformer'; import { ImportFileMetaTransformer } from './ImportFileMetaTransformer';
import { Import } from '@/system/models';
@Service() @Service()
export class ImportFileMeta { export class ImportFileMeta {
@@ -12,15 +13,15 @@ export class ImportFileMeta {
private transformer: TransformerInjectable; private transformer: TransformerInjectable;
/** /**
* * Retrieves the import meta of the given import model id.
* @param {number} tenantId * @param {number} tenantId
* @param {number} importId * @param {number} importId
* @returns {} * @returns {}
*/ */
async getImportMeta(tenantId: number, importId: string) { async getImportMeta(tenantId: number, importId: string) {
const { Import } = this.tenancy.models(tenantId); const importFile = await Import.query()
.where('tenantId', tenantId)
const importFile = await Import.query().findOne('importId', importId); .findOne('importId', importId);
// Retrieves the transformed accounts collection. // Retrieves the transformed accounts collection.
return this.transformer.transform( return this.transformer.transform(

View File

@@ -2,19 +2,21 @@ 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 { ERRORS, getSheetColumns, getUnmappedSheetColumns } from './_utils'; import {
import HasTenancyService from '../Tenancy/TenancyService'; 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';
@Service() @Service()
export class ImportFileProcess { export class ImportFileProcess {
@Inject()
private tenancy: HasTenancyService;
@Inject() @Inject()
private resource: ResourceService; private resource: ResourceService;
@@ -38,10 +40,9 @@ export class ImportFileProcess {
importId: number, importId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> { ): Promise<ImportFilePreviewPOJO> {
const { Import } = this.tenancy.models(tenantId);
const importFile = await Import.query() const importFile = await Import.query()
.findOne('importId', importId) .findOne('importId', importId)
.where('tenantId', tenantId)
.throwIfNotFound(); .throwIfNotFound();
// Throw error if the import file is not mapped yet. // Throw error if the import file is not mapped yet.
@@ -49,27 +50,37 @@ export class ImportFileProcess {
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED); throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
} }
// Read the imported file. // Read the imported file.
const buffer = await this.importCommon.readImportFile(importFile.filename); const buffer = await readImportFile(importFile.filename);
const sheetData = this.importCommon.parseXlsxSheet(buffer); const sheetData = this.importCommon.parseXlsxSheet(buffer);
const header = getSheetColumns(sheetData); const header = getSheetColumns(sheetData);
const importableFields = this.resource.getResourceImportableFields( const resource = importFile.resource;
tenantId, const resourceFields = this.resource.getResourceFields2(tenantId, resource);
importFile.resource
);
// Prases the sheet json data.
const parsedData = this.importParser.parseSheetData(
importFile,
importableFields,
sheetData
);
// Runs the importing operation with ability to return errors that will happen. // Runs the importing operation with ability to return errors that will happen.
const [successedImport, failedImport] = await this.uow.withTransaction( const [successedImport, failedImport, allData] =
tenantId, await this.uow.withTransaction(
(trx: Knex.Transaction) => tenantId,
this.importCommon.import(tenantId, importFile, parsedData, trx), async (trx: Knex.Transaction) => {
trx // Prases the sheet json data.
); const parsedData = await this.importParser.parseSheetData(
tenantId,
importFile,
resourceFields,
sheetData,
trx
);
const [successedImport, failedImport] =
await this.importCommon.import(
tenantId,
importFile,
parsedData,
trx
);
return [successedImport, failedImport, parsedData];
},
trx
);
const mapping = importFile.mappingParsed; const mapping = importFile.mappingParsed;
const errors = chain(failedImport) const errors = chain(failedImport)
.map((oper) => oper.error) .map((oper) => oper.error)
@@ -77,13 +88,14 @@ export class ImportFileProcess {
.value(); .value();
const unmappedColumns = getUnmappedSheetColumns(header, mapping); const unmappedColumns = getUnmappedSheetColumns(header, mapping);
const totalCount = parsedData.length; const totalCount = allData.length;
const createdCount = successedImport.length; const createdCount = successedImport.length;
const errorsCount = failedImport.length; const errorsCount = failedImport.length;
const skippedCount = errorsCount; const skippedCount = errorsCount;
return { return {
resource,
createdCount, createdCount,
skippedCount, skippedCount,
totalCount, totalCount,

View File

@@ -1,18 +1,19 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService'; import {
import { sanitizeResourceName } from './_utils'; deleteImportFile,
getResourceColumns,
readImportFile,
sanitizeResourceName,
validateSheetEmpty,
} from './_utils';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import { IModelMetaField } from '@/interfaces';
import { ImportFileCommon } from './ImportFileCommon'; import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator'; import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces'; import { ImportFileUploadPOJO } from './interfaces';
import { ServiceError } from '@/exceptions'; import { Import } from '@/system/models';
@Service() @Service()
export class ImportFileUploadService { export class ImportFileUploadService {
@Inject()
private tenancy: HasTenancyService;
@Inject() @Inject()
private resourceService: ResourceService; private resourceService: ResourceService;
@@ -23,11 +24,12 @@ export class ImportFileUploadService {
private importValidator: ImportFileDataValidator; private importValidator: ImportFileDataValidator;
/** /**
* Reads the imported file and stores the import file meta under unqiue id. * Imports the specified file for the given resource.
* @param {number} tenantId - Tenant id. * Deletes the file if an error occurs during the import process.
* @param {string} resource - Resource name. * @param {number} tenantId
* @param {string} filePath - File path. * @param {string} resourceName
* @param {string} fileName - File name. * @param {string} filename
* @param {Record<string, number | string>} params
* @returns {Promise<ImportFileUploadPOJO>} * @returns {Promise<ImportFileUploadPOJO>}
*/ */
public async import( public async import(
@@ -36,8 +38,33 @@ export class ImportFileUploadService {
filename: string, filename: string,
params: Record<string, number | string> params: Record<string, number | string>
): Promise<ImportFileUploadPOJO> { ): Promise<ImportFileUploadPOJO> {
const { Import } = this.tenancy.models(tenantId); try {
return await this.importUnhandled(
tenantId,
resourceName,
filename,
params
);
} catch (err) {
deleteImportFile(filename);
throw err;
}
}
/**
* Reads the imported file and stores the import file meta under unqiue id.
* @param {number} tenantId - Tenant id.
* @param {string} resource - Resource name.
* @param {string} filePath - File path.
* @param {string} fileName - File name.
* @returns {Promise<ImportFileUploadPOJO>}
*/
public async importUnhandled(
tenantId: number,
resourceName: string,
filename: string,
params: Record<string, number | string>
): Promise<ImportFileUploadPOJO> {
const resource = sanitizeResourceName(resourceName); const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.resourceService.getResourceMeta( const resourceMeta = this.resourceService.getResourceMeta(
tenantId, tenantId,
@@ -47,10 +74,14 @@ export class ImportFileUploadService {
this.importValidator.validateResourceImportable(resourceMeta); this.importValidator.validateResourceImportable(resourceMeta);
// Reads the imported file into buffer. // Reads the imported file into buffer.
const buffer = await this.importFileCommon.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 = this.importFileCommon.parseXlsxSheet(buffer);
// Throws service error if the sheet data is empty.
validateSheetEmpty(sheetData);
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData); const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
const coumnsStringified = JSON.stringify(sheetColumns); const coumnsStringified = JSON.stringify(sheetColumns);
@@ -70,15 +101,16 @@ export class ImportFileUploadService {
const importFile = await Import.query().insert({ const importFile = await Import.query().insert({
filename, filename,
resource, resource,
tenantId,
importId: filename, importId: filename,
columns: coumnsStringified, columns: coumnsStringified,
params: paramsStringified, params: paramsStringified,
}); });
const resourceColumnsMap = this.resourceService.getResourceImportableFields( const resourceColumnsMap = this.resourceService.getResourceFields2(
tenantId, tenantId,
resource resource
); );
const resourceColumns = this.getResourceColumns(resourceColumnsMap); const resourceColumns = getResourceColumns(resourceColumnsMap);
return { return {
import: { import: {
@@ -89,23 +121,4 @@ export class ImportFileUploadService {
resourceColumns, resourceColumns,
}; };
} }
getResourceColumns(resourceColumns: { [key: string]: IModelMetaField }) {
return Object.entries(resourceColumns)
.map(
([key, { name, importHint, required, order }]: [
string,
IModelMetaField
]) => ({
key,
name,
required,
hint: importHint,
order,
})
)
.sort((a, b) =>
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0
);
}
} }

View File

@@ -0,0 +1,34 @@
import moment from 'moment';
import bluebird from 'bluebird';
import { Import } from '@/system/models';
import { deleteImportFile } from './_utils';
import { Service } from 'typedi';
@Service()
export class ImportDeleteExpiredFiles {
/**
* Delete expired files.
*/
async deleteExpiredFiles() {
const yesterday = moment().subtract(1, 'hour').format('YYYY-MM-DD HH:mm');
const expiredImports = await Import.query().where(
'createdAt',
'<',
yesterday
);
await bluebird.map(
expiredImports,
async (expiredImport) => {
await deleteImportFile(expiredImport.filename);
},
{ concurrency: 10 }
);
const expiredImportsIds = expiredImports.map(
(expiredImport) => expiredImport.id
);
if (expiredImportsIds.length > 0) {
await Import.query().whereIn('id', expiredImportsIds).delete();
}
}
}

View File

@@ -5,7 +5,7 @@ export class ImportableRegistry {
private static instance: ImportableRegistry; private static instance: ImportableRegistry;
private importables: Record<string, Importable>; private importables: Record<string, Importable>;
private constructor() { constructor() {
this.importables = {}; this.importables = {};
} }

View File

@@ -4,6 +4,18 @@ import { ImportableRegistry } from './ImportableRegistry';
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable'; import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable'; import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable'; import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
import { ItemsImportable } from '../Items/ItemsImportable';
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
import { ManualJournalImportable } from '../ManualJournals/ManualJournalsImport';
import { BillsImportable } from '../Purchases/Bills/BillsImportable';
import { ExpensesImportable } from '../Expenses/ExpensesImportable';
import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';
import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable';
import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable';
import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable';
import { PaymentReceivesImportable } from '../Sales/PaymentReceives/PaymentReceivesImportable';
import { CreditNotesImportable } from '../CreditNotes/CreditNotesImportable';
import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
@Service() @Service()
export class ImportableResources { export class ImportableResources {
@@ -24,6 +36,18 @@ export class ImportableResources {
}, },
{ resource: 'Customer', importable: CustomersImportable }, { resource: 'Customer', importable: CustomersImportable },
{ resource: 'Vendor', importable: VendorsImportable }, { resource: 'Vendor', importable: VendorsImportable },
{ resource: 'Item', importable: ItemsImportable },
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
{ resource: 'ManualJournal', importable: ManualJournalImportable },
{ resource: 'Bill', importable: BillsImportable },
{ resource: 'Expense', importable: ExpensesImportable },
{ resource: 'SaleInvoice', importable: SaleInvoicesImportable },
{ resource: 'SaleEstimate', importable: SaleEstimatesImportable },
{ resource: 'BillPayment', importable: BillPaymentsImportable },
{ resource: 'PaymentReceive', importable: PaymentReceivesImportable },
{ resource: 'VendorCredit', importable: VendorCreditsImportable },
{ resource: 'CreditNote', importable: CreditNotesImportable },
{ resource: 'SaleReceipt', importable: SaleReceiptsImportable }
]; ];
public get registry() { public get registry() {

View File

@@ -0,0 +1,3 @@
export const CurrencyParsingDTOs = 10;

View File

@@ -1,9 +1,28 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { upperFirst, camelCase, first, isUndefined } from 'lodash'; import moment from 'moment';
import * as R from 'ramda';
import { Knex } from 'knex';
import fs from 'fs/promises';
import path from 'path';
import {
defaultTo,
upperFirst,
camelCase,
first,
isUndefined,
pickBy,
isEmpty,
castArray,
get,
head,
split,
last,
} from 'lodash';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces'; import { ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField } from '@/interfaces'; import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import moment from 'moment'; import { ServiceError } from '@/exceptions';
import { multiNumberParse } from '@/utils/multi-number-parse';
export const ERRORS = { export const ERRORS = {
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE', RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
@@ -13,9 +32,15 @@ export const ERRORS = {
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED', IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT', INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED', MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
IMPORTED_SHEET_EMPTY: 'IMPORTED_SHEET_EMPTY',
}; };
export function trimObject(obj) { /**
* Trimms the imported object string values before parsing.
* @param {Record<string, string | number>} obj
* @returns {<Record<string, string | number>}
*/
export function trimObject(obj: Record<string, string | number>) {
return Object.entries(obj).reduce((acc, [key, value]) => { return Object.entries(obj).reduce((acc, [key, value]) => {
// Trim the key // Trim the key
const trimmedKey = key.trim(); const trimmedKey = key.trim();
@@ -28,8 +53,14 @@ export function trimObject(obj) {
}, {}); }, {});
} }
/**
* Generates the Yup validation schema based on the given resource fields.
* @param {ResourceMetaFieldsMap} fields
* @returns {Yup}
*/
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => { export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
const yupSchema = {}; const yupSchema = {};
Object.keys(fields).forEach((fieldName: string) => { Object.keys(fields).forEach((fieldName: string) => {
const field = fields[fieldName] as IModelMetaField; const field = fields[fieldName] as IModelMetaField;
let fieldSchema; let fieldSchema;
@@ -79,15 +110,43 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
); );
} else if (field.fieldType === 'url') { } else if (field.fieldType === 'url') {
fieldSchema = fieldSchema.url(); fieldSchema = fieldSchema.url();
} else if (field.fieldType === 'collection') {
const nestedFieldShema = convertFieldsToYupValidation(field.fields);
fieldSchema = Yup.array().label(field.name);
if (!isUndefined(field.collectionMaxLength)) {
fieldSchema = fieldSchema.max(field.collectionMaxLength);
}
if (!isUndefined(field.collectionMinLength)) {
fieldSchema = fieldSchema.min(field.collectionMinLength);
}
fieldSchema = fieldSchema.of(nestedFieldShema);
} }
if (field.required) { if (field.required) {
fieldSchema = fieldSchema.required(); fieldSchema = fieldSchema.required();
} }
yupSchema[fieldName] = fieldSchema; const _fieldName = parseFieldName(fieldName, field);
yupSchema[_fieldName] = fieldSchema;
}); });
return Yup.object().shape(yupSchema); return Yup.object().shape(yupSchema);
}; };
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
let _key = fieldName;
if (field.dataTransferObjectKey) {
_key = field.dataTransferObjectKey;
}
return _key;
};
/**
* Retrieves the unmapped sheet columns.
* @param columns
* @param mapping
* @returns
*/
export const getUnmappedSheetColumns = (columns, mapping) => { export const getUnmappedSheetColumns = (columns, mapping) => {
return columns.filter( return columns.filter(
(column) => !mapping.some((map) => map.from === column) (column) => !mapping.some((map) => map.from === column)
@@ -101,3 +160,300 @@ export const sanitizeResourceName = (resourceName: string) => {
export const getSheetColumns = (sheetData: unknown[]) => { export const getSheetColumns = (sheetData: unknown[]) => {
return Object.keys(first(sheetData)); return Object.keys(first(sheetData));
}; };
/**
* Retrieves the unique value from the given imported object DTO based on the
* configured unique resource field.
* @param {{ [key: string]: IModelMetaField }} importableFields -
* @param {<Record<string, any>}
* @returns {string}
*/
export const getUniqueImportableValue = (
importableFields: { [key: string]: IModelMetaField2 },
objectDTO: Record<string, any>
) => {
const uniqueImportableValue = pickBy(
importableFields,
(field) => field.unique
);
const uniqueImportableKeys = Object.keys(uniqueImportableValue);
const uniqueImportableKey = first(uniqueImportableKeys);
return defaultTo(objectDTO[uniqueImportableKey], '');
};
/**
* Throws service error the given sheet is empty.
* @param {Array<any>} sheetData
*/
export const validateSheetEmpty = (sheetData: Array<any>) => {
if (isEmpty(sheetData)) {
throw new ServiceError(ERRORS.IMPORTED_SHEET_EMPTY);
}
};
const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];
/**
* Parses the given string value to boolean.
* @param {string} value
* @returns {string|null}
*/
export const parseBoolean = (value: string): boolean | null => {
const normalizeValue = (value: string): string =>
value.toString().trim().toLowerCase();
const normalizedValue = normalizeValue(value);
const valuesRepresentingTrue =
booleanValuesRepresentingTrue.map(normalizeValue);
const valueRepresentingFalse =
booleanValuesRepresentingFalse.map(normalizeValue);
if (valuesRepresentingTrue.includes(normalizedValue)) {
return true;
} else if (valueRepresentingFalse.includes(normalizedValue)) {
return false;
}
return null;
};
export const transformInputToGroupedFields = (input) => {
const output = [];
// Group for non-nested fields
const mainGroup = {
groupLabel: '',
groupKey: '',
fields: [],
};
input.forEach((item) => {
if (!item.fields) {
// If the item does not have nested fields, add it to the main group
mainGroup.fields.push(item);
} else {
// If the item has nested fields, create a new group for these fields
output.push({
groupLabel: item.name,
groupKey: item.key,
fields: item.fields,
});
}
});
// Add the main group to the output if it contains any fields
if (mainGroup.fields.length > 0) {
output.unshift(mainGroup); // Add the main group at the beginning
}
return output;
};
export const getResourceColumns = (resourceColumns: {
[key: string]: IModelMetaField2;
}) => {
const mapColumn =
(group: string) =>
([fieldKey, { name, importHint, required, order, ...field }]: [
string,
IModelMetaField2
]) => {
const extra: Record<string, any> = {};
const key = fieldKey;
if (group) {
extra.group = group;
}
if (field.fieldType === 'collection') {
extra.fields = mapColumns(field.fields, key);
}
return {
key,
name,
required,
hint: importHint,
order,
...extra,
};
};
const sortColumn = (a, b) =>
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0;
const mapColumns = (columns, parentKey = '') =>
Object.entries(columns).map(mapColumn(parentKey)).sort(sortColumn);
return R.compose(transformInputToGroupedFields, mapColumns)(resourceColumns);
};
// Prases the given object value based on the field key type.
export const valueParser =
(fields: ResourceMetaFieldsMap, tenantModels: any, trx?: Knex.Transaction) =>
async (value: any, key: string, group = '') => {
let _value = value;
const fieldKey = key.includes('.') ? key.split('.')[0] : key;
const field = group ? fields[group]?.fields[fieldKey] : fields[fieldKey];
// Parses the boolean value.
if (field.fieldType === 'boolean') {
_value = parseBoolean(value);
// Parses the enumeration value.
} else if (field.fieldType === 'enumeration') {
const option = get(field, 'options', []).find(
(option) => option.label === value
);
_value = get(option, 'key');
// Parses the numeric value.
} else if (field.fieldType === 'number') {
_value = multiNumberParse(value);
// Parses the relation value.
} else if (field.fieldType === 'relation') {
const RelationModel = tenantModels[field.relationModel];
if (!RelationModel) {
throw new Error(`The relation model of ${key} field is not exist.`);
}
const relationQuery = RelationModel.query(trx);
const relationKeys = castArray(field?.relationImportMatch);
relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
} else if (field.fieldType === 'collection') {
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const _valueParser = valueParser(fields, tenantModels);
_value = await _valueParser(value, ObjectFieldKey, fieldKey);
}
return _value;
};
/**
* Parses the field key and detarmines the key path.
* @param {{ [key: string]: IModelMetaField2 }} fields
* @param {string} key - Mapped key path. formats: `group.key` or `key`.
* @returns {string}
*/
export const parseKey = R.curry(
(fields: { [key: string]: IModelMetaField2 }, key: string) => {
const fieldKey = getFieldKey(key);
const field = fields[fieldKey];
let _key = key;
if (field.fieldType === 'collection') {
if (field.collectionOf === 'object') {
const nestedFieldKey = last(key.split('.'));
_key = `${fieldKey}[0].${nestedFieldKey}`;
} else if (
field.collectionOf === 'string' ||
field.collectionOf ||
'numberic'
) {
_key = `${fieldKey}`;
}
}
return _key;
}
);
/**
* Retrieves the field root key, for instance: I -> entries.itemId O -> entries.
* @param {string} input
* @returns {string}
*/
export const getFieldKey = (input: string) => {
const keys = split(input, '.');
const firstKey = head(keys).split('[')[0]; // Split by "[" in case of array notation
return firstKey;
};
/**
{ * Aggregates the input array of objects based on a comparator attribute and groups the entries.
* This function is useful for combining multiple entries into a single entry based on a specific attribute,
* while aggregating other attributes into an array.}
*
* @param {Array} input - The array of objects to be aggregated.
* @param {string} comparatorAttr - The attribute of the objects used for comparison to aggregate.
* @param {string} groupOn - The attribute of the objects where the grouped entries will be pushed.
* @returns {Array} - The aggregated array of objects.
*
* @example
* // Example input:
* const input = [
* { id: 1, name: 'John', entries: ['entry1'] },
* { id: 2, name: 'Jane', entries: ['entry2'] },
* { id: 1, name: 'John', entries: ['entry3'] },
* ];
* const comparatorAttr = 'id';
* const groupOn = 'entries';
*
* // Example output:
* const output = [
* { id: 1, name: 'John', entries: ['entry1', 'entry3'] },
* { id: 2, name: 'Jane', entries: ['entry2'] },
* ];
*/
export function aggregate(
input: Array<any>,
comparatorAttr: string,
groupOn: string
): Array<Record<string, any>> {
return input.reduce((acc, curr) => {
const existingEntry = acc.find(
(entry) => entry[comparatorAttr] === curr[comparatorAttr]
);
if (existingEntry) {
existingEntry[groupOn].push(...curr.entries);
} else {
acc.push({ ...curr });
}
return acc;
}, []);
}
/**
* Sanitizes the data in the imported sheet by trimming object keys.
* @param json - The JSON data representing the imported sheet.
* @returns {string[][]} - The sanitized data with trimmed object keys.
*/
export const sanitizeSheetData = (json) => {
return R.compose(R.map(trimObject))(json);
};
/**
* Returns the path to map a value to based on the 'to' and 'group' parameters.
* @param {string} to - The target key to map the value to.
* @param {string} group - The group key to nest the target key under.
* @returns {string} - The path to map the value to.
*/
export const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to;
export const getImportsStoragePath = () => {
return path.join(global.__storage_dir, `/imports`);
}
/**
* Deletes the imported file from the storage and database.
* @param {string} filename
*/
export const deleteImportFile = async (filename: string) => {
const filePath = getImportsStoragePath();
// Deletes the imported file.
await fs.unlink(`${filePath}/${filename}`);
};
/**
* Reads the import file.
* @param {string} filename
* @returns {Promise<Buffer>}
*/
export const readImportFile = (filename: string) => {
const filePath = getImportsStoragePath();
return fs.readFile(`${filePath}/${filename}`);
};

View File

@@ -1,9 +1,10 @@
import { IModelMetaField } from '@/interfaces'; import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import Import from '@/models/Import'; import Import from '@/models/Import';
export interface ImportMappingAttr { export interface ImportMappingAttr {
from: string; from: string;
to: string; to: string;
group?: string;
dateFormat?: string; dateFormat?: string;
} }
@@ -13,7 +14,7 @@ export interface ImportValidationError {
constraints: Record<string, string>; constraints: Record<string, string>;
} }
export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField }; export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField2 };
export interface ImportInsertError { export interface ImportInsertError {
rowNumber: number; rowNumber: number;
@@ -43,6 +44,7 @@ export interface ImportFileMapPOJO {
} }
export interface ImportFilePreviewPOJO { export interface ImportFilePreviewPOJO {
resource: string;
createdCount: number; createdCount: number;
skippedCount: number; skippedCount: number;
totalCount: number; totalCount: number;
@@ -58,19 +60,18 @@ export interface ImportOperSuccess {
} }
export interface ImportOperError { export interface ImportOperError {
error: ImportInsertError; error: ImportInsertError[];
index: number; index: number;
} }
export interface ImportableContext { export interface ImportableContext {
import: Import, import: Import;
rowIndex: number; rowIndex: number;
} }
export const ImportDateFormats = [ export const ImportDateFormats = [
'yyyy-MM-dd', 'yyyy-MM-dd',
'dd.MM.yy', 'dd.MM.yy',
'MM/dd/yy', 'MM/dd/yy',
'dd/MMM/yyyy' 'dd/MMM/yyyy',
] ];

View File

@@ -0,0 +1,28 @@
import Container, { Service } from 'typedi';
import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
@Service()
export class ImportDeleteExpiredFilesJobs {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define('delete-expired-imported-files', this.handler);
}
/**
* Triggers sending invoice mail.
*/
private handler = async (job, done: Function) => {
const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
try {
console.log('Delete expired import files has started.');
await importDeleteExpiredFiles.deleteExpiredFiles();
done();
} catch (error) {
console.log(error);
done(error);
}
};
}

View File

@@ -0,0 +1,38 @@
import { Inject, Service } from 'typedi';
import ItemCategoriesService from './ItemCategoriesService';
import { Importable } from '../Import/Importable';
import { Knex } from 'knex';
import { IItemCategoryOTD } from '@/interfaces';
import { ItemCategoriesSampleData } from './constants';
@Service()
export class ItemCategoriesImportable extends Importable {
@Inject()
private itemCategoriesService: ItemCategoriesService;
/**
* Importing to create new item category service.
* @param {number} tenantId
* @param {any} createDTO
* @param {Knex.Transaction} trx
*/
public async importable(
tenantId: number,
createDTO: IItemCategoryOTD,
trx?: Knex.Transaction
) {
await this.itemCategoriesService.newItemCategory(
tenantId,
createDTO,
{},
trx
);
}
/**
* Item categories sample data used to download sample sheet file.
*/
public sampleData(): any[] {
return ItemCategoriesSampleData;
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import Knex from 'knex'; import { Knex } from 'knex';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
IItemCategory, IItemCategory,
@@ -102,7 +102,10 @@ export default class ItemCategoriesService implements IItemCategoriesService {
} }
}); });
if (foundItemCategory) { if (foundItemCategory) {
throw new ServiceError(ERRORS.CATEGORY_NAME_EXISTS); throw new ServiceError(
ERRORS.CATEGORY_NAME_EXISTS,
'The item category name is already exist.'
);
} }
} }
@@ -115,7 +118,8 @@ export default class ItemCategoriesService implements IItemCategoriesService {
public async newItemCategory( public async newItemCategory(
tenantId: number, tenantId: number,
itemCategoryOTD: IItemCategoryOTD, itemCategoryOTD: IItemCategoryOTD,
authorizedUser: ISystemUser authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<IItemCategory> { ): Promise<IItemCategory> {
const { ItemCategory } = this.tenancy.models(tenantId); const { ItemCategory } = this.tenancy.models(tenantId);
@@ -139,20 +143,24 @@ export default class ItemCategoriesService implements IItemCategoriesService {
authorizedUser authorizedUser
); );
// Creates item category under unit-of-work evnirement. // Creates item category under unit-of-work evnirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Inserts the item category. tenantId,
const itemCategory = await ItemCategory.query(trx).insert({ async (trx: Knex.Transaction) => {
...itemCategoryObj, // Inserts the item category.
}); const itemCategory = await ItemCategory.query(trx).insert({
// Triggers `onItemCategoryCreated` event. ...itemCategoryObj,
await this.eventPublisher.emitAsync(events.itemCategory.onCreated, { });
itemCategory, // Triggers `onItemCategoryCreated` event.
tenantId, await this.eventPublisher.emitAsync(events.itemCategory.onCreated, {
trx, itemCategory,
} as IItemCategoryCreatedPayload); tenantId,
trx,
} as IItemCategoryCreatedPayload);
return itemCategory; return itemCategory;
}); },
trx
);
} }
/** /**

View File

@@ -11,3 +11,25 @@ export const ERRORS = {
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS', CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS',
}; };
export const ItemCategoriesSampleData = [
{
Name: 'Kassulke Group',
Description: 'Optio itaque eaque qui adipisci illo sed.',
},
{
Name: 'Crist, Mraz and Lueilwitz',
Description:
'Dolores veniam deserunt sed commodi error quia veritatis non.',
},
{
Name: 'Gutmann and Sons',
Description:
'Ratione aperiam voluptas rem adipisci assumenda eos neque veritatis tempora.',
},
{
Name: 'Reichel - Raynor',
Description:
'Necessitatibus repellendus placeat possimus dolores excepturi ut.',
},
];

View File

@@ -88,7 +88,11 @@ export class CreateItem {
* @param {IItemDTO} item * @param {IItemDTO} item
* @return {Promise<IItem>} * @return {Promise<IItem>}
*/ */
public async createItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem> { public async createItem(
tenantId: number,
itemDTO: IItemDTO,
trx?: Knex.Transaction
): Promise<IItem> {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
// Authorize the item before creating. // Authorize the item before creating.
@@ -111,7 +115,8 @@ export class CreateItem {
} as IItemEventCreatedPayload); } as IItemEventCreatedPayload);
return item; return item;
} },
trx
); );
return item; return item;
} }

View File

@@ -35,7 +35,10 @@ export class ItemsValidators {
} }
}); });
if (foundItems.length > 0) { if (foundItems.length > 0) {
throw new ServiceError(ERRORS.ITEM_NAME_EXISTS); throw new ServiceError(
ERRORS.ITEM_NAME_EXISTS,
'The item name is already exist.'
);
} }
} }

View File

@@ -0,0 +1,34 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { Importable } from '@/services/Import/Importable';
import { IItemCreateDTO } from '@/interfaces';
import { CreateItem } from './CreateItem';
import { ItemsSampleData } from './constants';
@Service()
export class ItemsImportable extends Importable {
@Inject()
private createItemService: CreateItem;
/**
* Mapps the imported data to create a new item service.
* @param {number} tenantId
* @param {ICustomerNewDTO} createDTO
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public async importable(
tenantId: number,
createDTO: IItemCreateDTO,
trx?: Knex.Transaction<any, any[]>
): Promise<void> {
await this.createItemService.createItem(tenantId, createDTO, trx);
}
/**
* Retrieves the sample data of customers used to download sample sheet.
*/
public sampleData(): any[] {
return ItemsSampleData;
}
}

View File

@@ -1,4 +1,3 @@
export const ERRORS = { export const ERRORS = {
NOT_FOUND: 'NOT_FOUND', NOT_FOUND: 'NOT_FOUND',
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
@@ -19,7 +18,8 @@ export const ERRORS = {
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT: ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE', ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS:
'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED', INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS', ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
@@ -53,8 +53,84 @@ export const DEFAULT_VIEWS = [
slug: 'non-inventory', slug: 'non-inventory',
rolesLogicExpression: '1', rolesLogicExpression: '1',
roles: [ roles: [
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'non-inventory' }, {
index: 1,
fieldKey: 'type',
comparator: 'equals',
value: 'non-inventory',
},
], ],
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },
] ];
export const ItemsSampleData = [
{
'Item Type': 'Inventory',
'Item Name': 'Hettinger, Schumm and Bartoletti',
'Item Code': '1000',
Sellable: 'T',
Purchasable: 'T',
'Cost Price': '10000',
'Sell Price': '1000',
'Cost Account': 'Cost of Goods Sold',
'Sell Account': 'Other Income',
'Inventory Account': 'Inventory Asset',
'Sell Description': 'Description ....',
'Purchase Description': 'Description ....',
Category: 'sdafasdfsadf',
Note: 'At dolor est non tempore et quisquam.',
Active: 'TRUE',
},
{
'Item Type': 'Inventory',
'Item Name': 'Schmitt Group',
'Item Code': '1001',
Sellable: 'T',
Purchasable: 'T',
'Cost Price': '10000',
'Sell Price': '1000',
'Cost Account': 'Cost of Goods Sold',
'Sell Account': 'Other Income',
'Inventory Account': 'Inventory Asset',
'Sell Description': 'Description ....',
'Purchase Description': 'Description ....',
Category: 'sdafasdfsadf',
Note: 'Id perspiciatis at adipisci minus accusamus dolor iure dolore.',
Active: 'TRUE',
},
{
'Item Type': 'Inventory',
'Item Name': 'Marks - Carroll',
'Item Code': '1002',
Sellable: 'T',
Purchasable: 'T',
'Cost Price': '10000',
'Sell Price': '1000',
'Cost Account': 'Cost of Goods Sold',
'Sell Account': 'Other Income',
'Inventory Account': 'Inventory Asset',
'Sell Description': 'Description ....',
'Purchase Description': 'Description ....',
Category: 'sdafasdfsadf',
Note: 'Odio odio minus similique.',
Active: 'TRUE',
},
{
'Item Type': 'Inventory',
'Item Name': 'VonRueden, Ruecker and Hettinger',
'Item Code': '1003',
Sellable: 'T',
Purchasable: 'T',
'Cost Price': '10000',
'Sell Price': '1000',
'Cost Account': 'Cost of Goods Sold',
'Sell Account': 'Other Income',
'Inventory Account': 'Inventory Asset',
'Sell Description': 'Description ....',
'Purchase Description': 'Description ....',
Category: 'sdafasdfsadf',
Note: 'Quibusdam dolores illo.',
Active: 'TRUE',
},
];

View File

@@ -82,7 +82,10 @@ export class CommandManualJournalValidators {
} }
}); });
if (journals.length > 0) { if (journals.length > 0) {
throw new ServiceError(ERRORS.JOURNAL_NUMBER_EXISTS); throw new ServiceError(
ERRORS.JOURNAL_NUMBER_EXISTS,
'The journal number is already exist.'
);
} }
} }

View File

@@ -73,9 +73,7 @@ export class CreateManualJournalService {
return R.compose( return R.compose(
// Omits the `branchId` from entries if multiply branches feature not active. // Omits the `branchId` from entries if multiply branches feature not active.
this.branchesDTOTransformer.transformDTO(tenantId) this.branchesDTOTransformer.transformDTO(tenantId)
)( )(initialDTO);
initialDTO
);
} }
/** /**
@@ -133,7 +131,8 @@ export class CreateManualJournalService {
public makeJournalEntries = async ( public makeJournalEntries = async (
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
authorizedUser: ISystemUser authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<{ manualJournal: IManualJournal }> => { ): Promise<{ manualJournal: IManualJournal }> => {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
@@ -156,27 +155,31 @@ export class CreateManualJournalService {
); );
// Creates a manual journal transactions with associated transactions // Creates a manual journal transactions with associated transactions
// under unit-of-work envirement. // under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onManualJournalCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, { async (trx: Knex.Transaction) => {
tenantId, // Triggers `onManualJournalCreating` event.
manualJournalDTO, await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
trx, tenantId,
} as IManualJournalCreatingPayload); manualJournalDTO,
trx,
} as IManualJournalCreatingPayload);
// Upsert the manual journal object. // Upsert the manual journal object.
const manualJournal = await ManualJournal.query(trx).upsertGraph({ const manualJournal = await ManualJournal.query(trx).upsertGraph({
...manualJournalObj, ...manualJournalObj,
}); });
// Triggers `onManualJournalCreated` event. // Triggers `onManualJournalCreated` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, { await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
tenantId, tenantId,
manualJournal, manualJournal,
manualJournalId: manualJournal.id, manualJournalId: manualJournal.id,
trx, trx,
} as IManualJournalEventCreatedPayload); } as IManualJournalEventCreatedPayload);
return { manualJournal }; return { manualJournal };
}); },
trx
);
}; };
} }

View File

@@ -0,0 +1,60 @@
import { Inject } from 'typedi';
import { Knex } from 'knex';
import * as Yup from 'yup';
import { Importable } from '../Import/Importable';
import { CreateManualJournalService } from './CreateManualJournal';
import { IManualJournalDTO } from '@/interfaces';
import { ImportableContext } from '../Import/interfaces';
import { ManualJournalsSampleData } from './constants';
export class ManualJournalImportable extends Importable {
@Inject()
private createManualJournalService: CreateManualJournalService;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createJournalDTO: IManualJournalDTO,
trx?: Knex.Transaction
) {
return this.createManualJournalService.makeJournalEntries(
tenantId,
createJournalDTO,
{},
trx
);
}
/**
* Transformes the DTO before passing it to importable and validation.
* @param {Record<string, any>} createDTO
* @param {ImportableContext} context
* @returns {Record<string, any>}
*/
public transform(createDTO: Record<string, any>, context: ImportableContext) {
return createDTO;
}
/**
* Params validation schema.
* @returns {ValidationSchema[]}
*/
public paramsValidationSchema() {
return Yup.object().shape({
autoIncrement: Yup.boolean(),
});
}
/**
* Retrieves the sample data of manual journals that used to download sample sheet.
* @returns {Record<string, any>}
*/
public sampleData(): Record<string, any>[] {
return ManualJournalsSampleData;
}
}

View File

@@ -29,3 +29,36 @@ export const CONTACTS_CONFIG = [
]; ];
export const DEFAULT_VIEWS = []; export const DEFAULT_VIEWS = [];
export const ManualJournalsSampleData = [
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'Animi quasi qui itaque aut possimus illum est magnam enim.',
Credit: 1000,
Debit: 0,
Note: 'Qui reprehenderit voluptate.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'In assumenda dicta autem non est corrupti non et.',
Credit: 0,
Debit: 1000,
Note: 'Omnis tempora qui fugiat neque dolor voluptatem aut repudiandae nihil.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
];

View File

@@ -144,6 +144,7 @@ export default class OrganizationService {
public async currentOrganization(tenantId: number): Promise<ITenant> { public async currentOrganization(tenantId: number): Promise<ITenant> {
const tenant = await Tenant.query() const tenant = await Tenant.query()
.findById(tenantId) .findById(tenantId)
.withGraphFetched('subscriptions')
.withGraphFetched('metadata'); .withGraphFetched('metadata');
this.throwIfTenantNotExists(tenant); this.throwIfTenantNotExists(tenant);

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IBillPaymentDTO } from '@/interfaces';
import { CreateBillPayment } from './CreateBillPayment';
import { Importable } from '@/services/Import/Importable';
import { BillsPaymentsSampleData } from './constants';
@Service()
export class BillPaymentsImportable extends Importable {
@Inject()
private createBillPaymentService: CreateBillPayment;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
billPaymentDTO: IBillPaymentDTO,
trx?: Knex.Transaction
) {
return this.createBillPaymentService.createBillPayment(
tenantId,
billPaymentDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return BillsPaymentsSampleData;
}
}

View File

@@ -48,7 +48,8 @@ export class CreateBillPayment {
*/ */
public async createBillPayment( public async createBillPayment(
tenantId: number, tenantId: number,
billPaymentDTO: IBillPaymentDTO billPaymentDTO: IBillPaymentDTO,
trx?: Knex.Transaction
): Promise<IBillPayment> { ): Promise<IBillPayment> {
const { BillPayment, Contact } = this.tenancy.models(tenantId); const { BillPayment, Contact } = this.tenancy.models(tenantId);
@@ -97,28 +98,32 @@ export class CreateBillPayment {
); );
// Writes bill payment transacation with associated transactions // Writes bill payment transacation with associated transactions
// under unit-of-work envirement. // under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onBillPaymentCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.billPayment.onCreating, { async (trx: Knex.Transaction) => {
tenantId, // Triggers `onBillPaymentCreating` event.
billPaymentDTO, await this.eventPublisher.emitAsync(events.billPayment.onCreating, {
trx, tenantId,
} as IBillPaymentCreatingPayload); billPaymentDTO,
trx,
} as IBillPaymentCreatingPayload);
// Writes the bill payment graph to the storage. // Writes the bill payment graph to the storage.
const billPayment = await BillPayment.query(trx).insertGraphAndFetch({ const billPayment = await BillPayment.query(trx).insertGraphAndFetch({
...billPaymentObj, ...billPaymentObj,
}); });
// Triggers `onBillPaymentCreated` event. // Triggers `onBillPaymentCreated` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreated, { await this.eventPublisher.emitAsync(events.billPayment.onCreated, {
tenantId, tenantId,
billPayment, billPayment,
billPaymentId: billPayment.id, billPaymentId: billPayment.id,
trx, trx,
} as IBillPaymentEventCreatedPayload); } as IBillPaymentEventCreatedPayload);
return billPayment; return billPayment;
}); },
trx
);
} }
} }

View File

@@ -15,3 +15,36 @@ export const ERRORS = {
}; };
export const DEFAULT_VIEWS = []; export const DEFAULT_VIEWS = [];
export const BillsPaymentsSampleData = [
{
'Payment Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10001',
'Reference No.': 'REF-1',
'Payment Account': 'Petty Cash',
Statement: 'Vel et dolorem architecto veniam.',
'Bill No': 'B-120',
'Payment Amount': 100,
},
{
'Payment Date': '2024-03-02',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10002',
'Reference No.': 'REF-2',
'Payment Account': 'Petty Cash',
Statement: 'Id est molestias.',
'Bill No': 'B-121',
'Payment Amount': 100,
},
{
'Payment Date': '2024-03-03',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10003',
'Reference No.': 'REF-3',
'Payment Account': 'Petty Cash',
Statement: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
'Bill No': 'B-122',
'Payment Amount': 100,
},
];

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { Importable } from '@/services/Import/Importable';
import { CreateBill } from './CreateBill';
import { IBillDTO } from '@/interfaces';
import { BillsSampleData } from './constants';
@Service()
export class BillsImportable extends Importable {
@Inject()
private createBillService: CreateBill;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: IBillDTO,
trx?: Knex.Transaction
) {
return this.createBillService.createBill(
tenantId,
createAccountDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return BillsSampleData;
}
}

View File

@@ -28,7 +28,7 @@ export class BillsValidators {
*/ */
public validateBillAmountBiggerPaidAmount( public validateBillAmountBiggerPaidAmount(
billAmount: number, billAmount: number,
paidAmount: number, paidAmount: number
) { ) {
if (billAmount < paidAmount) { if (billAmount < paidAmount) {
throw new ServiceError(ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT); throw new ServiceError(ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT);
@@ -53,7 +53,10 @@ export class BillsValidators {
}); });
if (foundBills.length > 0) { if (foundBills.length > 0) {
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS); throw new ServiceError(
ERRORS.BILL_NUMBER_EXISTS,
'The bill number is not unique.'
);
} }
} }

View File

@@ -53,7 +53,8 @@ export class CreateBill {
public async createBill( public async createBill(
tenantId: number, tenantId: number,
billDTO: IBillDTO, billDTO: IBillDTO,
authorizedUser: ISystemUser authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<IBill> { ): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId); const { Bill, Contact } = this.tenancy.models(tenantId);
@@ -91,26 +92,30 @@ export class CreateBill {
authorizedUser authorizedUser
); );
// Write new bill transaction with associated transactions under UOW env. // Write new bill transaction with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onBillCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.bill.onCreating, { async (trx: Knex.Transaction) => {
trx, // Triggers `onBillCreating` event.
billDTO, await this.eventPublisher.emitAsync(events.bill.onCreating, {
tenantId, trx,
} as IBillCreatingPayload); billDTO,
tenantId,
} as IBillCreatingPayload);
// Inserts the bill graph object to the storage. // Inserts the bill graph object to the storage.
const bill = await Bill.query(trx).upsertGraph(billObj); const bill = await Bill.query(trx).upsertGraph(billObj);
// Triggers `onBillCreated` event. // Triggers `onBillCreated` event.
await this.eventPublisher.emitAsync(events.bill.onCreated, { await this.eventPublisher.emitAsync(events.bill.onCreated, {
tenantId, tenantId,
bill, bill,
billId: bill.id, billId: bill.id,
trx, trx,
} as IBillCreatedPayload); } as IBillCreatedPayload);
return bill; return bill;
}); },
trx
);
} }
} }

View File

@@ -75,3 +75,49 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },
]; ];
export const BillsSampleData = [
{
'Bill No.': 'B-101',
'Reference No.': 'REF0',
Date: '2024-01-01',
'Due Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Exchange Rate': 1,
Note: 'Vel in sit sint.',
Open: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
'Line Description': 'Id a vel quis vel aut.',
},
{
'Bill No.': 'B-102',
'Reference No.': 'REF0',
Date: '2024-01-01',
'Due Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Exchange Rate': 1,
Note: 'Quia ut dolorem qui sint velit.',
Open: 'T',
Item: 'Thompson - Reichert',
Quantity: 200,
Rate: 50,
'Line Description':
'Nesciunt in adipisci quia ab reiciendis nam sed saepe consequatur.',
},
{
'Bill No.': 'B-103',
'Reference No.': 'REF0',
Date: '2024-01-01',
'Due Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Exchange Rate': 1,
Note: 'Dolore aut voluptatem minus pariatur alias pariatur.',
Open: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
'Line Description': 'Quam eligendi provident.',
},
];

View File

@@ -30,10 +30,12 @@ export default class CreateVendorCredit extends BaseVendorCredit {
* Creates a new vendor credit. * Creates a new vendor credit.
* @param {number} tenantId - * @param {number} tenantId -
* @param {IVendorCreditCreateDTO} vendorCreditCreateDTO - * @param {IVendorCreditCreateDTO} vendorCreditCreateDTO -
* @param {Knex.Transaction} trx -
*/ */
public newVendorCredit = async ( public newVendorCredit = async (
tenantId: number, tenantId: number,
vendorCreditCreateDTO: IVendorCreditCreateDTO vendorCreditCreateDTO: IVendorCreditCreateDTO,
trx?: Knex.Transaction
) => { ) => {
const { VendorCredit, Vendor } = this.tenancy.models(tenantId); const { VendorCredit, Vendor } = this.tenancy.models(tenantId);
@@ -59,27 +61,31 @@ export default class CreateVendorCredit extends BaseVendorCredit {
vendor.currencyCode vendor.currencyCode
); );
// Saves the vendor credit transactions under UOW envirement. // Saves the vendor credit transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onVendorCreditCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.vendorCredit.onCreating, { async (trx: Knex.Transaction) => {
tenantId, // Triggers `onVendorCreditCreating` event.
vendorCreditCreateDTO, await this.eventPublisher.emitAsync(events.vendorCredit.onCreating, {
trx, tenantId,
} as IVendorCreditCreatingPayload); vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatingPayload);
// Saves the vendor credit graph. // Saves the vendor credit graph.
const vendorCredit = await VendorCredit.query(trx).upsertGraphAndFetch({ const vendorCredit = await VendorCredit.query(trx).upsertGraphAndFetch({
...vendorCreditModel, ...vendorCreditModel,
}); });
// Triggers `onVendorCreditCreated` event. // Triggers `onVendorCreditCreated` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreated, { await this.eventPublisher.emitAsync(events.vendorCredit.onCreated, {
tenantId, tenantId,
vendorCredit, vendorCredit,
vendorCreditCreateDTO, vendorCreditCreateDTO,
trx, trx,
} as IVendorCreditCreatedPayload); } as IVendorCreditCreatedPayload);
return vendorCredit; return vendorCredit;
}); },
trx
);
}; };
} }

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { Importable } from '@/services/Import/Importable';
import CreateVendorCredit from './CreateVendorCredit';
import { IVendorCreditCreateDTO } from '@/interfaces';
import { VendorCreditsSampleData } from './constants';
@Service()
export class VendorCreditsImportable extends Importable {
@Inject()
private createVendorCreditService: CreateVendorCredit;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createPaymentDTO: IVendorCreditCreateDTO,
trx?: Knex.Transaction
) {
return this.createVendorCreditService.newVendorCredit(
tenantId,
createPaymentDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return VendorCreditsSampleData;
}
}

View File

@@ -1,11 +1,14 @@
export const ERRORS = { export const ERRORS = {
VENDOR_CREDIT_NOT_FOUND: 'VENDOR_CREDIT_NOT_FOUND', VENDOR_CREDIT_NOT_FOUND: 'VENDOR_CREDIT_NOT_FOUND',
VENDOR_CREDIT_ALREADY_OPENED: 'VENDOR_CREDIT_ALREADY_OPENED', VENDOR_CREDIT_ALREADY_OPENED: 'VENDOR_CREDIT_ALREADY_OPENED',
VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT: 'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT', VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT:
VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND: 'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND', 'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND:
'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND',
BILLS_HAS_NO_REMAINING_AMOUNT: 'BILLS_HAS_NO_REMAINING_AMOUNT', BILLS_HAS_NO_REMAINING_AMOUNT: 'BILLS_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS: 'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS', VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS:
VENDOR_CREDIT_HAS_APPLIED_BILLS: 'VENDOR_CREDIT_HAS_APPLIED_BILLS' 'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS',
VENDOR_CREDIT_HAS_APPLIED_BILLS: 'VENDOR_CREDIT_HAS_APPLIED_BILLS',
}; };
export const DEFAULT_VIEW_COLUMNS = []; export const DEFAULT_VIEW_COLUMNS = [];
@@ -62,3 +65,18 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },
]; ];
export const VendorCreditsSampleData = [
{
Vendor: 'Randall Kohler VENDOR',
'Vendor Credit Date': '2024-01-01',
'Vendor Credit No.': 'VC-0001',
'Reference No.': 'REF-00001',
'Exchange Rate': '',
Note: 'Note',
Open: 'T',
'Item Name': 'Hettinger, Schumm and Bartoletti',
Quantity: 100,
Rate: 100,
},
];

View File

@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import { camelCase, upperFirst, pickBy } from 'lodash'; import { camelCase, upperFirst, pickBy } from 'lodash';
import * as qim from 'qim'; import * as qim from 'qim';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { IModelMeta, IModelMetaField } from '@/interfaces'; import { IModelMeta, IModelMetaField, IModelMetaField2 } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import I18nService from '@/services/I18n/I18nService'; import I18nService from '@/services/I18n/I18nService';
@@ -74,6 +74,15 @@ export default class ResourceService {
return meta.fields; return meta.fields;
} }
public getResourceFields2(
tenantId: number,
modelName: string
): { [key: string]: IModelMetaField2 } {
const meta = this.getResourceMeta(tenantId, modelName);
return meta.fields2;
}
/** /**
* *
* @param {number} tenantId * @param {number} tenantId
@@ -96,9 +105,14 @@ export default class ResourceService {
const $enumerationType = (field) => const $enumerationType = (field) =>
field.fieldType === 'enumeration' ? field : undefined; field.fieldType === 'enumeration' ? field : undefined;
const $hasFields = (field) => 'undefined' !== typeof field.fields ? field : undefined;
const naviagations = [ const naviagations = [
['fields', qim.$each, 'name'], ['fields', qim.$each, 'name'],
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, 'name'],
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
]; ];
return this.i18nService.i18nApply(naviagations, meta, tenantId); return this.i18nService.i18nApply(naviagations, meta, tenantId);
} }

View File

@@ -43,7 +43,8 @@ export class CreateSaleEstimate {
*/ */
public async createEstimate( public async createEstimate(
tenantId: number, tenantId: number,
estimateDTO: ISaleEstimateDTO estimateDTO: ISaleEstimateDTO,
trx?: Knex.Transaction
): Promise<ISaleEstimate> { ): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId); const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
@@ -75,28 +76,32 @@ export class CreateSaleEstimate {
estimateDTO.entries estimateDTO.entries
); );
// Creates a sale estimate transaction with associated transactions as UOW. // Creates a sale estimate transaction with associated transactions as UOW.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onSaleEstimateCreating` event. tenantId,
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, { async (trx: Knex.Transaction) => {
estimateDTO, // Triggers `onSaleEstimateCreating` event.
tenantId, await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
trx, estimateDTO,
} as ISaleEstimateCreatingPayload); tenantId,
trx,
} as ISaleEstimateCreatingPayload);
// Upsert the sale estimate graph to the storage. // Upsert the sale estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({ const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
...estimateObj, ...estimateObj,
}); });
// Triggers `onSaleEstimateCreated` event. // Triggers `onSaleEstimateCreated` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, { await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
tenantId, tenantId,
saleEstimate, saleEstimate,
saleEstimateId: saleEstimate.id, saleEstimateId: saleEstimate.id,
saleEstimateDTO: estimateDTO, saleEstimateDTO: estimateDTO,
trx, trx,
} as ISaleEstimateCreatedPayload); } as ISaleEstimateCreatedPayload);
return saleEstimate; return saleEstimate;
}); },
trx
);
} }
} }

View File

@@ -41,7 +41,10 @@ export class SaleEstimateValidators {
} }
}); });
if (foundSaleEstimate) { if (foundSaleEstimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE); throw new ServiceError(
ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE,
'The given sale estimate is not unique.'
);
} }
} }

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ISaleEstimateDTO } from '@/interfaces';
import { CreateSaleEstimate } from './CreateSaleEstimate';
import { Importable } from '@/services/Import/Importable';
import { SaleEstimatesSampleData } from './constants';
@Service()
export class SaleEstimatesImportable extends Importable {
@Inject()
private createEstimateService: CreateSaleEstimate;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createEstimateDTO: ISaleEstimateDTO,
trx?: Knex.Transaction
) {
return this.createEstimateService.createEstimate(
tenantId,
createEstimateDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return SaleEstimatesSampleData;
}
}

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