Compare commits

...

169 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
2a944f8507 feat: Add Stripe payment env variables examples 2024-10-01 12:53:27 +02:00
Ahmed Bouhuolia
8a2754d9ce Merge pull request #683 from bigcapitalhq/feat-hook-up-customer-address
feat: Hook up customer/company address to invoice preview of payment page
2024-10-01 09:49:14 +02:00
Ahmed Bouhuolia
ace75f2dfa feat: Hook up customer/company address to invoice preview of payment page 2024-10-01 09:48:07 +02:00
Ahmed Bouhuolia
7ceb785c1b Merge pull request #682 from bigcapitalhq/listen-stripe-integration-events
feat: Listen to Stripe integration events
2024-09-30 23:13:14 +02:00
Ahmed Bouhuolia
904a52f5a1 feat: listen to Stripe integration events 2024-09-30 23:12:42 +02:00
Ahmed Bouhuolia
04fe65b176 fix: payment link events tracker 2024-09-30 18:09:57 +02:00
Ahmed Bouhuolia
7ac6e0d349 Merge pull request #681 from bigcapitalhq/fix-pdf-template-customize-content
fix: Branding customize content
2024-09-30 14:52:36 +02:00
Ahmed Bouhuolia
4ec3586173 fix: branding customize content 2024-09-30 14:51:03 +02:00
Ahmed Bouhuolia
4b6ab7035e Merge pull request #680 from bigcapitalhq/add-posthog-events-tracking-to-pdf-templates
feat: Track pdf templates Posthog events
2024-09-30 12:44:27 +02:00
Ahmed Bouhuolia
3fe7babe00 feat: Track pdf templates Posthog events 2024-09-30 12:43:51 +02:00
Ahmed Bouhuolia
f21570982e Merge pull request #679 from bigcapitalhq/fix-listen-to-stripe-session-completed
fix: Listen to Stripe session completed event
2024-09-30 11:49:48 +02:00
Ahmed Bouhuolia
ad8fe52b84 fix: Listen to Stripe session completed event 2024-09-30 11:49:19 +02:00
Ahmed Bouhuolia
15ce6ac710 Merge pull request #678 from bigcapitalhq/pdf-templates-company-customer-address
feat: Pdf templates customer/company addresses
2024-09-30 11:21:14 +02:00
Ahmed Bouhuolia
783387dce6 fix: pdf templates server-side rendered 2024-09-30 11:15:05 +02:00
Ahmed Bouhuolia
863c7ad99f feat: Hook up customer/company address to pdf templates 2024-09-29 22:59:14 +02:00
Ahmed Bouhuolia
776b69475c feat: PDF templates company/customer address 2024-09-29 19:31:00 +02:00
Ahmed Bouhuolia
6b6027a588 feat: Pdf templates customer/company addresses 2024-09-29 18:04:56 +02:00
Ahmed Bouhuolia
d465ee15bd Merge pull request #677 from bigcapitalhq/preferences-company-branding
feat: Company branding preferences
2024-09-29 13:44:29 +02:00
Ahmed Bouhuolia
be2049ca6e feat: Pdf template address 2024-09-29 13:43:09 +02:00
Ahmed Bouhuolia
9b63c176cd feat: Hook up company address to payment page 2024-09-28 20:19:05 +02:00
Ahmed Bouhuolia
e506a7ba35 feat: Hook orgnization name and logo to payment page 2024-09-28 19:20:01 +02:00
Ahmed Bouhuolia
2191ad0d40 feat: hook up preferences branding form 2024-09-28 18:44:08 +02:00
Ahmed Bouhuolia
ca162206a3 feat: Organization address and branding patch endpoint 2024-09-28 17:43:47 +02:00
Ahmed Bouhuolia
c5d7a2bfd8 feat: Company branding preferences 2024-09-28 14:47:59 +02:00
Ahmed Bouhuolia
b9506424d1 Merge pull request #675 from bigcapitalhq/hook-up-company-logo-to-pdf-templates
feat: Hook up company logo to server-side pdf templates
2024-09-26 18:33:45 +02:00
Ahmed Bouhuolia
46a145ae58 feat: Hook up company logo to server-side pdf templates 2024-09-26 18:33:21 +02:00
Ahmed Bouhuolia
e4044ef563 Merge pull request #674 from bigcapitalhq/clean-up-payment-links-endpoints
feat: Clean up payment links endpoints
2024-09-25 19:38:28 +02:00
Ahmed Bouhuolia
1cc71eb368 feat: Clean up payment links endpoints 2024-09-25 19:37:40 +02:00
Ahmed Bouhuolia
323b95de7b Merge pull request #673 from bigcapitalhq/fix-invoice-customize-bugs
fix: Invoice customize bugs
2024-09-25 15:21:12 +02:00
Ahmed Bouhuolia
b0658be041 fix: Invoice customize bugs 2024-09-25 15:20:24 +02:00
Ahmed Bouhuolia
b222d56148 Merge pull request #672 from bigcapitalhq/fix-invoice-brand-customize
fix: Invoice pdf customize
2024-09-25 12:22:07 +02:00
Ahmed Bouhuolia
946872204b fix: payment page 2024-09-25 12:21:26 +02:00
Ahmed Bouhuolia
2f9adfd908 fix: Invoice pdf customize 2024-09-25 11:04:17 +02:00
Ahmed Bouhuolia
1c8e19378f Merge pull request #670 from bigcapitalhq/upload-company-logo
feat: Upload company logo to invoice templates
2024-09-24 20:31:12 +02:00
Ahmed Bouhuolia
37fd4a1fdb feat: Uploading company logo 2024-09-24 20:28:19 +02:00
Ahmed Bouhuolia
7aed3d9c8c Merge pull request #668 from bigcapitalhq/stripe-integrate
feat: Onboard accounts to Stripe Connect
2024-09-24 14:12:39 +02:00
Ahmed Bouhuolia
b125e3e58b feat: Stripe connect using OAuth 2024-09-24 14:10:53 +02:00
Ahmed Bouhuolia
70bba4a6ed fix: Stripe integration content 2024-09-23 17:34:27 +02:00
Ahmed Bouhuolia
1570995021 feat: Add Stripe pre-setup dialog 2024-09-23 14:44:07 +02:00
Ahmed Bouhuolia
8109236e72 fix: Stripe payment integration 2024-09-23 13:21:54 +02:00
Ahmed Bouhuolia
9ba651decb feat: Delete Stripe pamyent connection 2024-09-22 21:57:46 +02:00
Ahmed Bouhuolia
eb5fdbf4ee feat: Control the payment method from invoice form 2024-09-22 21:23:02 +02:00
Ahmed Bouhuolia
9827a84857 feat: Hook up edit Stripe settings form 2024-09-22 17:25:27 +02:00
Ahmed Bouhuolia
3308133736 feat: Edit Stripe payment settings 2024-09-22 14:55:48 +02:00
Ahmed Bouhuolia
3129c76c30 feat: Delete Stripe payment method 2024-09-22 14:30:47 +02:00
Ahmed Bouhuolia
c0a4c965f0 feat: Edit stripe payment integation drawer 2024-09-22 12:52:59 +02:00
Ahmed Bouhuolia
d16c57b63b feat: Upload company logo to invoice templates 2024-09-22 00:01:12 +02:00
Ahmed Bouhuolia
e04f5d26a3 feat: listen to stripe account updated webhook 2024-09-21 23:59:54 +02:00
Ahmed Bouhuolia
ad74007d58 fix: Style of paper template address 2024-09-21 20:04:23 +02:00
Ahmed Bouhuolia
6271d6c268 chore: remove newrelic logs file 2024-09-21 19:13:20 +02:00
Ahmed Bouhuolia
7756b5b304 feat: Stripe payment integration 2024-09-21 16:50:22 +02:00
Ahmed Bouhuolia
8de8695b25 feat: clean up the style of public payment page. 2024-09-21 09:53:00 +02:00
Ahmed Bouhuolia
11c56c75a4 feat: clean up the stripe payment integration 2024-09-21 09:18:39 +02:00
Ahmed Bouhuolia
f5a1d68c52 feat: Stripe payment checkout session 2024-09-19 22:24:07 +02:00
Ahmed Bouhuolia
0ae7a25c27 feat: Map the invoice preview data 2024-09-19 14:32:14 +02:00
Ahmed Bouhuolia
16eaacd4bc feat: Payment invoice preview drawer 2024-09-19 12:45:06 +02:00
Ahmed Bouhuolia
809973730f feat: style tweaks in the public payment page 2024-09-19 11:28:40 +02:00
Ahmed Bouhuolia
2ebb4595a8 feat: Emit Stripe webhooks to events in the system 2024-09-19 10:25:13 +02:00
Ahmed Bouhuolia
77f628509c fix: make the base url of payment link configurable 2024-09-18 23:53:46 +02:00
Ahmed Bouhuolia
d2cd32a735 feat: inactive associated Stripe payment link on invoice deleting 2024-09-18 23:41:59 +02:00
Ahmed Bouhuolia
4665f529e6 feat: integrate Stripe payment to invoices 2024-09-18 19:24:01 +02:00
Ahmed Bouhuolia
df706d2573 feat: payment methods preferences page 2024-09-18 11:19:59 +02:00
Ahmed Bouhuolia
5270e99de8 feat: select payment methods dialog 2024-09-18 10:43:21 +02:00
Ahmed Bouhuolia
eb48f66f6e Merge branch 'develop' into stripe-integrate 2024-09-17 19:26:13 +02:00
Ahmed Bouhuolia
5e7cff0eb7 Merge pull request #667 from bigcapitalhq/invoice-customize
feat: customize pdf templates
2024-09-17 19:21:26 +02:00
Ahmed Bouhuolia
34e781b4a2 fix: typo in invoice customize drawer 2024-09-17 19:18:22 +02:00
Ahmed Bouhuolia
5f40d50852 fix: pdf template customization 2024-09-17 18:19:28 +02:00
Ahmed Bouhuolia
bb0d91a9cb fix: pdf templates 2024-09-17 17:46:56 +02:00
Ahmed Bouhuolia
2c790427fa feat: rendering pdf templates on the server-side 2024-09-17 13:53:57 +02:00
Ahmed Bouhuolia
4f59b27d70 feat: hook up branding templates to invoices 2024-09-16 20:02:17 +02:00
Ahmed Bouhuolia
94c08f0b9e chore: clean pdf templates code 2024-09-15 22:55:39 +02:00
Ahmed Bouhuolia
ef4beaa564 feat: seed initial standard branding templates 2024-09-15 22:01:11 +02:00
Ahmed Bouhuolia
2b42215381 feat: add loading state to generate payment link dialog 2024-09-15 21:08:41 +02:00
Ahmed Bouhuolia
18d6ec7b59 feat: style the generate payment link dialog 2024-09-15 21:03:36 +02:00
Ahmed Bouhuolia
430cf19533 feat: Link transations with payment methods 2024-09-15 19:42:43 +02:00
Ahmed Bouhuolia
542e61dbfc feat: sharable payment link dialog 2024-09-15 19:28:43 +02:00
Ahmed Bouhuolia
8566422ce3 fix: Add mising address in branding templates customize 2024-09-14 22:52:37 +02:00
Ahmed Bouhuolia
9517b4e279 feat: wip public payment page 2024-09-14 22:10:27 +02:00
Ahmed Bouhuolia
70551bee30 feat: the element customize submit button 2024-09-14 20:18:03 +02:00
Ahmed Bouhuolia
d690c6a3fe feat: optimize branding templates customiing 2024-09-14 19:32:16 +02:00
Ahmed Bouhuolia
28319c2cdc feat: cannot delete a predefined branding template 2024-09-14 16:26:34 +02:00
Ahmed Bouhuolia
df0f73f338 feat: mark specific template as default 2024-09-14 16:19:06 +02:00
Ahmed Bouhuolia
411ac55986 feat: templates customize 2024-09-12 17:49:00 +02:00
Ahmed Bouhuolia
12226d469a feat: pdf template customize 2024-09-12 16:50:44 +02:00
Ahmed Bouhuolia
632c4629de feat: hook up the invice customize api 2024-09-12 14:16:07 +02:00
Ahmed Bouhuolia
a7df23cebc feat: branding templates table 2024-09-11 21:16:21 +02:00
Ahmed Bouhuolia
ef74e250f1 feat: link pdf template to sales transactions 2024-09-11 16:49:44 +02:00
Ahmed Bouhuolia
c0769662bd feat(server): add pdf template crud endpoints 2024-09-11 14:54:13 +02:00
Ahmed Bouhuolia
5b6270a184 feat: invoice pdf customize 2024-09-10 23:32:34 +02:00
Ahmed Bouhuolia
4541d28b68 chore: dump CHANGELOG 2024-09-10 22:25:16 +02:00
Ahmed Bouhuolia
716dec799a feat: paper template customize 2024-09-10 21:54:37 +02:00
Ahmed Bouhuolia
77a1e35ff4 feat: Paper template reusable 2024-09-10 18:37:38 +02:00
Ahmed Bouhuolia
317adfa0de feat: wip estimate, receipt, payment received customize 2024-09-10 17:06:17 +02:00
Ahmed Bouhuolia
f0dfc3d1b0 feat: invoice customize paper preview 2024-09-10 13:29:25 +02:00
Ahmed Bouhuolia
67904f52af feat: add more customize drawers 2024-09-09 21:31:14 +02:00
Ahmed Bouhuolia
f644ed6708 feat: element customize component 2024-09-09 21:07:22 +02:00
Ahmed Bouhuolia
dc18bde6be feat: wip invoice customizer 2024-09-09 19:40:23 +02:00
Ahmed Bouhuolia
132c1dfdbe feat: craft the paper template style 2024-09-09 16:24:09 +02:00
Ahmed Bouhuolia
162b92ce84 feat: wip Stripe connect integration 2024-09-09 14:18:04 +02:00
Ahmed Bouhuolia
9247745ab0 feat: wip invoice customize 2024-09-08 21:01:54 +02:00
Ahmed Bouhuolia
c5c0342c7b feat: wip styling invoice customize 2024-09-08 20:13:27 +02:00
Ahmed Bouhuolia
f5e9485a12 feat: wip invoice customize 2024-09-08 17:34:19 +02:00
Ahmed Bouhuolia
a183666df6 feat: Onboard accounts to Stripe Connect 2024-09-08 11:42:26 +02:00
Ahmed Bouhuolia
e6bad27771 feat: wip invoice customizer 2024-09-07 21:39:05 +02:00
Ahmed Bouhuolia
6d24474162 Merge pull request #663 from bigcapitalhq/fix-uncategorize-bank-transaction
fix: Un-categorize bank transactions
2024-09-07 13:27:11 +02:00
Ahmed Bouhuolia
5962b990c4 fix: Uncategorize bank transactions 2024-09-07 13:26:02 +02:00
Ahmed Bouhuolia
9f21f649f0 Merge pull request #662 from bigcapitalhq/refactor-expense-gl
refactor: The expense G/L writer
2024-09-04 21:10:51 +02:00
Ahmed Bouhuolia
d3d2112b8a refactor: The expense G/L writer 2024-09-04 21:10:13 +02:00
Ahmed Bouhuolia
3795322a65 Merge pull request #659 from bigcapitalhq/format-money-table-columns
feat: Tabular number of all money columns
2024-09-04 18:48:38 +02:00
Ahmed Bouhuolia
fe5cd5a8ea feat: Mark more columns as money columns 2024-09-04 18:46:42 +02:00
Ahmed Bouhuolia
c032a5db16 Merge pull request #661 from bigcapitalhq/fix-payment-made-full-amount
fix: Payment made filling the form full amount field
2024-09-04 17:35:54 +02:00
Ahmed Bouhuolia
3fcb6fefde fix: Payment made filling the form full amount field 2024-09-04 17:34:19 +02:00
Ahmed Bouhuolia
16f5cb713d Merge pull request #660 from bigcapitalhq/fix-cast-array-rule-ids
fix: Array cast of recognize function rule ids
2024-09-04 16:49:32 +02:00
Ahmed Bouhuolia
f7a7925028 fix: Array cast of recognize function rule ids 2024-09-04 16:48:34 +02:00
Ahmed Bouhuolia
66fb0c9fa3 feat: Tabular number of all money columns 2024-09-04 14:58:31 +02:00
Ahmed Bouhuolia
85acc85f17 Merge pull request #655 from bigcapitalhq/ui-tweaks
feat: Datatable UI improvements
2024-09-04 14:02:43 +02:00
Ahmed Bouhuolia
c76ce09191 feat: optimize the style of bank account transactions tables 2024-09-04 13:59:13 +02:00
Ahmed Bouhuolia
e3532098b2 Merge branch 'develop' into ui-tweaks 2024-09-04 09:53:55 +02:00
Ahmed Bouhuolia
b6783eb22e Merge pull request #658 from bigcapitalhq/banking-layout-breaking
feat: Bank pages layout breaking
2024-09-04 09:52:35 +02:00
Ahmed Bouhuolia
4ef00ab122 feat: Bank pages layout breaking 2024-09-04 09:51:35 +02:00
Ahmed Bouhuolia
3dd827417a fix: remove duplicated event types 2024-09-03 17:31:00 +02:00
Ahmed Bouhuolia
cbacd02aa2 Merge pull request #656 from bigcapitalhq/add-help-dropdown-menu
feat: Add help dropdown menu
2024-09-03 17:28:12 +02:00
Ahmed Bouhuolia
3b7e0fb78a Merge pull request #657 from bigcapitalhq/suspense-lazy-banking-pages
fix: Suspense the lazy loaded components in banking pages
2024-09-03 17:26:02 +02:00
Ahmed Bouhuolia
9add716395 fix: Suspense the lazy loaded components in banking pages 2024-09-03 17:24:11 +02:00
Ahmed Bouhuolia
083ea28a1f feat: Add help dropdown menu 2024-09-03 16:41:20 +02:00
Ahmed Bouhuolia
1b51742c36 feat: Datatable UI improvements 2024-09-03 16:39:13 +02:00
Ahmed Bouhuolia
0c6f23e770 Merge pull request #654 from bigcapitalhq/expense-credit-card
fix: Expense cannot accept credit card as payment account
2024-09-03 12:23:03 +02:00
Ahmed Bouhuolia
37a8ca4e97 fix: Expense cannot accept credit card as payment account 2024-09-03 12:22:07 +02:00
Ahmed Bouhuolia
795303c3a8 Merge pull request #653 from bigcapitalhq/tracking-more-events
feat: Tracking more Posthog events
2024-09-03 11:28:27 +02:00
Ahmed Bouhuolia
63ba3f0898 Merge branch 'develop' into tracking-more-events 2024-09-03 11:28:05 +02:00
Ahmed Bouhuolia
62594efa00 feat: Tracking more Posthog events 2024-09-03 11:26:24 +02:00
Ahmed Bouhuolia
9ac0dcbdc3 Merge pull request #651 from bigcapitalhq/bank-transactions-events
feat: Track banking service events
2024-09-02 17:13:49 +02:00
Ahmed Bouhuolia
df588dc4ce Merge branch 'develop' into bank-transactions-events 2024-09-02 17:13:43 +02:00
Ahmed Bouhuolia
d54f14a87a feat: Track banking service events 2024-09-02 17:12:37 +02:00
Ahmed Bouhuolia
a4d4be54c1 Merge pull request #650 from bigcapitalhq/cover-more-tracking-events
feat: Cover more tracking events.
2024-09-02 15:20:11 +02:00
Ahmed Bouhuolia
ddd17e74b5 feat: Cover more tracking events. 2024-09-02 15:19:01 +02:00
Ahmed Bouhuolia
81c0761fbe Merge pull request #649 from bigcapitalhq/import-multi-branches-expenses
fix: Integrate multiple branches with expense resource
2024-09-02 15:02:53 +02:00
Ahmed Bouhuolia
0812e3087e fix: Integrate multiple branches with expense resource 2024-09-02 15:02:02 +02:00
Ahmed Bouhuolia
0dd05493b2 Merge pull request #645 from bigcapitalhq/multi-branches-warehoues-in-importing
feat: integrate multiple branches and warehouses to resource importing
2024-09-02 14:46:08 +02:00
Ahmed Bouhuolia
bfb3909d26 feat: integrate multiple branches and warehouses with import 2024-09-02 14:42:05 +02:00
Ahmed Bouhuolia
266902026e Merge pull request #648 from bigcapitalhq/fix-bank-transactions-infinity-scrolling
fix: Bank transactions infinity scrolling
2024-09-02 10:41:54 +02:00
Ahmed Bouhuolia
791c4a4e9e fix: Bank transactions infinity scrolling 2024-09-02 10:41:16 +02:00
Ahmed Bouhuolia
2cfa9123b8 fix: Syntax error 2024-09-01 23:32:54 +02:00
Ahmed Bouhuolia
bb9614aafb Merge pull request #646 from bigcapitalhq/events-tracking
feat(server): Events tracking using Posthog
2024-09-01 23:06:38 +02:00
Ahmed Bouhuolia
302564a56e fix: remove the un-used file 2024-09-01 23:02:41 +02:00
Ahmed Bouhuolia
dcfc231d4d feat(server): Events tracking using Posthog 2024-09-01 23:01:25 +02:00
Ahmed Bouhuolia
64080ed678 Merge pull request #644 from bigcapitalhq/set-default-index-entries
fix: Set default index to transaction entries
2024-09-01 15:10:40 +02:00
Ahmed Bouhuolia
263935d91e Merge pull request #643 from bigcapitalhq/import-bugs
fix: Import bugs
2024-09-01 15:02:31 +02:00
Ahmed Bouhuolia
7f5ffb8da1 fix: Add the missing columns to the payment received and made models 2024-09-01 15:00:51 +02:00
Ahmed Bouhuolia
f07d25edbe fix: Set default index to transaction entries 2024-09-01 13:32:47 +02:00
Ahmed Bouhuolia
7c07d6b5ff fix: The unimported functions 2024-09-01 11:16:50 +02:00
Ahmed Bouhuolia
e433f4ad68 fix: Item resource columns labels 2024-09-01 11:07:55 +02:00
Ahmed Bouhuolia
a79b9caff6 Merge pull request #641 from bigcapitalhq/fix-getting-sheet-columns
fix: Getting the sheet columns in import sheet
2024-08-30 17:56:53 +02:00
Ahmed Bouhuolia
2227cead66 Merge pull request #624 from bigcapitalhq/subscription-middleware
fix: Subscription middleware
2024-08-30 17:56:02 +02:00
Ahmed Bouhuolia
410c4ea3e2 fix: Subscription active detarminer 2024-08-30 17:52:53 +02:00
Ahmed Bouhuolia
f92acbcbe0 fix: Getting the sheet columns in import sheet 2024-08-30 17:03:16 +02:00
Ahmed Bouhuolia
c986585cd9 Merge pull request #640 from bigcapitalhq/fix-typo-one-click-demo
fix: Typo one-click demo page
2024-08-30 00:15:29 +02:00
Ahmed Bouhuolia
250f0a30ef fix: Typo one-click demo page 2024-08-30 00:14:59 +02:00
Ahmed Bouhuolia
84b5e1adc1 Merge pull request #639 from bigcapitalhq/add-customer-type-to-customers
fix: Add customer type to customers resource
2024-08-29 22:50:20 +02:00
Ahmed Bouhuolia
2ab28370db fix: Add customer type to customers resource 2024-08-29 22:49:49 +02:00
Ahmed Bouhuolia
a88a525326 Merge pull request #638 from bigcapitalhq/use-standard-date-format-export
fix: use standard ISO 8601 format for exported data
2024-08-29 22:40:59 +02:00
Ahmed Bouhuolia
fce8e2c5a4 fix: use standard ISO 8601 format for exported data 2024-08-29 22:39:51 +02:00
Ahmed Bouhuolia
095608266c Merge pull request #632 from bigcapitalhq/split-lazy-loading
feat: Optimize loading perf. by splitting big chunks and lazy loading them
2024-08-29 21:27:40 +02:00
Ahmed Bouhuolia
0ec8aaa330 fix: delete unwanted files 2024-08-29 21:27:20 +02:00
Ahmed Bouhuolia
9fcb3ef77d feat: Add fallback spinner to authentication lazy-loaded pages 2024-08-29 21:19:36 +02:00
Ahmed Bouhuolia
dc61c57daf fix: Add spinner to preferences lazy loaded pages 2024-08-29 21:03:27 +02:00
Ahmed Bouhuolia
af284f3f6d feat: split the preferences pages 2024-08-29 20:49:08 +02:00
Ahmed Bouhuolia
c9fe6d9b37 feat: Optimize loading perf. by spliting big chunks and lazy loading them 2024-08-26 22:51:40 +02:00
Ahmed Bouhuolia
ee2d8d3065 fix: subscription middleare 2024-08-25 12:42:42 +02:00
497 changed files with 19548 additions and 1472 deletions

View File

@@ -89,3 +89,14 @@ S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=
S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=
# Stripe Payment
STRIPE_PAYMENT_SECRET_KEY=
STRIPE_PAYMENT_PUBLISHABLE_KEY=
STRIPE_PAYMENT_CLIENT_ID=
STRIPE_PAYMENT_WEBHOOKS_SECRET=
STRIPE_PAYMENT_REDIRECT_URL=

View File

@@ -2,7 +2,77 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.19.4] - 18-08-2024
# [0.19.17]
* fix: Un-categorize bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/663
# [0.19.16]
* feat: Tracking more Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/653
* fix: Expense cannot accept credit card as payment account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/654
* fix: Suspense the lazy loaded components in banking pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/657
* feat: Add help dropdown menu by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/656
* feat: Bank pages layout breaking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/658
* feat: Datatable UI improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/655
* fix: Array cast of recognize function rule ids by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/660
* fix: Payment made filling the form full amount field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/661
* feat: Tabular number of all money columns by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/659
* refactor: The expense G/L writer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/662
## [0.19.15] -
* fix: Bank transactions infinity scrolling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/648
* feat: Integrate multiple branches and warehouses to resource importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/645
* fix: Integrate multiple branches with expense resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/649
* feat: Cover more tracking events. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/650
* feat: Track banking service events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/651
## [0.19.14]
* fix: Import bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/643
* fix: Set default index to transaction entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/644
* feat(server): Events tracking using Posthog by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/646
## [0.19.13]
* fix: Subscription middleware by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/624
* fix: Getting the sheet columns in import sheet by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/641
## [0.19.12]
* fix: Typo one-click demo page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/640
## [0.19.11]
* fix: Avoid running the cost job in import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/635
* fix: Debounce scheduling calculating items cost by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/634
* fix: Expand the resources export page size limitation by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/636
* feat: Optimize loading perf. by splitting big chunks and lazy loading them by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/632
* fix: Use standard ISO 8601 format for exported data by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/638
* fix: Add customer type to customers resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/639
## [0.19.10]
* fix: Add subscription plans offer text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/629
## [0.19.9]
* fix: Make webapp package env variables dynamic by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/628
## [v0.19.8]
* fix: Cannot import items income and cost accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/617
* fix: Some bank account details hidden by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/618
* feat(ee): One-click demo account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/616
* feat: change banking service language by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/619
* feat(banking): Filter uncategorized bank transactions by date by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/590
* Fix: Syntax error caused error by @wolone in https://github.com/bigcapitalhq/bigcapital/pull/622
* fix: Listen to payment webhooks by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/623
* fix: Add prefix J-00001 to manual journals increments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/625
* fix: Disable sms service until Twilo integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/626
* fix: Style tweaks in onboarding page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/627
## [0.19.5] - 18-08-2024
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
@@ -23,6 +93,15 @@ All notable changes to Bigcapital server-side will be in this file.
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
## [v0.19.4]
* feat: Import and export tax rates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/591
* feat: Un-categorize bank transactions in bulk by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/587
* feat: Pending bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/589
* fix: Update `dev` Script in `package.json` to Use `cross-env` by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/588
* fix: Should not load branches on reconcile matching form if the branches not enabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/592
* fix: Rounding the total amount the pending and matched transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/593
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511

View File

@@ -99,6 +99,7 @@
"objection-unique": "^1.2.2",
"plaid": "^10.3.0",
"pluralize": "^8.0.0",
"posthog-node": "^4.2.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
"qim": "0.0.52",
@@ -108,11 +109,13 @@
"rtl-detect": "^1.0.4",
"socket.io": "^4.7.4",
"source-map-loader": "^4.0.1",
"stripe": "^16.10.0",
"tmp-promise": "^3.0.3",
"ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0",
"typedi": "^0.8.0",
"uniqid": "^5.2.0",
"uuid": "^10.0.0",
"winston": "^3.2.1",
"xlsx": "^0.18.5",
"yup": "^0.28.1"

View File

@@ -0,0 +1,40 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
*,
*::before,
*::after {
box-sizing: border-box;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
body{
margin: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #000;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body, h1, h2, h3, h4, h5, h6{
font-family: "Noto Sans", sans-serif;
font-optical-sizing: auto;
font-style: normal;
}

View File

@@ -1,35 +1 @@
@import "./normalize.scss";
*,
*::before,
*::after {
box-sizing: border-box;
}
th {
text-align: inherit; // 2
text-align: -webkit-match-parent; // 3
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
body{
margin: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
direction: ltr;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}

View File

@@ -0,0 +1,379 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@@ -1,6 +1,9 @@
html(lang=locale)
head
title My Site - #{title}
style
include ../scss/normalize.css
include ../scss/base.css
block head
body
div.paper-template

View File

@@ -1,81 +1,196 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/credit-rtl.css
else
include ../../css/modules/credit.css
- var prefix = 'bc'
style.
.#{prefix}-root {
color: #111;
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-big-title {
font-size: 60px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
}
.#{prefix}-terms-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label {
min-width: 120px;
color: #333;
}
.#{prefix}-terms-item__value {
/* Styles for the term value */
}
.#{prefix}-address-section{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: flex-start;
align-items: flex-start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-address-section > * {
flex: 1 1;
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--border-gray {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--border-dark {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--font-weight-bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-statement {
margin-bottom: 20px;
}
.#{prefix}-statement__label {
color: #666;
}
.#{prefix}-statement__value {
/* Styles for statement value */
}
block content
div.credit
div.credit__header
div.paper
h1.title #{__('credit.paper.credit_note')}
if creditNote.creditNoteNumber
span.creditNoteNumber #{creditNote.creditNoteNumber}
div(class=`${prefix}-root`)
div(class=`${prefix}-big-title`) Credit Note
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
div(class=`${prefix}-terms-list`)
if showCreditNoteNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
div.credit__full-amount
div.label #{__('credit.paper.amount')}
div.amount #{creditNote.formattedAmount}
if showCreditNoteDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
div.credit__meta
div.credit__meta-item.credit__meta-item--amount
span.label #{__('credit.paper.remaining')}
span.value #{creditNote.formattedCreditsRemaining}
div(class=`${prefix}-address-section`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
div.credit__meta-item.credit__meta-item--billed-to
span.label #{__("credit.paper.billed_to")}
span.value #{creditNote.customer.displayName}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
div.credit__meta-item.credit__meta-item--credit-date
span.label #{__("credit.paper.credit_date")}
span.value #{creditNote.formattedCreditNoteDate}
div.credit__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) #{'Item'}
th(class=`${prefix}-table__header`) #{'Description'}
th(class=`${prefix}-table__header`) #{'Rate'}
th(class=`${prefix}-table__header`) #{'Total'}
tbody
each entry in creditNote.entries
tr
td.item
div.title=entry.item.name
span.description=entry.description
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
each line in lines
tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`) #{line.item}
td(class=`${prefix}-table__cell`) #{line.description}
td(class=`${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell--right`) #{line.total}
div.credit__table-after
div.credit__table-total
table
tbody
tr.total
td #{__('credit.paper.total')}
td #{creditNote.formattedAmount}
tr.payment-amount
td #{__('credit.paper.credits_used')}
td #{creditNote.formattedCreditsUsed}
tr.blanace-due
td #{__('credit.paper.credits_remaining')}
td #{creditNote.formattedCreditsRemaining}
div(class=`${prefix}-totals`)
if showSubtotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
div(class=`${prefix}-totals__item-label`) #{subtotallabel}
div(class=`${prefix}-totals__item-amount`) #{subtotal}
div.credit__footer
if creditNote.termsConditions
div.credit__conditions
h3 #{__("credit.paper.terms_conditions")}
p #{creditNote.termsConditions}
if showTotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark`)
div(class=`${prefix}-totals__item-amount`) #{totalLabel}:
div(class=`${prefix}-totals__item-label`) #{total}
if creditNote.note
div.credit__notes
h3 #{__("credit.paper.notes")}
p #{creditNote.note}
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel}:
div(class=`${prefix}-statement__value`) #{customerNote}
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}:
div(class=`${prefix}-statement__value`) #{termsConditions}

View File

@@ -1,82 +1,202 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/estimate-rtl.css
else
include ../../css/modules/estimate.css
block head
- var prefix = 'bc'
style.
.#{prefix}-root {
color: #111;
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-big-title {
font-size: 60px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
}
.#{prefix}-terms {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label {
min-width: 120px;
color: #333;
}
.#{prefix}-terms-item__value {
}
.#{prefix}-addresses{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-addresses > * {
flex: 1 1;
}
.#{prefix}-address {
}
.#{prefix}-address__item {
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell--right{
text-align: right;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--border-gray {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--border-dark {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--font-weight-bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-statement {
margin-bottom: 20px;
}
.#{prefix}-statement__label {
color: #666;
}
.#{prefix}-statement__value {
}
block content
div.estimate
div.estimate__header
div.paper
h1.title #{__("estimate.paper.estimate")}
span.email #{saleEstimate.estimateNumber}
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
h1(class=`${prefix}-big-title`) Estimate
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
div.estimate__estimate-amount
div.label #{__('estimate.paper.estimate_amount')}
div.amount #{saleEstimate.formattedAmount}
//- Terms List
div(class=`${prefix}-terms`)
if showEstimateNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
if showEstimateDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateDateLabel}
div(class=`${prefix}-terms-item__value`) #{estimateDate}
if showExpirationDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel}
div(class=`${prefix}-terms-item__value`) #{expirationDate}
div.estimate__meta
if saleEstimate.estimateNumber
div.estimate__meta-item.estimate__meta-item--estimate-number
span.label #{__("estimate.paper.estimate_number")}
span.value #{saleEstimate.estimateNumber}
//- Addresses (Group section)
div(class=`${prefix}-addresses`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
div.estimate__meta-item.estimate__meta-item--billed-to
span.label #{__("estimate.paper.billed_to")}
span.value #{saleEstimate.customer.displayName}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
div.estimate__meta-item.estimate__meta-item--estimate-date
span.label #{__("estimate.paper.estimate_date")}
span.value #{saleEstimate.formattedEstimateDate}
div.estimate__meta-item.estimate__meta-item--due-date
span.label #{__("estimate.paper.expiration_date")}
span.value #{saleEstimate.formattedExpirationDate}
div.estimate__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
//- Table section (Line items)
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) Item
th(class=`${prefix}-table__header`) Description
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
tbody
each entry in saleEstimate.entries
tr
td.item
div.title=entry.item.name
span.description=entry.description
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
each line in lines
tr
td(class=`${prefix}-table__cell`) #{line.item}
td(class=`${prefix}-table__cell`) #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
div.estimate__table-after
div.estimate__table-total
table
tbody
tr.subtotal
td #{__('estimate.paper.subtotal')}
td #{saleEstimate.formattedAmount}
tr.total
td #{__('estimate.paper.total')}
td #{saleEstimate.formattedAmount}
//- Totals section
div(class=`${prefix}-totals`)
if showSubtotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
div(class=`${prefix}-totals__item-amount`) #{subtotal}
if showTotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
div(class=`${prefix}-totals__item-label`) #{totalLabel}
div(class=`${prefix}-totals__item-amount`) #{total}
div.estimate__footer
if saleEstimate.termsConditions
div.estimate__conditions
h3 #{__("estimate.paper.conditions_title")}
p #{saleEstimate.termsConditions}
//- Statements section
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
div(class=`${prefix}-statement__value`) #{customerNote}
if saleEstimate.note
div.estimate__notes
h3 #{__("estimate.paper.notes_title")}
p #{saleEstimate.note}
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}

View File

@@ -1,92 +0,0 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/invoice-rtl.css
else
include ../../css/modules/invoice.css
block content
div.invoice
div.invoice__header
div.paper
h1.title #{__("invoice.paper.invoice")}
if saleInvoice.invoiceNo
span.invoiceNo #{saleInvoice.invoiceNo}
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.invoice__due-amount
div.label #{__('invoice.paper.invoice_amount')}
div.amount #{saleInvoice.totalFormatted}
div.invoice__meta
div.invoice__meta-item.invoice__meta-item--amount
span.label #{__('invoice.paper.due_amount')}
span.value #{saleInvoice.dueAmountFormatted}
div.invoice__meta-item.invoice__meta-item--billed-to
span.label #{__("invoice.paper.billed_to")}
span.value #{saleInvoice.customer.displayName}
div.invoice__meta-item.invoice__meta-item--invoice-date
span.label #{__("invoice.paper.invoice_date")}
span.value #{saleInvoice.invoiceDateFormatted}
div.invoice__meta-item.invoice__meta-item--due-date
span.label #{__("invoice.paper.due_date")}
span.value #{saleInvoice.dueDateFormatted}
div.invoice__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in saleInvoice.entries
tr
td.item
div.title=entry.item.name
span.description=entry.description
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.invoice__table-after
div.invoice__table-total
table
tbody
tr.subtotal
td #{__('invoice.paper.subtotal')}
td #{saleInvoice.subtotalFormatted}
each tax in saleInvoice.taxes
tr.tax_line
td #{tax.name} [#{tax.taxRate}%]
td #{tax.taxRateAmountFormatted}
tr.total
td #{__('invoice.paper.total')}
td #{saleInvoice.totalFormatted}
tr.payment-amount
td #{__('invoice.paper.payment_amount')}
td #{saleInvoice.paymentAmountFormatted}
tr.blanace-due
td #{__('invoice.paper.balance_due')}
td #{saleInvoice.dueAmountFormatted}
div.invoice__footer
if saleInvoice.termsConditions
div.invoice__conditions
h3 #{__("invoice.paper.conditions_title")}
p #{saleInvoice.termsConditions}
if saleInvoice.invoiceMessage
div.invoice__notes
h3 #{__("invoice.paper.notes_title")}
p #{saleInvoice.invoiceMessage}

View File

@@ -0,0 +1,237 @@
extends ../PaperTemplateLayout.pug
block head
- var prefix = 'bc'
style.
.#{prefix}-root {
color: #111;
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-big-title {
font-size: 60px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
}
.#{prefix}-details {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-detail {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-detail__label {
min-width: 120px;
color: #333;
}
.#{prefix}-detail__value {
/* Styles for detail values */
}
.#{prefix}-address-root {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-address-from {
flex: 1;
}
.#{prefix}-address-from__item {
/* Styles for items in the billed-from address */
}
.#{prefix}-address-to {
flex: 1;
}
.#{prefix}-address-to__item {
/* Styles for items in the billed-to address */
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--border-gray {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--border-dark {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--font-weight-bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-paragraph {
margin-bottom: 20px;
}
.#{prefix}-paragraph__label {
color: #666;
}
.#{prefix}-paragraph__value {
/* Styles for values within the paragraph section */
}
block content
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Invoice details
div(class=`${prefix}-details`)
if showInvoiceNumber
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
div(class=`${prefix}-detail__value`) #{invoiceNumber}
if showDateIssue
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
div(class=`${prefix}-detail__value`) #{dateIssue}
if showDueDate
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dueDateLabel}
div(class=`${prefix}-detail__value`) #{dueDate}
//- Address section
div(class=`${prefix}-address-root`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
//- Invoice table
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) #{lineItemLabel}
th(class=`${prefix}-table__header`) #{lineDescriptionLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel}
tbody
each line in lines
tr
td(class=`${prefix}-table__cell`) #{line.item}
td(class=`${prefix}-table__cell`) #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
//- Totals section
div(class=`${prefix}-totals`)
if showSubtotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
div(class=`${prefix}-totals__item-amount`) #{subtotal}
if showDiscount
div(class=`${prefix}-totals__item`)
div(class=`${prefix}-totals__item-label`) #{discountLabel}
div(class=`${prefix}-totals__item-amount`) #{discount}
if showTaxes
each tax in taxes
div(class=`${prefix}-totals__item`)
div(class=`${prefix}-totals__item-label`) #{tax.label}
div(class=`${prefix}-totals__item-amount`) #{tax.amount}
if showTotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
div(class=`${prefix}-totals__item-label`) #{totalLabel}
div(class=`${prefix}-totals__item-amount`) #{total}
if showPaymentMade
div(class=`${prefix}-totals__item`)
div(class=`${prefix}-totals__item-label`) #{paymentMadeLabel}
div(class=`${prefix}-totals__item-amount`) #{paymentMade}
if showBalanceDue
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
div(class=`${prefix}-totals__item-label`) #{balanceDueLabel}
div(class=`${prefix}-totals__item-amount`) #{balanceDue}
//- Footer section
if showTermsConditions && termsConditions
div(class=`${prefix}-paragraph`)
if termsConditionsLabel
div(class=`${prefix}-paragraph__label`) #{termsConditionsLabel}
div(class=`${prefix}-paragraph__value`) #{termsConditions}
if showStatement && statement
div(class=`${prefix}-paragraph`)
if statementLabel
div(class=`${prefix}-paragraph__label`) #{statementLabel}
div(class=`${prefix}-paragraph__value`) #{statement}

View File

@@ -1,67 +1,173 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/payment-rtl.css
else
include ../../css/modules/payment.css
- var prefix = 'bp3';
style.
.#{prefix}-root{
color: #111;
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-big-title{
font-size: 60px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap{
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
}
.#{prefix}-terms-list{
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item{
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label{
min-width: 120px;
color: #333;
}
.#{prefix}-addresses{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-addresses > * {
flex: 1 1;
}
.#{prefix}-address__label{
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--gray-border {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--dark-border {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
block content
div.payment
div.payment__header
div.paper
h1.title #{__("payment.paper.payment_receipt")}
if paymentReceive.paymentReceiveNo
span.paymentNumber #{paymentReceive.paymentReceiveNo}
div(class=`${prefix}-root`)
div(class=`${prefix}-big-title`) Payment
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt="Company Logo")
div(class=`${prefix}-terms-list`)
if showPaymentReceivedNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
div.payment__received-amount
div.label #{__('payment.paper.amount_received')}
div.amount #{paymentReceive.formattedAmount}
if showPaymentReceivedDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
div(class=`${prefix}-addresses`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
div.payment__meta
div.payment__meta-item.payment__meta-item--billed-to
span.label #{__("payment.paper.billed_to")}
span.value #{paymentReceive.customer.displayName}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
div.payment__meta-item.payment__meta-item--payment-date
span.label #{__("payment.paper.payment_date")}
span.value #{paymentReceive.formattedPaymentDate}
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) Invoice #
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Invoice Amount
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Paid Amount
div.payment__table
table
thead
tr
th.item #{__("payment.paper.invoice_number")}
th.date #{__("payment.paper.invoice_date")}
th.invoiceAmount #{__("payment.paper.invoice_amount")}
th.paymentAmount #{__("payment.paper.payment_amount")}
tbody
each entry in paymentReceive.entries
tr
td.item=entry.invoice.invoiceNo
td.date=entry.invoice.invoiceDateFormatted
td.invoiceAmount=entry.invoice.totalFormatted
td.paymentAmount=entry.invoice.paymentAmountFormatted
tbody
each line in lines
tr
td(class=`${prefix}-table__cell`) #{line.invoiceNumber}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.invoiceAmount}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.paidAmount}
div.payment__table-after
div.payment__table-total
table
tbody
tr.payment-amount
td #{__('payment.paper.payment_amount')}
td #{paymentReceive.formattedAmount}
tr.blanace-due
td #{__('payment.paper.balance_due')}
td #{paymentReceive.customer.closingBalance}
div(class=`${prefix}-totals`)
if showSubtotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--gray-border`)
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
div(class=`${prefix}-totals__item-amount`) #{subtotal}
div.payment__footer
if paymentReceive.statement
div.payment__notes
h3 #{__("payment.paper.statement")}
p #{paymentReceive.statement}
if showTotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--dark-border`)
div(class=`${prefix}-totals__item-label`) #{totalLabel}
div(class=`${prefix}-totals__item-amount`) #{total}

View File

@@ -1,77 +1,196 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/receipt-rtl.css
else
include ../../css/modules/receipt.css
- var prefix = 'bc'
style.
.#{prefix}-root {
color: #000;
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
}
.#{prefix}-big-title {
font-size: 60px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-terms-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label {
min-width: 120px;
color: #333;
}
.#{prefix}-terms-item__value {}
.#{prefix}-address-section {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: flex-start;
align-items: flex-start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-address-section > * {
flex: 1 1 auto;
}
.#{prefix}-address {}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__line {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__line--gray-border {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__line--dark-border {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__line__label {
min-width: 160px;
}
.#{prefix}-totals__line__amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-statement {
margin-bottom: 20px;
}
.#{prefix}-statement__label {}
.#{prefix}-statement__value {}
block content
div.receipt
div.receipt__header
div.paper
h1.title #{__("receipt.paper.receipt")}
span.receiptNumber #{saleReceipt.receiptNumber}
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
div.organization
h3.title #{organizationName}
//- Company Logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
div.receipt__receipt-amount
div.label #{__('receipt.paper.receipt_amount')}
div.amount #{saleReceipt.formattedAmount}
//- Terms List
div(class=`${prefix}-terms-list`)
if showReceiptNumber
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
span(class=`${prefix}-terms-item__value`)= receiptNumber
if showReceiptDate
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptDateLabel
span(class=`${prefix}-terms-item__value`)= receiptDate
div.receipt__meta
div.receipt__meta-item.receipt__meta-item--billed-to
span.label #{__("receipt.paper.billed_to")}
span.value #{saleReceipt.customer.displayName}
//- Address Section
div(class=`${prefix}-address-section`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
div.receipt__meta-item.receipt__meta-item--invoice-date
span.label #{__("receipt.paper.receipt_date")}
span.value #{saleReceipt.formattedReceiptDate}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
if saleReceipt.receiptNumber
div.receipt__meta-item.receipt__meta-item--invoice-number
span.label #{__("receipt.paper.receipt_number")}
span.value #{saleReceipt.receiptNumber}
//- Table Section
table(class=`${prefix}-table`)
thead(class=`${prefix}-table__header`)
tr
th(class=`${prefix}-table__header`) Item
th(class=`${prefix}-table__header`) Description
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
tbody
each line in lines
tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`)= line.item
td(class=`${prefix}-table__cell`)= line.description
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total
div.receipt__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in saleReceipt.entries
tr
td.item=entry.item.name
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.receipt__table-after
div.receipt__table-total
table
tbody
tr.total
td #{__('receipt.paper.total')}
td #{saleReceipt.formattedAmount}
tr.payment-amount
td #{__('receipt.paper.payment_amount')}
td #{saleReceipt.formattedAmount}
tr.blanace-due
td #{__('receipt.paper.balance_due')}
td #{'$0'}
//- Totals Section
div(class=`${prefix}-totals`)
if showSubtotal
div(class=`${prefix}-totals__line ${prefix}-totals__line--gray-border`)
span(class=`${prefix}-totals__line__label`)= subtotalLabel
span(class=`${prefix}-totals__line__amount`)= subtotal
div.receipt__footer
if saleReceipt.statement
div.receipt__conditions
h3 #{__("receipt.paper.statement")}
p #{saleReceipt.statement}
if showTotal
div(class=`${prefix}-totals__line ${prefix}-totals__line--dark-border`)
span(class=`${prefix}-totals__line__label`)= totalLabel
span(class=`${prefix}-totals__line__amount`)= total
if saleReceipt.receiptMessage
div.receipt__notes
h3 #{__("receipt.paper.notes")}
p #{saleReceipt.receiptMessage}
//- Customer Note Section
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`)= customerNoteLabel
div(class=`${prefix}-statement__value`)= customerNote
//- Terms & Conditions Section
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`)= termsConditionsLabel
div(class=`${prefix}-statement__value`)= termsConditions

View File

@@ -18,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
@Service()
export default class OrganizationController extends BaseController {
@Inject()
organizationService: OrganizationService;
private organizationService: OrganizationService;
/**
* Router constructor.
@@ -56,10 +56,10 @@ export default class OrganizationController extends BaseController {
}
/**
* Organization setup schema.
* @return {ValidationChain[]}
* Build organization validation schema.
* @returns {ValidationChain[]}
*/
private get commonOrganizationValidationSchema(): ValidationChain[] {
private get buildOrganizationValidationSchema(): ValidationChain[] {
return [
check('name').exists().trim(),
check('industry').optional({ nullable: true }).isString().trim(),
@@ -72,21 +72,34 @@ export default class OrganizationController extends BaseController {
];
}
/**
* Build organization validation schema.
* @returns {ValidationChain[]}
*/
private get buildOrganizationValidationSchema(): ValidationChain[] {
return [...this.commonOrganizationValidationSchema];
}
/**
* Update organization validation schema.
* @returns {ValidationChain[]}
*/
private get updateOrganizationValidationSchema(): ValidationChain[] {
return [
...this.commonOrganizationValidationSchema,
// # Profile
check('name').optional().trim(),
check('industry').optional({ nullable: true }).isString().trim(),
check('location').optional().isString().isISO31661Alpha2(),
check('base_currency').optional().isISO4217(),
check('timezone').optional().isIn(moment.tz.names()),
check('fiscal_year').optional().isIn(MONTHS),
check('language').optional().isString().isIn(ACCEPTED_LOCALES),
check('date_format').optional().isIn(DATE_FORMATS),
// # Address
check('address.address_1').optional().isString().trim(),
check('address.address_2').optional().isString().trim(),
check('address.postal_code').optional().isString().trim(),
check('address.city').optional().isString().trim(),
check('address.state_province').optional().isString().trim(),
check('address.phone').optional().isString().trim(),
// # Branding
check('primary_color').optional({ nullable: true }).isHexColor().trim(),
check('logo_key').optional({ nullable: true }).isString().trim(),
check('tax_number').optional({ nullable: true }).isString().trim(),
];
}
@@ -156,7 +169,7 @@ export default class OrganizationController extends BaseController {
next: NextFunction
) {
const { tenantId } = req;
const tenantDTO = this.matchedBodyData(req);
const tenantDTO = this.matchedBodyData(req, { includeOptionals: false });
try {
await this.organizationService.updateOrganization(tenantId, tenantDTO);

View File

@@ -0,0 +1,179 @@
import { Service, Inject } from 'typedi';
import { Request, Response, Router, NextFunction } from 'express';
import { body, param } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { PaymentServicesApplication } from '@/services/PaymentServices/PaymentServicesApplication';
@Service()
export class PaymentServicesController extends BaseController {
@Inject()
private paymentServicesApp: PaymentServicesApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
asyncMiddleware(this.getPaymentServicesSpecificInvoice.bind(this))
);
router.get('/state', this.getPaymentMethodsState.bind(this));
router.get('/:paymentServiceId', this.getPaymentService.bind(this));
router.post(
'/:paymentMethodId',
[
param('paymentMethodId').exists(),
body('name').optional().isString(),
body('options.bank_account_id').optional().isNumeric(),
body('options.clearing_account_id').optional().isNumeric(),
],
this.validationResult,
asyncMiddleware(this.updatePaymentMethod.bind(this))
);
router.delete(
'/:paymentMethodId',
[param('paymentMethodId').exists()],
this.validationResult,
this.deletePaymentMethod.bind(this)
);
return router;
}
/**
* Retrieve accounts types list.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Promise<Response | void>}
*/
private async getPaymentServicesSpecificInvoice(
req: Request<{ invoiceId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const paymentServices =
await this.paymentServicesApp.getPaymentServicesForInvoice(tenantId);
return res.status(200).send({ paymentServices });
} catch (error) {
next(error);
}
}
/**
* Retrieves a specific payment service.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next - Next function.
* @return {Promise<Response | void>}
*/
private async getPaymentService(
req: Request<{ paymentServiceId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { paymentServiceId } = req.params;
try {
const paymentService = await this.paymentServicesApp.getPaymentService(
tenantId,
paymentServiceId
);
return res.status(200).send({ data: paymentService });
} catch (error) {
next(error);
}
}
/**
* Edits the given payment method settings.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Promise<Response | void>}
*/
private async updatePaymentMethod(
req: Request<{ paymentMethodId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { paymentMethodId } = req.params;
const updatePaymentMethodDTO = this.matchedBodyData(req);
try {
await this.paymentServicesApp.editPaymentMethod(
tenantId,
paymentMethodId,
updatePaymentMethodDTO
);
return res.status(200).send({
id: paymentMethodId,
message: 'The given payment method has been updated.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieves the payment state providing state.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next - Next function.
* @return {Promise<Response | void>}
*/
private async getPaymentMethodsState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const paymentMethodsState =
await this.paymentServicesApp.getPaymentMethodsState(tenantId);
return res.status(200).send({ data: paymentMethodsState });
} catch (error) {
next(error);
}
}
/**
* Deletes the given payment method.
* @param {Request<{ paymentMethodId: number }>} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next - Next function.
* @return {Promise<Response | void>}
*/
private async deletePaymentMethod(
req: Request<{ paymentMethodId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { paymentMethodId } = req.params;
try {
await this.paymentServicesApp.deletePaymentMethod(
tenantId,
paymentMethodId
);
return res.status(204).send({
id: paymentMethodId,
message: 'The payment method has been deleted.',
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,195 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import BaseController from '@/api/controllers/BaseController';
import { PdfTemplateApplication } from '@/services/PdfTemplate/PdfTemplateApplication';
@Service()
export class PdfTemplatesController extends BaseController {
@Inject()
public pdfTemplateApplication: PdfTemplateApplication;
/**
* Router constructor method.
*/
public router() {
const router = Router();
router.delete(
'/:template_id',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.deletePdfTemplate.bind(this)
);
router.post(
'/:template_id',
[
param('template_id').exists().isInt().toInt(),
check('template_name').exists(),
check('attributes').exists(),
],
this.validationResult,
this.editPdfTemplate.bind(this)
);
router.get('/state', this.getOrganizationBrandingState.bind(this));
router.get(
'/',
[query('resource').optional()],
this.validationResult,
this.getPdfTemplates.bind(this)
);
router.get(
'/:template_id',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.getPdfTemplate.bind(this)
);
router.post(
'/',
[
check('template_name').exists(),
check('resource').exists(),
check('attributes').exists(),
],
this.validationResult,
this.createPdfInvoiceTemplate.bind(this)
);
router.post(
'/:template_id/assign_default',
[param('template_id').exists().isInt().toInt()],
this.validationResult,
this.assginPdfTemplateAsDefault.bind(this)
);
return router;
}
async createPdfInvoiceTemplate(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { templateName, resource, attributes } = this.matchedBodyData(req);
try {
const result = await this.pdfTemplateApplication.createPdfTemplate(
tenantId,
templateName,
resource,
attributes
);
return res.status(201).send({
id: result.id,
message: 'The PDF template has been created successfully.',
});
} catch (error) {
next(error);
}
}
async editPdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
const editTemplateDTO = this.matchedBodyData(req);
try {
const result = await this.pdfTemplateApplication.editPdfTemplate(
tenantId,
Number(templateId),
editTemplateDTO
);
return res.status(200).send({
id: result.id,
message: 'The PDF template has been updated successfully.',
});
} catch (error) {
next(error);
}
}
async deletePdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
await this.pdfTemplateApplication.deletePdfTemplate(
tenantId,
Number(templateId)
);
return res.status(204).send({
id: templateId,
message: 'The PDF template has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
async getPdfTemplate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
const template = await this.pdfTemplateApplication.getPdfTemplate(
tenantId,
Number(templateId)
);
return res.status(200).send(template);
} catch (error) {
next(error);
}
}
async getPdfTemplates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const templates = await this.pdfTemplateApplication.getPdfTemplates(
tenantId,
query
);
return res.status(200).send(templates);
} catch (error) {
next(error);
}
}
async assginPdfTemplateAsDefault(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { template_id: templateId } = req.params;
try {
await this.pdfTemplateApplication.assignPdfTemplateAsDefault(
tenantId,
Number(templateId)
);
return res.status(204).send({
id: templateId,
message: 'The given pdf template has been assigned as default template',
});
} catch (error) {
next(error);
}
}
async getOrganizationBrandingState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data =
await this.pdfTemplateApplication.getPdfTemplateBrandingState(tenantId);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
}

View File

@@ -236,6 +236,9 @@ export default class PaymentReceivesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -167,6 +167,9 @@ export default class PaymentReceivesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -168,9 +168,7 @@ export default class SalesEstimatesController extends BaseController {
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.discount')
.optional({ nullable: true })
.isNumeric()
@@ -186,6 +184,9 @@ export default class SalesEstimatesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -224,9 +224,7 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
@@ -257,6 +255,14 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// Payment methods.
check('payment_methods').optional({ nullable: true }).isArray(),
check('payment_methods.*.payment_integration_id').exists().toInt(),
check('payment_methods.*.enable').exists().isBoolean(),
];
}

View File

@@ -148,17 +148,20 @@ export default class SalesReceiptsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('receipt_message').optional().trim(),
check('statement').optional().trim(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
];
}

View File

@@ -0,0 +1,83 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { PaymentLinksApplication } from '@/services/PaymentLinks/PaymentLinksApplication';
@Service()
export class PublicSharableLinkController extends BaseController {
@Inject()
private paymentLinkApp: PaymentLinksApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/:paymentLinkId/invoice',
[param('paymentLinkId').exists()],
this.validationResult,
this.getPaymentLinkPublicMeta.bind(this),
this.validationResult
);
router.post(
'/:paymentLinkId/stripe_checkout_session',
[param('paymentLinkId').exists()],
this.validationResult,
this.createInvoicePaymentLinkCheckoutSession.bind(this)
);
return router;
}
/**
* Retrieves the payment link public meta.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
public async getPaymentLinkPublicMeta(
req: Request<{ paymentLinkId: string }>,
res: Response,
next: NextFunction
) {
const { paymentLinkId } = req.params;
try {
const data = await this.paymentLinkApp.getInvoicePaymentLink(
paymentLinkId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Creates a Stripe checkout session for the given payment link id.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
public async createInvoicePaymentLinkCheckoutSession(
req: Request<{ paymentLinkId: string }>,
res: Response,
next: NextFunction
) {
const { paymentLinkId } = req.params;
try {
const session =
await this.paymentLinkApp.createInvoicePaymentCheckoutSession(
paymentLinkId
);
return res.status(200).send(session);
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { body } from 'express-validator';
import { AbilitySubject, PaymentReceiveAction } from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { GenerateShareLink } from '@/services/Sales/Invoices/GenerateeInvoicePaymentLink';
@Service()
export class ShareLinkController extends BaseController {
@Inject()
private generateShareLinkService: GenerateShareLink;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post(
'/payment-links/generate',
CheckPolicies(PaymentReceiveAction.Edit, AbilitySubject.PaymentReceive),
[
body('transaction_type').exists(),
body('transaction_id').exists().isNumeric().toInt(),
body('publicity').optional(),
body('expiry_date').optional({ nullable: true }),
],
this.validationResult,
asyncMiddleware(this.generateShareLink.bind(this))
);
return router;
}
/**
* Generates sharable link for the given transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async generateShareLink(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { transactionType, transactionId, publicity, expiryDate } =
this.matchedBodyData(req);
try {
const link = await this.generateShareLinkService.generatePaymentLink(
tenantId,
transactionId,
transactionType,
publicity,
expiryDate
);
res.status(200).json({ link });
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,122 @@
import { NextFunction, Request, Response, Router } from 'express';
import { body } from 'express-validator';
import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication';
import BaseController from '../BaseController';
@Service()
export class StripeIntegrationController extends BaseController {
@Inject()
private stripePaymentApp: StripePaymentApplication;
public router() {
const router = Router();
router.get('/link', this.getStripeConnectLink.bind(this));
router.post(
'/callback',
[body('code').exists()],
this.validationResult,
this.exchangeOAuth.bind(this)
);
router.post('/account', asyncMiddleware(this.createAccount.bind(this)));
router.post(
'/account_link',
[body('stripe_account_id').exists()],
this.validationResult,
asyncMiddleware(this.createAccountLink.bind(this))
);
return router;
}
/**
* Retrieves Stripe OAuth2 connect link.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
public async getStripeConnectLink(
req: Request,
res: Response,
next: NextFunction
) {
try {
const authorizationUri = this.stripePaymentApp.getStripeConnectLink();
return res.status(200).send({ url: authorizationUri });
} catch (error) {
next(error);
}
}
/**
* Exchanges the given Stripe authorization code to Stripe user id and access token.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<void>}
*/
public async exchangeOAuth(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { code } = this.matchedBodyData(req);
try {
await this.stripePaymentApp.exchangeStripeOAuthToken(tenantId, code);
return res.status(200).send({});
} catch (error) {
next(error);
}
}
/**
* Creates a new Stripe account.
* @param {Request} req - The Express request object.
* @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function.
* @returns {Promise<void>}
*/
public async createAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const accountId = await this.stripePaymentApp.createStripeAccount(
tenantId
);
return res.status(201).json({
accountId,
message: 'The Stripe account has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Creates a new Stripe account session.
* @param {Request} req - The Express request object.
* @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function.
* @returns {Promise<void>}
*/
public async createAccountLink(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { stripeAccountId } = this.matchedBodyData(req);
try {
const clientSecret = await this.stripePaymentApp.createAccountLink(
tenantId,
stripeAccountId
);
return res.status(200).json({ clientSecret });
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,83 @@
import { NextFunction, Request, Response, Router } from 'express';
import { Inject, Service } from 'typedi';
import bodyParser from 'body-parser';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { StripeWebhookEventPayload } from '@/interfaces/StripePayment';
import { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
import events from '@/subscribers/events';
import config from '@/config';
@Service()
export class StripeWebhooksController {
@Inject()
private stripePaymentService: StripePaymentService;
@Inject()
private eventPublisher: EventPublisher;
public router() {
const router = Router();
router.post(
'/stripe',
bodyParser.raw({ type: 'application/json' }),
this.handleWebhook.bind(this)
);
return router;
}
/**
* Handles incoming Stripe webhook events.
* Verifies the webhook signature, processes the event based on its type,
* and triggers appropriate actions or events in the system.
*
* @param {Request} req - The Express request object containing the webhook payload.
* @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function.
*/
private async handleWebhook(req: Request, res: Response, next: NextFunction) {
try {
let event = req.body;
const sig = req.headers['stripe-signature'];
// Verify webhook signature and extract the event.
// See https://stripe.com/docs/webhooks#verify-events for more information.
try {
event = this.stripePaymentService.stripe.webhooks.constructEvent(
req.rawBody,
sig,
config.stripePayment.webhooksSecret
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event based on its type
switch (event.type) {
case 'checkout.session.completed':
// Triggers `onStripeCheckoutSessionCompleted` event.
this.eventPublisher.emitAsync(
events.stripeWebhooks.onCheckoutSessionCompleted,
{
event,
} as StripeWebhookEventPayload
);
break;
case 'account.updated':
this.eventPublisher.emitAsync(
events.stripeWebhooks.onAccountUpdated,
{
event,
} as StripeWebhookEventPayload
);
break;
// Add more cases as needed
default:
console.log(`Unhandled event type ${event.type}`);
}
res.status(200).json({ received: true });
} catch (error) {
next(error);
}
}
}

View File

@@ -1,9 +1,10 @@
import { NextFunction, Router, Request, Response } from 'express';
import { Inject, Service } from 'typedi';
import Container, { Inject, Service } from 'typedi';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
import { StripeWebhooksController } from '../StripeIntegration/StripeWebhooksController';
@Service()
export class Webhooks extends BaseController {
@@ -24,6 +25,8 @@ export class Webhooks extends BaseController {
router.post('/lemon', this.lemonWebhooks.bind(this));
router.use(Container.get(StripeWebhooksController).router());
return router;
}

View File

@@ -64,6 +64,11 @@ import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
import { StripeIntegrationController } from './controllers/StripeIntegration/StripeIntegrationController';
import { ShareLinkController } from './controllers/ShareLink/ShareLinkController';
import { PublicSharableLinkController } from './controllers/ShareLink/PublicSharableLinkController';
import { PdfTemplatesController } from './controllers/PdfTemplates/PdfTemplatesController';
import { PaymentServicesController } from './controllers/PaymentServices/PaymentServicesController';
export default () => {
const app = Router();
@@ -81,7 +86,11 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())
app.use('/demo', Container.get(OneClickDemoController).router());
app.use(
'/payment-links',
Container.get(PublicSharableLinkController).router()
);
// - Dashboard routes.
// ---------------------------
@@ -147,10 +156,22 @@ export default () => {
dashboard.use('/import', Container.get(ImportController).router());
dashboard.use('/export', Container.get(ExportController).router());
dashboard.use('/attachments', Container.get(AttachmentsController).router());
dashboard.use(
'/stripe_integration',
Container.get(StripeIntegrationController).router()
);
dashboard.use(
'/pdf-templates',
Container.get(PdfTemplatesController).router()
);
dashboard.use(
'/payment-services',
Container.get(PaymentServicesController).router()
);
dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router());
dashboard.use('/', Container.get(WarehousesItemController).router());
dashboard.use('/', Container.get(ShareLinkController).router());
dashboard.use('/dashboard', Container.get(DashboardController).router());
dashboard.use('/', Container.get(Miscellaneous).router());

View File

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

View File

@@ -50,7 +50,8 @@ export const injectI18nUtils = (req) => {
export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
.withGraphFetched('metadata')
.throwIfNotFound();
const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);

View File

@@ -253,4 +253,23 @@ module.exports = {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
/**
* PostHog
*/
posthog: {
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST,
},
/**
* Stripe Payment Integration.
*/
stripePayment: {
secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY || '',
publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY || '',
clientId: process.env.STRIPE_PAYMENT_CLIENT_ID || '',
redirectTo: process.env.STRIPE_PAYMENT_REDIRECT_URL || '',
webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET || '',
},
};

View File

@@ -0,0 +1,91 @@
export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited';
export const BILL_DELETED = 'Bill deleted';
export const PAYMENT_MADE_CREATED = 'Payment made created';
export const PAYMENT_MADE_EDITED = 'Payment made edited';
export const PAYMENT_MADE_DELETED = 'Payment made deleted';
export const EXPENSE_CREATED = 'Expense created';
export const EXPENSE_EDITED = 'Expense edited';
export const EXPENSE_DELETED = 'Expense deleted';
export const ACCOUNT_CREATED = 'Account created';
export const ACCOUNT_EDITED = 'Account Edited';
export const ACCOUNT_DELETED = 'Account deleted';
export const ITEM_EVENT_CREATED = 'Item created';
export const ITEM_EVENT_EDITED = 'Item edited';
export const ITEM_EVENT_DELETED = 'Item deleted';
export const AUTH_SIGNED_UP = 'Auth Signed-up';
export const AUTH_RESET_PASSWORD = 'Auth reset password';
export const SUBSCRIPTION_CANCELLED = 'Subscription cancelled';
export const SUBSCRIPTION_RESUMED = 'Subscription resumed';
export const SUBSCRIPTION_PLAN_CHANGED = 'Subscription plan changed';
export const CUSTOMER_CREATED = 'Customer created';
export const CUSTOMER_EDITED = 'Customer edited';
export const CUSTOMER_DELETED = 'Customer deleted';
export const VENDOR_CREATED = 'Vendor created';
export const VENDOR_EDITED = 'Vendor edited';
export const VENDOR_DELETED = 'Vendor deleted';
export const TRANSACTIONS_LOCKING_LOCKED = 'Transactions locking locked';
export const TRANSACTIONS_LOCKING_LOCKING_CANCELLED =
'Transactions locking cancelled';
export const TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED =
'Transactions locking partially unlocked';
export const TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED =
'Transactions locking partially unlock cancelled';
export const BANK_TRANSACTION_MATCHED = 'Bank transaction matching deleted';
export const BANK_TRANSACTION_EXCLUDED = 'Bank transaction excluded';
export const BANK_TRANSACTION_CATEGORIZED = 'Bank transaction categorized';
export const BANK_TRANSACTION_UNCATEGORIZED = 'Bank transaction uncategorized';
export const BANK_ACCOUNT_DISCONNECTED = 'Bank account disconnected';
export const MANUAL_JOURNAL_CREATED = 'Manual journal created';
export const MANUAL_JOURNAL_EDITED = 'Manual journal edited';
export const MANUAL_JOURNAL_DELETED = 'Manual journal deleted';
export const MANUAL_JOURNAL_PUBLISHED = 'Manual journal published';
export const BANK_RULE_CREATED = 'Bank rule created';
export const BANK_RULE_EDITED = 'Bank rule edited';
export const BANK_RULE_DELETED = 'Bank rule deleted';
export const PDF_TEMPLATE_CREATED = 'PDF template created';
export const PDF_TEMPLATE_EDITED = 'PDF template edited';
export const PDF_TEMPLATE_DELETED = 'PDF template deleted';
export const PDF_TEMPLATE_ASSIGNED_DEFAULT = 'PDF template assigned as default';
export const PAYMENT_METHOD_EDITED = 'Payment method edited';
export const PAYMENT_METHOD_DELETED = 'Payment method deleted';
export const INVOICE_PAYMENT_LINK_GENERATED = 'Invoice payment link generated';
export const STRIPE_INTEGRAION_CONNECTED = 'Stripe integration oauth2 connected';
// # Event Groups
export const ACCOUNT_GROUP = 'Account';
export const ITEM_GROUP = 'Item';
export const AUTH_GROUP = 'Auth';
export const SALE_GROUP = 'Sale';
export const PAYMENT_GROUP = 'Payment';
export const BILL_GROUP = 'Bill';
export const EXPENSE_GROUP = 'Expense';

View File

@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.table('payment_receives', (table) => {
table.string('stripe_pintent_id').nullable();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('payment_receives', (table) => {
table.dropColumn('stripe_pintent_id');
});
};

View File

@@ -0,0 +1,75 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema
.createTable('pdf_templates', (table) => {
table.increments('id').primary();
table.text('resource');
table.text('template_name');
table.json('attributes');
table.boolean('predefined').defaultTo(false);
table.boolean('default').defaultTo(false);
table.timestamps();
})
.table('sales_invoices', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('sales_estimates', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('sales_receipts', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('credit_notes', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
})
.table('payment_receives', (table) => {
table
.integer('pdf_template_id')
.unsigned()
.references('id')
.inTable('pdf_templates');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema
.table('payment_receives', (table) => {
table.dropColumn('pdf_template_id');
})
.table('credit_notes', (table) => {
table.dropColumn('pdf_template_id');
})
.table('sales_receipts', (table) => {
table.dropColumn('pdf_template_id');
})
.table('sales_estimates', (table) => {
table.dropColumn('pdf_template_id');
})
.table('sales_invoices', (table) => {
table.dropColumn('pdf_template_id');
})
.dropTableIfExists('pdf_templates');
};

View File

@@ -0,0 +1,25 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('payment_integrations', (table) => {
table.increments('id');
table.string('service');
table.string('name');
table.string('slug');
table.boolean('payment_enabled').defaultTo(false);
table.boolean('payout_enabled').defaultTo(false);
table.string('account_id');
table.json('options');
table.timestamps();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('payment_integrations');
};

View File

@@ -0,0 +1,27 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('transactions_payment_methods', (table) => {
table.increments('id');
table.integer('reference_id').unsigned();
table.string('reference_type');
table
.integer('payment_integration_id')
.unsigned()
.index()
.references('id')
.inTable('payment_integrations');
table.boolean('enable').defaultTo(false);
table.json('options').nullable();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('transactions_payment_methods');
};

View File

@@ -0,0 +1,44 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex('pdf_templates').insert([
{
resource: 'SaleInvoice',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'SaleEstimate',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'SaleReceipt',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'CreditNote',
templateName: 'Standard Template',
predefined: true,
default: true,
},
{
resource: 'PaymentReceive',
templateName: 'Standard Template',
predefined: true,
default: true,
},
]);
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {};

View File

@@ -31,6 +31,17 @@ export const PrepardExpenses = {
predefined: true,
};
export const StripeClearingAccount = {
name: 'Stripe Clearing',
slug: 'stripe-clearing',
account_type: 'other-current-asset',
parent_account_id: null,
code: '100020',
active: true,
index: 1,
predefined: true,
}
export default [
{
name: 'Bank Account',

View File

@@ -99,6 +99,7 @@ export interface IBillsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
page: number;
pageSize: number;
filterQuery?: (q: any) => void;
}
export interface IBillsService {

View File

@@ -146,6 +146,7 @@ export interface ICashflowTransactionUncategorizedPayload {
tenantId: number;
uncategorizedTransactionId: number;
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
oldMainUncategorizedTransaction: IUncategorizedCashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
trx: Knex.Transaction;
}

View File

@@ -241,6 +241,7 @@ export interface ICustomerEventCreatingPayload {
trx: Knex.Transaction;
}
export interface ICustomerEventEditedPayload {
tenantId: number
customerId: number;
customer: ICustomer;
trx: Knex.Transaction;

View File

@@ -1,10 +1,10 @@
import { Knex } from 'knex';
import { IDynamicListFilter, IItemEntry, IVendorCredit } from '@/interfaces';
import { IDynamicListFilter, IItemEntry } from '@/interfaces';
import { ILedgerEntry } from './Ledger';
import { AttachmentLinkDTO } from './Attachments';
export interface ICreditNoteEntryNewDTO {
index: number;
index?: number;
itemId: number;
rate: number;
quantity: number;
@@ -22,7 +22,7 @@ export interface ICreditNoteNewDTO {
entries: ICreditNoteEntryNewDTO[];
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[]
attachments?: AttachmentLinkDTO[];
}
export interface ICreditNoteEditDTO {
@@ -35,7 +35,7 @@ export interface ICreditNoteEditDTO {
entries: ICreditNoteEntryNewDTO[];
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[]
attachments?: AttachmentLinkDTO[];
}
export interface ICreditNoteEntry extends IItemEntry {}
@@ -61,7 +61,9 @@ export interface ICreditNote {
localAmount?: number;
branchId?: number;
warehouseId: number;
createdAt?: Date,
createdAt?: Date;
termsConditions: string;
note: string;
}
export enum CreditNoteAction {
@@ -130,7 +132,9 @@ export interface ICreditNoteOpenedPayload {
trx: Knex.Transaction;
}
export interface ICreditNotesQueryDTO {}
export interface ICreditNotesQueryDTO {
filterQuery?: (query: any) => void;
}
export interface ICreditNotesQueryDTO extends IDynamicListFilter {
page: number;
@@ -256,3 +260,57 @@ export type ICreditNoteGLCommonEntry = Pick<
| 'debit'
| 'branchId'
>;
export interface CreditNotePdfTemplateAttributes {
// # Primary color
primaryColor: string;
secondaryColor: string;
// # Company logo
showCompanyLogo: boolean;
companyLogo: string;
// # Company name
companyName: string;
// # Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// # Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
total: string;
totalLabel: string;
showTotal: boolean;
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;
lines: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
showCreditNoteNumber: boolean;
creditNoteNumberLabel: string;
creditNoteNumebr: string;
creditNoteDate: string;
showCreditNoteDate: boolean;
creditNoteDateLabel: string;
}

View File

@@ -17,6 +17,7 @@ export interface IExpensesFilter {
columnSortBy: string;
sortOrder: string;
viewSlug?: string;
filterQuery?: (query: any) => void;
}
export interface IExpense {

View File

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

View File

@@ -151,6 +151,7 @@ export interface IModelMetaFieldCommon2 {
importHint?: string;
order?: number;
unique?: number;
features?: Array<any>
}
export interface IModelMetaRelationField2 {

View File

@@ -25,6 +25,7 @@ export interface IPaymentReceived {
updatedAt: Date;
localAmount?: number;
branchId?: number;
pdfTemplateId?: number;
}
export interface IPaymentReceivedCreateDTO {
customerId: number;
@@ -66,7 +67,7 @@ export interface IPaymentReceivedEntry {
export interface IPaymentReceivedEntryDTO {
id?: number;
index: number;
index?: number;
paymentReceiveId?: number;
invoiceId: number;
paymentAmount: number;
@@ -74,6 +75,7 @@ export interface IPaymentReceivedEntryDTO {
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
filterQuery?: (trx: Knex.Transaction) => void;
}
export interface IPaymentReceivePageEntry {
@@ -184,3 +186,55 @@ export interface PaymentReceiveMailPresendEvent {
paymentReceiveId: number;
messageOptions: PaymentReceiveMailOptsDTO;
}
export interface PaymentReceivedPdfLineItem {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface PaymentReceivedPdfTax {
label: string;
amount: string;
}
export interface PaymentReceivedPdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
total: string;
totalLabel: string;
showTotal: boolean;
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
lines: Array<{
invoiceNumber: string;
invoiceAmount: string;
paidAmount: string;
}>;
showPaymentReceivedNumber: boolean;
paymentReceivedNumberLabel: string;
paymentReceivedNumebr: string;
paymentReceivedDate: string;
showPaymentReceivedDate: boolean;
paymentReceivedDateLabel: string;
}

View File

@@ -44,6 +44,7 @@ export interface ISaleEstimateDTO {
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
filterQuery?: (q: any) => void;
}
export interface ISalesEstimatesService {
@@ -142,3 +143,4 @@ export interface ISaleEstimateMailPresendEvent {
saleEstimateId: number;
messageOptions: SaleEstimateMailOptionsDTO;
}

View File

@@ -5,6 +5,34 @@ import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { AttachmentLinkDTO } from './Attachments';
export interface PaymentIntegrationTransactionLink {
id: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
}
export interface PaymentIntegrationTransactionLinkEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
saleInvoiceId: number;
trx?: Knex.Transaction
}
export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
oldSaleInvoiceId: number;
trx?: Knex.Transaction
}
export interface ISaleInvoice {
id: number;
amount: number;
@@ -45,6 +73,13 @@ export interface ISaleInvoice {
subtotal: number;
subtotalLocal: number;
subtotalExludingTax: number;
termsConditions: string;
invoiceMessage: string;
pdfTemplateId?: number;
paymentMethods?: Array<PaymentIntegrationTransactionLink>;
}
export interface ISaleInvoiceDTO {
@@ -79,6 +114,7 @@ export interface ISalesInvoicesFilter extends IDynamicListFilter {
page: number;
pageSize: number;
searchKeyword?: string;
filterQuery?: (q: Knex.QueryBuilder) => void;
}
export interface ISalesInvoicesService {
@@ -130,9 +166,15 @@ export interface ISaleInvoiceEditingPayload {
export interface ISaleInvoiceDeletePayload {
tenantId: number;
saleInvoice: ISaleInvoice;
oldSaleInvoice: ISaleInvoice;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceDeletingPayload {
tenantId: number;
oldSaleInvoice: ISaleInvoice;
saleInvoiceId: number;
trx: Knex.Transaction;
}
export interface ISaleInvoiceDeletedPayload {
@@ -216,3 +258,84 @@ export interface ISaleInvoiceMailSent {
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
}
// Invoice Pdf Document
export interface InvoicePdfLine {
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}
export interface InvoicePdfTax {
label: string;
amount: string;
}
export interface InvoicePdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
companyName: string;
showCompanyLogo: boolean;
companyLogo: string;
dueDate: string;
dueDateLabel: string;
showDueDate: boolean;
dateIssue: string;
dateIssueLabel: string;
showDateIssue: boolean;
invoiceNumberLabel: string;
invoiceNumber: string;
showInvoiceNumber: boolean;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
lineItemLabel: string;
lineDescriptionLabel: string;
lineRateLabel: string;
lineTotalLabel: string;
totalLabel: string;
subtotalLabel: string;
discountLabel: string;
paymentMadeLabel: string;
balanceDueLabel: string;
showTotal: boolean;
showSubtotal: boolean;
showDiscount: boolean;
showTaxes: boolean;
showPaymentMade: boolean;
showDueAmount: boolean;
showBalanceDue: boolean;
total: string;
subtotal: string;
discount: string;
paymentMade: string;
balanceDue: string;
termsConditionsLabel: string;
showTermsConditions: boolean;
termsConditions: string;
lines: InvoicePdfLine[];
taxes: InvoicePdfTax[];
statementLabel: string;
showStatement: boolean;
statement: string;
}

View File

@@ -29,7 +29,9 @@ export interface ISaleReceipt {
entries?: IItemEntry[];
}
export interface ISalesReceiptsFilter {}
export interface ISalesReceiptsFilter {
filterQuery?: (query: any) => void;
}
export interface ISaleReceiptDTO {
customerId: number;
@@ -153,3 +155,59 @@ export interface ISaleReceiptMailPresend {
saleReceiptId: number;
messageOptions: SaleReceiptMailOptsDTO;
}
export interface ISaleReceiptBrandingTemplateAttributes {
primaryColor: string;
secondaryColor: string;
showCompanyLogo: boolean;
companyLogo: string;
companyName: string;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
// Total
total: string;
totalLabel: string;
showTotal: boolean;
// Subtotal
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
// Customer Note
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
// Terms & Conditions
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;
// Lines
lines: Array<{
item: string;
description: string;
rate: string;
quantity: string;
total: string;
}>;
// Receipt Number
showReceiptNumber: boolean;
receiptNumberLabel: string;
receiptNumebr: string;
// Receipt Date
receiptDate: string;
showReceiptDate: boolean;
receiptDateLabel: string;
}

View File

@@ -18,14 +18,26 @@ export interface IOrganizationBuildDTO {
dateFormat?: string;
}
interface OrganizationAddressDTO {
address1: string;
address2: string;
postalCode: string;
city: string;
stateProvince: string;
phone: string;
}
export interface IOrganizationUpdateDTO {
name: string;
location: string;
baseCurrency: string;
timezone: string;
fiscalYear: string;
industry: string;
taxNumber: string;
location?: string;
baseCurrency?: string;
timezone?: string;
fiscalYear?: string;
industry?: string;
taxNumber?: string;
primaryColor?: string;
logoKey?: string;
address?: OrganizationAddressDTO;
}
export interface IOrganizationBuildEventPayload {
@@ -36,4 +48,4 @@ export interface IOrganizationBuildEventPayload {
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}
}

View File

@@ -0,0 +1,20 @@
export interface StripePaymentLinkCreatedEventPayload {
tenantId: number;
paymentLinkId: string;
saleInvoiceId: number;
stripeIntegrationId: number;
}
export interface StripeCheckoutSessionCompletedEventPayload {
event: any;
}
export interface StripeInvoiceCheckoutSessionPOJO {
sessionId: string;
publishableKey: string;
redirectTo: string;
}
export interface StripeWebhookEventPayload {
event: any;
}

View File

@@ -106,6 +106,7 @@ export interface IVendorCreditsQueryDTO extends IDynamicListFilter {
page: number;
pageSize: number;
searchKeyword?: string;
filterQuery?: (q: any) => void;
}
export interface IVendorCreditEditingPayload {

View File

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

View File

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

View File

@@ -117,7 +117,10 @@ import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAcco
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
import { InvoicePaymentIntegrationSubscriber } from '@/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber';
import { StripeWebhooksSubscriber } from '@/services/StripePayment/events/StripeWebhooksSubscriber';
import { SeedStripeAccountsOnOAuthGrantedSubscriber } from '@/services/StripePayment/events/SeedStripeAccounts';
export default () => {
return new EventPublisher();
@@ -251,7 +254,6 @@ export const susbcribers = () => {
// Subscription
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp,
TriggerInvalidateCacheOnSubscriptionChange,
// Attachments
AttachmentsOnSaleInvoiceCreated,
@@ -289,5 +291,12 @@ export const susbcribers = () => {
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
// Stripe Payment
InvoicePaymentIntegrationSubscriber,
StripeWebhooksSubscriber,
SeedStripeAccountsOnOAuthGrantedSubscriber,
...EventsTrackerListeners
];
};

View File

@@ -68,6 +68,9 @@ import { BankRule } from '@/models/BankRule';
import { BankRuleCondition } from '@/models/BankRuleCondition';
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
import { PdfTemplate } from '@/models/PdfTemplate';
import { PaymentIntegration } from '@/models/PaymentIntegration';
import { TransactionPaymentServiceEntry } from '@/models/TransactionPaymentServiceEntry';
export default (knex) => {
const models = {
@@ -139,6 +142,9 @@ export default (knex) => {
BankRuleCondition,
RecognizedBankTransaction,
MatchedBankTransaction,
PdfTemplate,
PaymentIntegration,
TransactionPaymentServiceEntry,
};
return mapValues(models, (model) => model.bindKnex(knex));
};

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default {
defaultFilterField: 'vendor',
defaultSort: {
@@ -167,6 +169,18 @@ export default {
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
},
fields2: {
billNumber: {
@@ -238,6 +252,22 @@ export default {
},
},
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
},
};

View File

@@ -402,6 +402,7 @@ export default class Bill extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const BillLandedCost = require('models/BillLandedCost');
const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
@@ -453,6 +454,18 @@ export default class Bill extends mixin(TenantModel, [
},
},
/**
* Bill may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'bills.warehouseId',
to: 'warehouses.id',
},
},
/**
* Bill may has associated tax rate transactions.
*/

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default {
defaultFilterField: 'vendor',
defaultSort: {
@@ -5,6 +7,8 @@ export default {
sortField: 'bill_date',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
@@ -77,7 +81,7 @@ export default {
paymentDate: {
name: 'bill_payment.field.payment_date',
type: 'date',
accessor: 'formattedPaymentDate'
accessor: 'formattedPaymentDate',
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
@@ -111,6 +115,40 @@ export default {
name: 'bill_payment.field.reference',
type: 'text',
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Bill date',
accessor: 'bill.formattedBillDate',
},
billNo: {
name: 'Bill No.',
accessor: 'bill.billNo',
},
billRefNo: {
name: 'Bill Reference No.',
accessor: 'bill.referenceNo',
},
billAmount: {
name: 'Bill Amount',
accessor: 'bill.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
},
fields2: {
vendorId: {
@@ -174,5 +212,13 @@ export default {
},
},
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
},
};

View File

@@ -0,0 +1,11 @@
export default {
fields2: {
name: {
name: 'Name',
fieldType: 'text',
required: true,
},
},
columns: {},
fields: {}
};

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import BranchMetadata from './Branch.settings';
import ModelSetting from './ModelSetting';
export default class Branch extends TenantModel {
export default class Branch extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
@@ -169,4 +171,11 @@ export default class Branch extends TenantModel {
},
};
}
/**
* Model settings.
*/
static get meta() {
return BranchMetadata;
}
}

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
@@ -100,7 +102,7 @@ export default {
},
creditNoteDate: {
name: 'Credit Note Date',
accessor: 'formattedCreditNoteDate'
accessor: 'formattedCreditNoteDate',
},
referenceNo: {
name: 'Reference No.',
@@ -147,6 +149,18 @@ export default {
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
},
fields2: {
customerId: {
@@ -215,5 +229,21 @@ export default {
},
},
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
},
};

View File

@@ -175,6 +175,7 @@ export default class CreditNote extends mixin(TenantModel, [
const Customer = require('models/Customer');
const Branch = require('models/Branch');
const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
return {
/**
@@ -235,6 +236,18 @@ export default class CreditNote extends mixin(TenantModel, [
},
},
/**
* Credit note may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'credit_notes.warehouseId',
to: 'warehouses.id',
},
},
/**
* Credit note may has many attached attachments.
*/

View File

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

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
/**
* Expense - Settings.
*/
@@ -119,6 +121,12 @@ export default {
type: 'boolean',
printable: false,
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
},
fields2: {
paymentAccountId: {
@@ -178,6 +186,14 @@ export default {
name: 'expense.field.publish',
fieldType: 'boolean',
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
},
};

View File

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

View File

@@ -0,0 +1,61 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export class PaymentIntegration extends Model {
paymentEnabled!: boolean;
payoutEnabled!: boolean;
static get tableName() {
return 'payment_integrations';
}
static get idColumn() {
return 'id';
}
static get virtualAttributes() {
return ['fullEnabled'];
}
static get jsonAttributes() {
return ['options'];
}
get fullEnabled() {
return this.paymentEnabled && this.payoutEnabled;
}
static get modifiers() {
return {
/**
* Query to filter enabled payment and payout.
*/
fullEnabled(query) {
query.where('paymentEnabled', true).andWhere('payoutEnabled', true);
},
};
}
static get jsonSchema() {
return {
type: 'object',
required: ['name', 'service'],
properties: {
id: { type: 'integer' },
service: { type: 'string' },
paymentEnabled: { type: 'boolean' },
payoutEnabled: { type: 'boolean' },
accountId: { type: 'string' },
options: {
type: 'object',
properties: {
bankAccountId: { type: 'number' },
clearingAccountId: { type: 'number' },
},
},
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
};
}
}

View File

@@ -1,6 +1,11 @@
import { Features } from '@/interfaces';
export default {
importable: true,
exportable: true,
exportFlattenOn: 'entries',
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo',
@@ -72,7 +77,7 @@ export default {
amount: {
name: 'payment_receive.field.amount',
type: 'number',
accessor: 'formattedAmount'
accessor: 'formattedAmount',
},
referenceNo: {
name: 'payment_receive.field.reference_no',
@@ -92,11 +97,45 @@ export default {
type: 'text',
printable: false,
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Invoice date',
accessor: 'invoice.invoiceDateFormatted',
},
invoiceNo: {
name: 'Invoice No.',
accessor: 'invoice.invoiceNo',
},
invoiceRefNo: {
name: 'Invoice Reference No.',
accessor: 'invoice.referenceNo',
},
invoiceAmount: {
name: 'Invoice Amount',
accessor: 'invoice.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
created_at: {
name: 'payment_receive.field.created_at',
type: 'date',
printable: false,
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
},
fields2: {
customerId: {
@@ -158,5 +197,13 @@ export default {
},
},
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
},
};

View File

@@ -0,0 +1,45 @@
import TenantModel from 'models/TenantModel';
export class PdfTemplate extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'pdf_templates';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Json schema.
*/
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
templateName: { type: 'string' },
attributes: { type: 'object' }, // JSON field definition
},
};
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {};
}
}

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default {
defaultFilterField: 'estimate_date',
defaultSort: {
@@ -13,7 +15,7 @@ export default {
importAggregateBy: 'estimateNumber',
print: {
pageTitle: 'Sale Estimates'
pageTitle: 'Sale Estimates',
},
fields: {
@@ -142,6 +144,7 @@ export default {
delivered: {
name: 'Delivered',
type: 'boolean',
accessor: 'isDelivered',
exportable: true,
printable: false,
},
@@ -173,6 +176,18 @@ export default {
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
},
fields2: {
customerId: {
@@ -251,6 +266,22 @@ export default {
},
},
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
},
};

View File

@@ -182,6 +182,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer');
const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const Document = require('models/Document');
return {
@@ -221,6 +222,18 @@ export default class SaleEstimate extends mixin(TenantModel, [
},
},
/**
* Sale estimate may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'sales_estimates.warehouseId',
to: 'warehouses.id',
},
},
/**
* Sale estimate transaction may has many attached attachments.
*/

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default {
defaultFilterField: 'customer',
defaultSort: {
@@ -155,6 +157,7 @@ export default {
name: 'invoice.field.delivered',
type: 'boolean',
printable: false,
accessor: 'isDelivered',
},
entries: {
name: 'Entries',
@@ -184,6 +187,18 @@ export default {
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
},
fields2: {
invoiceDate: {
@@ -267,6 +282,22 @@ export default {
fieldType: 'boolean',
printable: false,
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
},
};

View File

@@ -408,10 +408,15 @@ export default class SaleInvoice extends mixin(TenantModel, [
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const Account = require('models/Account');
const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const {
TransactionPaymentServiceEntry,
} = require('models/TransactionPaymentServiceEntry');
const { PdfTemplate } = require('models/PdfTemplate');
return {
/**
@@ -499,6 +504,18 @@ export default class SaleInvoice extends mixin(TenantModel, [
},
},
/**
* Invoice may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'sales_invoices.warehouseId',
to: 'warehouses.id',
},
},
/**
* Invoice may has associated written-off expense account.
*/
@@ -553,12 +570,42 @@ export default class SaleInvoice extends mixin(TenantModel, [
modelClass: MatchedBankTransaction,
join: {
from: 'sales_invoices.id',
to: "matched_bank_transactions.referenceId",
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice may belongs to payment methods entries.
*/
paymentMethods: {
relation: Model.HasManyRelation,
modelClass: TransactionPaymentServiceEntry,
join: {
from: 'sales_invoices.id',
to: 'transactions_payment_methods.referenceId',
},
beforeInsert: (model) => {
model.referenceType = 'SaleInvoice';
},
filter: (query) => {
query.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'sales_invoices.pdfTemplateId',
to: 'pdf_templates.id',
}
},
};
}

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default {
defaultFilterField: 'receipt_date',
defaultSort: {
@@ -169,6 +171,18 @@ export default {
type: 'date',
printable: false,
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
},
fields2: {
receiptDate: {
@@ -245,6 +259,22 @@ export default {
name: 'Receipt Message',
fieldType: 'text',
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
},
};

View File

@@ -109,6 +109,7 @@ export default class SaleReceipt extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const Branch = require('models/Branch');
const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
return {
customer: {
@@ -169,6 +170,18 @@ export default class SaleReceipt extends mixin(TenantModel, [
},
},
/**
* Sale receipt may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'sales_receipts.warehouseId',
to: 'warehouses.id',
},
},
/**
* Sale receipt transaction may has many attached attachments.
*/

View File

@@ -0,0 +1,46 @@
import TenantModel from 'models/TenantModel';
export class TransactionPaymentServiceEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'transactions_payment_methods';
}
/**
* Json schema of the model.
*/
static get jsonSchema() {
return {
type: 'object',
required: ['paymentIntegrationId'],
properties: {
id: { type: 'integer' },
referenceId: { type: 'integer' },
referenceType: { type: 'string' },
paymentIntegrationId: { type: 'integer' },
enable: { type: 'boolean' },
options: { type: 'object' },
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { PaymentIntegration } = require('./PaymentIntegration');
return {
paymentIntegration: {
relation: TenantModel.BelongsToOneRelation,
modelClass: PaymentIntegration,
join: {
from: 'transactions_payment_methods.paymentIntegrationId',
to: 'payment_integrations.id',
},
},
};
}
}

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
@@ -160,6 +162,18 @@ export default {
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
},
fields2: {
vendorId: {
@@ -225,5 +239,21 @@ export default {
},
},
},
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true
},
},
};

View File

@@ -178,6 +178,7 @@ export default class VendorCredit extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const Branch = require('models/Branch');
const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
return {
vendor: {
@@ -217,6 +218,18 @@ export default class VendorCredit extends mixin(TenantModel, [
},
},
/**
* Vendor credit may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'vendor_credits.warehouseId',
to: 'warehouses.id',
},
},
/**
* Vendor credit may has many attached attachments.
*/

View File

@@ -0,0 +1,11 @@
export default {
fields2: {
name: {
name: 'Name',
fieldType: 'text',
required: true,
},
},
columns: {},
fields: {}
};

View File

@@ -4,6 +4,7 @@ import { IAccount } from '@/interfaces';
import { Knex } from 'knex';
import {
PrepardExpenses,
StripeClearingAccount,
TaxPayableAccount,
UnearnedRevenueAccount,
} from '@/database/seeds/data/accounts';
@@ -247,4 +248,37 @@ export default class AccountRepository extends TenantRepository {
}
return result;
}
/**
* Finds or creates the stripe clearing account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateStripeClearing(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction
) {
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({
tenantId: this.tenantId,
});
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...StripeClearingAccount,
..._extraAttrs,
});
}
return result;
}
}

View File

@@ -1,17 +1,16 @@
import { NextFunction, Request, Response } from 'express';
import multer from 'multer';
import type { Multer } from 'multer';
import multerS3 from 'multer-s3';
import { s3 } from '@/lib/S3/S3';
import { Service } from 'typedi';
import config from '@/config';
import { NextFunction, Request, Response } from 'express';
@Service()
export class AttachmentUploadPipeline {
/**
* Middleware to ensure that S3 configuration is properly set before proceeding.
* This function checks if the necessary S3 configuration keys are present and throws an error if any are missing.
*
* @param req The HTTP request object.
* @param res The HTTP response object.
* @param next The callback to pass control to the next middleware function.
@@ -49,6 +48,11 @@ export class AttachmentUploadPipeline {
key: function (req, file, cb) {
cb(null, Date.now().toString());
},
acl: function(req, file, cb) {
// Conditionally set file to public or private based on isPublic flag
const aclValue = true ? 'public-read' : 'private';
cb(null, aclValue); // Set ACL based on the isPublic flag
}
}),
});
}

View File

@@ -3,7 +3,7 @@ import { isEmpty } from 'lodash';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod,
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletingPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
@@ -146,13 +146,13 @@ export class AttachmentsOnSaleInvoiceCreated {
*/
private async handleUnlinkAttachmentsOnInvoiceDeleted({
tenantId,
saleInvoice,
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletePayload) {
}: ISaleInvoiceDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'SaleInvoice',
saleInvoice.id,
oldSaleInvoice.id,
trx
);
}

View File

@@ -0,0 +1,7 @@
import path from 'path';
import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => {
return path.join(config.s3.endpoint, config.s3.bucket, objectKey);
}

View File

@@ -78,9 +78,9 @@ export class RecognizeTranasctionsService {
});
const bankRules = await BankRule.query(trx).onBuild((q) => {
const rulesIds = castArray(ruleId);
const rulesIds = !isEmpty(ruleId) ? castArray(ruleId) : [];
if (!isEmpty(rulesIds)) {
if (rulesIds?.length > 0) {
q.whereIn('id', rulesIds);
}
q.withGraphFetched('conditions');

View File

@@ -33,22 +33,25 @@ export class UncategorizeCashflowTransaction {
): Promise<Array<number>> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const oldUncategorizedTransaction =
const oldMainUncategorizedTransaction =
await UncategorizedCashflowTransaction.query()
.findById(uncategorizedTransactionId)
.throwIfNotFound();
validateTransactionShouldBeCategorized(oldUncategorizedTransaction);
validateTransactionShouldBeCategorized(oldMainUncategorizedTransaction);
const associatedUncategorizedTransactions =
await UncategorizedCashflowTransaction.query()
.where('categorizeRefId', oldUncategorizedTransaction.categorizeRefId)
.where('categorizeRefId', oldMainUncategorizedTransaction.categorizeRefId)
.where(
'categorizeRefType',
oldUncategorizedTransaction.categorizeRefType
);
oldMainUncategorizedTransaction.categorizeRefType
)
// Exclude the main transaction.
.whereNot('id', uncategorizedTransactionId);
const oldUncategorizedTransactions = [
oldUncategorizedTransaction,
oldMainUncategorizedTransaction,
...associatedUncategorizedTransactions,
];
const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map(
@@ -85,6 +88,7 @@ export class UncategorizeCashflowTransaction {
{
tenantId,
uncategorizedTransactionId,
oldMainUncategorizedTransaction,
uncategorizedTransactions,
oldUncategorizedTransactions,
trx,

View File

@@ -22,32 +22,25 @@ export class DeleteCashflowTransactionOnUncategorize {
};
/**
* Deletes the cashflow transaction on uncategorize transaction.
* Deletes the cashflow transaction once uncategorize the bank transaction.
* @param {ICashflowTransactionUncategorizedPayload} payload
*/
public async deleteCashflowTransactionOnUncategorize({
tenantId,
oldUncategorizedTransactions,
oldMainUncategorizedTransaction,
trx,
}: ICashflowTransactionUncategorizedPayload) {
const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter(
(transaction) => transaction.categorizeRefType === 'CashflowTransaction'
);
// Deletes the cashflow transaction.
if (_oldUncategorizedTransactions.length > 0) {
const result = await PromisePool.withConcurrency(1)
.for(_oldUncategorizedTransactions)
.process(async (oldUncategorizedTransaction) => {
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
oldUncategorizedTransaction.categorizeRefId,
trx
);
});
if (result.errors.length > 0) {
throw new ServiceError('SOMETHING_WRONG');
}
// Cannot continue if the main transaction does not reference to cashflow type.
if (
oldMainUncategorizedTransaction.categorizeRefType !==
'CashflowTransaction'
) {
return;
}
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
oldMainUncategorizedTransaction.categorizeRefId,
trx
);
}
}

View File

@@ -20,6 +20,10 @@ export class ChromiumlyTenancy {
properties?: PageProperties,
pdfFormat?: PdfFormat
) {
return this.htmlConvert.convert(tenantId, content, properties, pdfFormat);
const parsedProperties = {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
...properties,
}
return this.htmlConvert.convert(tenantId, content, parsedProperties, pdfFormat);
}
}

View File

@@ -65,6 +65,7 @@ export class EditCustomer {
});
// Triggers `onCustomerEdited` event.
await this.eventPublisher.emitAsync(events.customers.onEdited, {
tenantId,
customerId,
customer,
trx,

View File

@@ -59,7 +59,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
creditNoteDTO.entries
);
// Transformes the given DTO to storage layer data.
const creditNoteModel = this.transformCreateEditDTOToModel(
const creditNoteModel = await this.transformCreateEditDTOToModel(
tenantId,
creditNoteDTO,
customer.currencyCode

View File

@@ -0,0 +1,48 @@
import { Inject } from 'typedi';
import { GetPdfTemplate } from '../PdfTemplate/GetPdfTemplate';
import { defaultCreditNoteBrandingAttributes } from './constants';
import { mergePdfTemplateWithDefaultAttributes } from '../Sales/Invoices/utils';
import { GetOrganizationBrandingAttributes } from '../PdfTemplate/GetOrganizationBrandingAttributes';
export class CreditNoteBrandingTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
@Inject()
private getOrgBrandingAttributes: GetOrganizationBrandingAttributes;
/**
* Retrieves the credit note branding template.
* @param {number} tenantId
* @param {number} templateId
* @returns {}
*/
public async getCreditNoteBrandingTemplate(
tenantId: number,
templateId: number
) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
templateId
);
// Retrieves the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(
tenantId
);
// Merges the default branding attributes with common organization branding attrs.
const organizationBrandingAttrs = {
...defaultCreditNoteBrandingAttributes,
...commonOrgBrandingAttrs,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
organizationBrandingAttrs
);
return {
...template,
attributes,
};
}
}

View File

@@ -2,14 +2,22 @@ import { Service, Inject } from 'typedi';
import moment from 'moment';
import { omit } from 'lodash';
import * as R from 'ramda';
import composeAsync from 'async/compose';
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { ICreditNote, ICreditNoteEditDTO, ICreditNoteNewDTO } from '@/interfaces';
import {
ICreditNote,
ICreditNoteEditDTO,
ICreditNoteEntryNewDTO,
ICreditNoteNewDTO,
} from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer';
@Service()
export default class BaseCreditNotes {
@@ -28,25 +36,35 @@ export default class BaseCreditNotes {
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
/**
* Transformes the credit/edit DTO to model.
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
* @param {string} customerCurrencyCode -
*/
protected transformCreateEditDTOToModel = (
protected transformCreateEditDTOToModel = async (
tenantId: number,
creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO,
customerCurrencyCode: string,
oldCreditNote?: ICreditNote
): ICreditNote => {
): Promise<ICreditNote> => {
// Retrieve the total amount of the given items entries.
const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries
);
const entries = creditNoteDTO.entries.map((entry) => ({
...entry,
referenceType: 'CreditNote',
}));
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex,
// Associate the reference type to credit note entries.
R.map((entry: ICreditNoteEntryNewDTO) => ({
...entry,
referenceType: 'CreditNote',
}))
)(creditNoteDTO.entries);
// Retreive the next credit note number.
const autoNextNumber = this.getNextCreditNumber(tenantId);
@@ -70,10 +88,18 @@ export default class BaseCreditNotes {
refundedAmount: 0,
invoicesAmount: 0,
};
const initialAsyncDTO = await composeAsync(
// Assigns the default branding template id to the invoice DTO.
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
tenantId,
'CreditNote'
)
)(initialDTO);
return R.compose(
this.branchDTOTransform.transformDTO<ICreditNote>(tenantId),
this.warehouseDTOTransform.transformDTO<ICreditNote>(tenantId)
)(initialDTO);
)(initialAsyncDTO);
};
/**

View File

@@ -15,12 +15,17 @@ export class CreditNotesExportable extends Exportable {
* @returns {}
*/
public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
const filterQuery = (query) => {
query.withGraphFetched('branch');
query.withGraphFetched('warehouse');
};
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
filterQuery,
} as ICreditNotesQueryDTO;
return this.getCreditNotes

View File

@@ -63,7 +63,7 @@ export default class EditCreditNote extends BaseCreditNotes {
creditNoteEditDTO.entries
);
// Transformes the given DTO to storage layer data.
const creditNoteModel = this.transformCreateEditDTOToModel(
const creditNoteModel = await this.transformCreateEditDTOToModel(
tenantId,
creditNoteEditDTO,
customer.currencyCode,

View File

@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
import GetCreditNote from './GetCreditNote';
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
import { CreditNotePdfTemplateAttributes } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
import { transformCreditNoteToPdfTemplate } from './utils';
@Service()
export default class GetCreditNotePdf {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@@ -14,25 +21,60 @@ export default class GetCreditNotePdf {
@Inject()
private getCreditNoteService: GetCreditNote;
@Inject()
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
/**
* Retrieve sale invoice pdf content.
* Retrieves sale invoice pdf content.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
*/
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
const creditNote = await this.getCreditNoteService.getCreditNote(
const brandingAttributes = await this.getCreditNoteBrandingAttributes(
tenantId,
creditNoteId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/credit-note-standard',
{
creditNote,
}
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0, bottom: 0, left: 0, right: 0 },
});
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
}
/**
* Retrieves credit note branding attributes.
* @param {number} tenantId - The ID of the tenant.
* @param {number} creditNoteId - The ID of the credit note.
* @returns {Promise<CreditNotePdfTemplateAttributes>} The credit note branding attributes.
*/
public async getCreditNoteBrandingAttributes(
tenantId: number,
creditNoteId: number
): Promise<CreditNotePdfTemplateAttributes> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const creditNote = await this.getCreditNoteService.getCreditNote(
tenantId,
creditNoteId
);
// Retrieve the invoice template id of not found get the default template id.
const templateId =
creditNote.pdfTemplateId ??
(
await PdfTemplate.query().findOne({
resource: 'CreditNote',
default: true,
})
)?.id;
// Retrieves the credit note branding template.
const brandingTemplate =
await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate(
tenantId,
templateId
);
return {
...brandingTemplate.attributes,
...transformCreditNoteToPdfTemplate(creditNote),
};
}
}

View File

@@ -48,6 +48,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
creditNotesQuery?.filterQuery && creditNotesQuery?.filterQuery(builder);
})
.pagination(filter.page - 1, filter.pageSize);

View File

@@ -9,7 +9,7 @@ export const ERRORS = {
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES'
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES',
};
export const DEFAULT_VIEW_COLUMNS = [];
@@ -66,3 +66,67 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const defaultCreditNoteBrandingAttributes = {
// # Colors
primaryColor: '',
secondaryColor: '',
// # Company logo
showCompanyLogo: true,
companyLogoKey: '',
companyLogoUri: '',
// # Company name
companyName: 'Bigcapital Technology, Inc.',
// # Customer address
showCustomerAddress: true,
customerAddress: '',
// # Company address
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Total
total: '$1000.00',
totalLabel: 'Total',
showTotal: true,
// Subtotal
subtotal: '1000/00',
subtotalLabel: 'Subtotal',
showSubtotal: true,
// Customer note
showCustomerNote: true,
customerNote:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
customerNoteLabel: 'Customer Note',
// Terms & conditions
showTermsConditions: true,
termsConditions:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
termsConditionsLabel: 'Terms & Conditions',
lines: [
{
item: 'Simply dummy text',
description: 'Simply dummy text of the printing and typesetting',
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
// Credit note number.
showCreditNoteNumber: true,
creditNoteNumberLabel: 'Credit Note Number',
creditNoteNumebr: '346D3D40-0001',
// Credit note date.
creditNoteDate: 'September 3, 2024',
showCreditNoteDate: true,
creditNoteDateLabel: 'Credit Note Date',
};

View File

@@ -0,0 +1,25 @@
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const transformCreditNoteToPdfTemplate = (
creditNote: ICreditNote
): Partial<CreditNotePdfTemplateAttributes> => {
return {
creditNoteDate: creditNote.formattedCreditNoteDate,
creditNoteNumebr: creditNote.creditNoteNumber,
total: creditNote.formattedAmount,
subtotal: creditNote.formattedSubtotal,
lines: creditNote.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
total: entry.totalFormatted,
})),
customerNote: creditNote.note,
termsConditions: creditNote.termsConditions,
customerAddress: contactAddressTextFormat(creditNote.customer),
};
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBankRuleEventCreatedPayload,
IBankRuleEventEditedPayload,
IBankRuleEventDeletedPayload,
} from '@/services/Banking/Rules/types'; // Updated import path for interfaces
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BANK_RULE_CREATED,
BANK_RULE_EDITED,
BANK_RULE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class BankRuleEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.bankRules.onCreated,
this.handleTrackBankRuleCreatedEvent
);
bus.subscribe(
events.bankRules.onEdited,
this.handleTrackEditedBankRuleEvent
);
bus.subscribe(
events.bankRules.onDeleted,
this.handleTrackDeletedBankRuleEvent
);
}
private handleTrackBankRuleCreatedEvent = ({
tenantId,
}: IBankRuleEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_RULE_CREATED,
properties: {},
});
};
private handleTrackEditedBankRuleEvent = ({
tenantId,
}: IBankRuleEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_RULE_EDITED,
properties: {},
});
};
private handleTrackDeletedBankRuleEvent = ({
tenantId,
}: IBankRuleEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_RULE_DELETED,
properties: {},
});
};
}

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