Compare commits

...

69 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
415673656c feat: add api key table 2025-07-01 23:15:55 +02:00
Ahmed Bouhuolia
1906d9f3f5 Merge pull request #766 from bigcapitalhq/discount-line-level
fix: Line-level discount
2024-12-12 15:39:19 +02:00
Ahmed Bouhuolia
d640dc1f40 feat: add totalExcludingTax property and update GL entry calculations 2024-12-12 12:49:52 +02:00
Ahmed Bouhuolia
8cd1b36a02 feat: item-level discount 2024-12-11 15:05:50 +02:00
Ahmed Bouhuolia
5a8d9cc7e8 feat: wip line-level discount 2024-12-11 12:37:15 +02:00
Ahmed Bouhuolia
6323e2ffec fix: line-level discount 2024-12-11 11:44:10 +02:00
Ahmed Bouhuolia
7af2e7ccbc chore: update CHANGELOG 2024-12-09 12:23:46 +02:00
Ahmed Bouhuolia
baf4c691d6 Merge pull request #763 from bigcapitalhq/fix-discount-gl-entries
fix: discount transactions GL entries
2024-12-09 11:08:19 +02:00
Ahmed Bouhuolia
c633fa8522 feat: add vendor credit discount and adjustment GL entries 2024-12-09 11:06:42 +02:00
Ahmed Bouhuolia
1d54947764 fix: correct debit and credit calculations for local adjustments in BillGLEntries 2024-12-09 00:44:50 +02:00
Ahmed Bouhuolia
477da0e7c0 feat: add local adjustment and discount properties to SaleInvoice and SaleReceipt interfaces. 2024-12-09 00:19:22 +02:00
Ahmed Bouhuolia
b9963aa241 feat: filter ledger blank entries 2024-12-09 00:01:42 +02:00
Ahmed Bouhuolia
994c441bb8 feat: add local discount and adjustment calculations to financial models and transformers
- Introduced `discountAmountLocal` and `adjustmentLocal` properties across Bill, CreditNote, SaleInvoice, SaleReceipt, and VendorCredit models to calculate amounts in local currency.
- Updated transformers for CreditNote, PurchaseInvoice, and VendorCredit to include formatted representations of local discount and adjustment amounts.
- Enhanced GL entry services to handle discount and adjustment entries for SaleReceipt and CreditNote, ensuring accurate ledger entries.
- Improved overall consistency in handling financial calculations across various models and services.
2024-12-08 18:11:03 +02:00
Ahmed Bouhuolia
0a5115fc20 feat: add paid amount attr and formatting to SaleReceipt transformer 2024-12-08 17:26:52 +02:00
Ahmed Bouhuolia
11d7a40326 fix: display adjustment in minues 2024-12-08 14:47:03 +02:00
Ahmed Bouhuolia
46719ef361 fix: discount transactions GL entries 2024-12-08 14:20:11 +02:00
Ahmed Bouhuolia
14ae978bde Merge pull request #762 from bigcapitalhq/fix-discount-adjustment-bugs
fix: discount & adjustment sale transactions bugs
2024-12-05 14:48:11 +02:00
Ahmed Bouhuolia
beec09788e fix: discount & adjustment sale transactions bugs 2024-12-05 14:47:11 +02:00
Ahmed Bouhuolia
391dc77071 Merge pull request #761 from bigcapitalhq/fix-total-lines-style
fix: total lines style
2024-12-04 15:26:25 +02:00
Ahmed Bouhuolia
38f2004e56 fix: total lines style 2024-12-04 15:24:58 +02:00
Ahmed Bouhuolia
5a5eac246b chore: update CHANGELOG.md 2024-12-04 13:36:25 +02:00
Ahmed Bouhuolia
a7bafd4f62 Merge pull request #760 from bigcapitalhq/fix-formatted-hooks
fix: update financial forms to use new formatted amount utilities and…
2024-12-04 13:24:23 +02:00
Ahmed Bouhuolia
a25ab39647 refactor: replace journal total calculations with new formatted amount hooks 2024-12-04 13:23:40 +02:00
Ahmed Bouhuolia
7dd09e2903 fix: discount and adjustment fields 2024-12-04 12:18:20 +02:00
Ahmed Bouhuolia
6ab461a212 feat: enhance discount and adjustment formatting 2024-12-04 12:00:22 +02:00
Ahmed Bouhuolia
fabc88c81a feat: add adjustment total in estimates, invoices, and receipts pdf templates 2024-12-03 23:38:27 +02:00
Ahmed Bouhuolia
3a19518440 fix: update financial forms to use new formatted amount utilities and add adjustment fields 2024-12-03 17:53:37 +02:00
Ahmed Bouhuolia
56b5e3469e Merge pull request #758 from bigcapitalhq/add-discount-to-transactions
feat: Add discount to transactions
2024-12-03 14:25:39 +02:00
Ahmed Bouhuolia
542763ddf5 feat: enhance discount and adjustment validation in Bills and Vendor Credit controllers 2024-12-03 14:22:49 +02:00
Ahmed Bouhuolia
1010d97a92 fix: discount and adjustment fields across financial forms 2024-12-03 13:54:26 +02:00
Ahmed Bouhuolia
d5dacaa988 feat: add discount and adjustment fields to email templates. 2024-12-03 13:20:19 +02:00
Ahmed Bouhuolia
154ade9647 feat: stylw tweaks of discount and adjustment in estimates, invoices, and receipts 2024-12-02 18:57:42 +02:00
Ahmed Bouhuolia
5b75fa9286 feat: link discount to mail receipts 2024-12-02 18:45:16 +02:00
Ahmed Bouhuolia
05cf94940e refactor: implementing new formatted amount hooks 2024-12-02 15:32:39 +02:00
Ahmed Bouhuolia
03b0d2519b refactor: update estimate and receipt forms to use new subtotal and total formatting utilities 2024-12-01 18:19:09 +02:00
Ahmed Bouhuolia
000c3e40e1 feat: enhance discount handling in financial forms
- Implemented discount and adjustment fields in Bill, Credit Note, Estimate, Invoice, and Receipt forms.
- Created new components for displaying discount and adjustment totals, improving clarity in financial documents.
- Updated utility functions to format discount and adjustment amounts consistently across various forms.
- Enhanced user experience by integrating discount functionality into the form context, allowing for better data management and display.

This update improves the overall functionality and user experience related to discounts in financial transactions.
2024-12-01 17:59:01 +02:00
Ahmed Bouhuolia
ffb06f5194 feat: add discount fields in sale and purchase forms 2024-11-30 18:02:50 +02:00
Ahmed Bouhuolia
73ab92e693 feat: implement discount display in various detail drawers
- Added discount amount and percentage display in Bill, Credit Note, Estimate, Invoice, Receipt, and Vendor Credit detail tables.
- Updated models to include discount-related attributes for better data handling.
- Enhanced user interface to show discount information when applicable, improving clarity in financial documents.
2024-11-30 16:01:29 +02:00
Ahmed Bouhuolia
dd1392cdc8 feat: add discount functionality to sales and purchase transactions
- Introduced discount_type and discount fields in Bills and SalesReceipts controllers.
- Updated database migrations to include discount and discount_type in estimates and credit notes tables.
- Enhanced SaleReceipt and SaleEstimate models to support discount attributes.
- Implemented formatting for discount amounts in transformers and PDF templates.
- Updated email templates to display discount information.

This commit enhances the handling of discounts across various transaction types, improving the overall functionality and user experience.
2024-11-30 14:46:56 +02:00
Ahmed Bouhuolia
17b3bbe1d8 feat: discount vendor credit 2024-11-28 18:17:47 +02:00
Ahmed Bouhuolia
e02ad1e795 feat: discount formatted attributes of sale transactions 2024-11-28 17:59:09 +02:00
Ahmed Bouhuolia
df8391201f feat: discount sale and purchase transactions 2024-11-28 11:14:16 +02:00
Ahmed Bouhuolia
aa4aaeb612 feat: add discount to sale and purchase transactions 2024-11-28 10:12:48 +02:00
Ahmed Bouhuolia
09b98664c5 Merge pull request #757 from bigcapitalhq/estimate-mail-preview
feat: estimate, receipt, credit note mail preview
2024-11-26 13:27:09 +02:00
Ahmed Bouhuolia
be46147d9a chore: add newrelic log to gitignore 2024-11-26 13:26:21 +02:00
Ahmed Bouhuolia
1fe7d58c8c chore: doc comments 2024-11-26 13:25:56 +02:00
Ahmed Bouhuolia
4f57782be4 feat: change default mail template messages 2024-11-26 13:15:15 +02:00
Ahmed Bouhuolia
7b5f0d3930 feat: receipt mail preview 2024-11-26 11:36:08 +02:00
Ahmed Bouhuolia
831fb9180c chore: add newrelic logs file tp gitignore 2024-11-25 16:25:49 +02:00
Ahmed Bouhuolia
2594e37dc7 feat: wip mail receipt preview 2024-11-25 16:22:40 +02:00
Ahmed Bouhuolia
ca44d6346d feat: wip preview mail receipt 2024-11-25 16:18:29 +02:00
Ahmed Bouhuolia
df41de7239 feat: payment received mail receipt 2024-11-25 11:51:13 +02:00
Ahmed Bouhuolia
459bf4cd55 feat: mail receipt preview 2024-11-24 15:40:12 +02:00
Ahmed Bouhuolia
3537a05ea2 feat: payment received mail preview 2024-11-24 13:19:26 +02:00
Ahmed Bouhuolia
da47418f17 feat: mail receipt preview 2024-11-23 20:36:46 +02:00
Ahmed Bouhuolia
d5b0546301 feat: wip send mail preview 2024-11-23 10:19:26 +02:00
Ahmed Bouhuolia
b6f3c0145f feat: payment received mail preview 2024-11-21 14:32:28 +02:00
Ahmed Bouhuolia
c5c85bdfbe feat: wip estimate send mail 2024-11-21 11:29:52 +02:00
Ahmed Bouhuolia
63a95df534 feat: payment received mail receipt preview 2024-11-20 13:03:17 +02:00
Ahmed Bouhuolia
6103f1e4c7 feat: receipt send mail preview 2024-11-20 09:42:55 +02:00
Ahmed Bouhuolia
3e591beb03 feat: estimate mail receipt 2024-11-19 22:46:58 +02:00
Ahmed Bouhuolia
c6db54175f feat: add ssr email templates rendering 2024-11-19 17:14:13 +02:00
Ahmed Bouhuolia
f7bf24acb3 feat: email templates 2024-11-19 14:21:10 +02:00
Ahmed Bouhuolia
ddffe630ff feat: email templates 2024-11-19 14:00:25 +02:00
Ahmed Bouhuolia
2c54092591 feat: wip email templates 2024-11-19 11:56:52 +02:00
Ahmed Bouhuolia
7e65f3f642 feat: estimate send mail drawer 2024-11-19 00:38:45 +02:00
Ahmed Bouhuolia
7df6aa4110 feat: wip receipt mail preview 2024-11-18 15:52:13 +02:00
Ahmed Bouhuolia
7df316aa56 feat: wip send estimate mail preview 2024-11-18 15:15:03 +02:00
Ahmed Bouhuolia
53ab40a075 feat: estimate, receipt, credit note mail preview 2024-11-17 15:45:55 +02:00
279 changed files with 11225 additions and 3265 deletions

View File

@@ -2,6 +2,66 @@
All notable changes to Bigcapital server-side will be in this file.
# [0.22.0]
* feat: estimate, receipt, credit note mail preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/757
* feat: Add discount to transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/758
* fix: update financial forms to use new formatted amount utilities and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/760
* fix: total lines style by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/761
* fix: discount & adjustment sale transactions bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/762
* fix: discount transactions GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/763
# [0.21.2]
* hotbug: upload attachments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/755
# [0.21.1]
* fix: download invoice document on payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/750
* fix: attach branding template attrs to payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/751
* fix: make manual entries adjust decimal credit/debit amounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/754
* feat: allow quantity of entries accept decimal value by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/753
# [0.21.0]
* fix: Credit and debit totals not balancing when decimal values are used by @nklmantey in https://github.com/bigcapitalhq/bigcapital/pull/722
* docs: add nklmantey as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/725
* feat: track more services events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/721
* feat: Invoice mail receipt preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/723
* fix: change the send mail button on invoice drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/730
* refactor: notification mail services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/731
* fix: attach payment link in sending invoice mail receipt by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/732
* fix: send invoice drawer layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/733
* fix: hook up cc and bcc fields to mail sender by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/734
* fix: company logo does not show up in mail receipt preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/736
* fix: change default invoice mail message by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/737
* fix: typing invoice send mail fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/738
* fix: clean up ivnoice mail receipt preview component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/739
* feat: add shared package to pdf templates to render in the server and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/735
* feat: getting invoice preview on send mail view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/740
* fix: style SSR invoice paper template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/741
* fix: send invoice receipt addresses by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/742
* fix: due invoice server invoice by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/744
* fix: `BIG-265` forgot password text by @ibutiti in https://github.com/bigcapitalhq/bigcapital/pull/745
* Crims on sv translation by @Crims-on in https://github.com/bigcapitalhq/bigcapital/pull/671
* feat: Added Spanish language to the App 🇪🇸 by @angelosorno in https://github.com/bigcapitalhq/bigcapital/pull/530
* fix: mail services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/746
* fix: company logo of the template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/747
* fix: monorepo dependencies scope by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/748
# [0.20.6]
* fix: Import category column of item resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/710
* fix: Parse the uppercase values in importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/711
* chore: Move i18nApply localization to the account transformer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/713
* fix: Sync Plaid credit card account type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/714
* fix: Sync account normal of cashflow GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/715
* feat: Add quantity column to pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/716
* feat: Pre-line invoice statements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/717
* feat: Invoice number in downloaded pdf document by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/718
* feat: Track events of pdf documents views by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/719
* fix: Customer note does not appear in pdf document by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/720
# [0.20.5]
* fix: Disable tabs of the pdf customization if the first field not filed up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/701

View File

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

View File

@@ -1,242 +0,0 @@
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}-header {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{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__header--item{
width: 50%;
}
.#{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}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{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 {
white-space: pre-line;
}
block content
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Header (invluces big title, details and logo)
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
h1(class=`${prefix}-big-title`) Estimate
//- 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}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Addresses (Group section)
div(class=`${prefix}-addresses`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
//- Table section (Line items)
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody
each line in lines
tr
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${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 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}
//- Statements section
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
div(class=`${prefix}-statement__value`) #{customerNote}

View File

@@ -1,272 +0,0 @@
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}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{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__header--item {
width: 50%;
}
.#{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}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item__description {
color: #5f6b7c;
}
.#{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 {
white-space: pre-line;
}
block content
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
//- 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}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- 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 ${prefix}-table__header--item`) #{lineItemLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
tbody
each line in lines
tr
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
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,192 +0,0 @@
extends ../PaperTemplateLayout.pug
block head
- 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}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title{
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{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(class=`${prefix}-root`)
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
div(class=`${prefix}-big-title`) Payment
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}
if showPaymentReceivedDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt="Company Logo")
div(class=`${prefix}-addresses`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
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
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(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}
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,231 +0,0 @@
extends ../PaperTemplateLayout.pug
block head
- 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}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
align-items: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
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__header--item{
width: 50%;
}
.#{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}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{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 {
white-space: pre-line;
}
block content
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
//- 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
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
//- Address Section
div(class=`${prefix}-address-section`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
//- Table Section
table(class=`${prefix}-table`)
thead(class=`${prefix}-table__header`)
tr
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody
each line in lines
tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
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__line ${prefix}-totals__line--gray-border`)
span(class=`${prefix}-totals__line__label`)= subtotalLabel
span(class=`${prefix}-totals__line__amount`)= subtotal
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
//- 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

@@ -4,6 +4,7 @@ import { check, param, query } from 'express-validator';
import {
AbilitySubject,
BillAction,
DiscountType,
IBillDTO,
IBillEditDTO,
} from '@/interfaces';
@@ -126,6 +127,11 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.landed_cost')
.optional({ nullable: true })
@@ -144,8 +150,18 @@ export default class BillsController extends BaseController {
.isNumeric()
.toInt(),
// Attachments
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// # Discount
check('discount_type')
.default(DiscountType.Amount)
.isIn([DiscountType.Amount, DiscountType.Percentage]),
check('discount').optional({ nullable: true }).isDecimal().toFloat(),
// # Adjustment
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}
@@ -188,6 +204,15 @@ export default class BillsController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// # Discount
check('discount_type')
.default(DiscountType.Amount)
.isIn([DiscountType.Amount, DiscountType.Percentage]),
check('discount').optional({ nullable: true }).isDecimal().toFloat(),
// # Adjustment
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}

View File

@@ -3,6 +3,7 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import {
AbilitySubject,
DiscountType,
IVendorCreditCreateDTO,
IVendorCreditEditDTO,
VendorCreditAction,
@@ -175,6 +176,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
@@ -183,6 +188,16 @@ export default class VendorCreditController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Discount.
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.optional({ nullable: true })
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
// Adjustment.
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}
@@ -214,6 +229,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
@@ -222,6 +241,16 @@ export default class VendorCreditController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Discount.
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.optional({ nullable: true })
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
// Adjustment.
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}

View File

@@ -4,6 +4,7 @@ import { Inject, Service } from 'typedi';
import {
AbilitySubject,
CreditNoteAction,
DiscountType,
ICreditNoteEditDTO,
ICreditNoteNewDTO,
} from '@/interfaces';
@@ -238,17 +239,32 @@ export default class PaymentReceivesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
// Attachments.
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// Discount.
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.optional({ nullable: true })
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
// Adjustment.
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}
@@ -755,8 +771,9 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req;
try {
const data =
await this.getCreditNoteStateService.getCreditNoteState(tenantId);
const data = await this.getCreditNoteStateService.getCreditNoteState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);

View File

@@ -130,9 +130,18 @@ export default class PaymentReceivesController extends BaseController {
[
...this.paymentReceiveValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.sendPaymentReceiveByMail.bind(this),
@@ -470,8 +479,9 @@ export default class PaymentReceivesController extends BaseController {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
]);
// Response in pdf format.
// Responds pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const [pdfContent, filename] =
await this.paymentReceiveApplication.getPaymentReceivePdf(
@@ -484,7 +494,15 @@ export default class PaymentReceivesController extends BaseController {
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Response in json format.
// Responds html format.
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent =
await this.paymentReceiveApplication.getPaymentReceivedHtml(
tenantId,
paymentReceiveId
);
return res.status(200).send({ htmlContent });
// Responds json format.
} else {
const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive(

View File

@@ -3,6 +3,7 @@ import { body, check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import {
AbilitySubject,
DiscountType,
ISaleEstimateDTO,
SaleEstimateAction,
SaleEstimateMailOptionsDTO,
@@ -13,11 +14,8 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SalesEstimatesController extends BaseController {
@Inject()
@@ -133,8 +131,18 @@ export default class SalesEstimatesController extends BaseController {
[
...this.validateSpecificEstimateSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
@@ -143,10 +151,10 @@ export default class SalesEstimatesController extends BaseController {
this.handleServiceErrors
);
router.get(
'/:id/mail',
'/:id/mail/state',
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.getSaleEstimateMail.bind(this)),
asyncMiddleware(this.getSaleEstimateMailState.bind(this)),
this.handleServiceErrors
);
return router;
@@ -179,6 +187,11 @@ export default class SalesEstimatesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
@@ -188,11 +201,21 @@ export default class SalesEstimatesController extends BaseController {
check('terms_conditions').optional().trim(),
check('send_to_email').optional().trim(),
// # Attachments
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
// # Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// # Discount
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.default(DiscountType.Amount)
.isIn([DiscountType.Amount, DiscountType.Percentage]),
// # Adjustment
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}
@@ -395,6 +418,7 @@ export default class SalesEstimatesController extends BaseController {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
]);
// Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
@@ -410,7 +434,14 @@ export default class SalesEstimatesController extends BaseController {
});
res.send(pdfContent);
// Retrieves estimates in json format.
} else {
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent =
await this.saleEstimatesApplication.getSaleEstimateHtml(
tenantId,
estimateId
);
return res.status(200).send({ htmlContent });
} else if (ACCEPT_TYPE.APPLICATION_JSON) {
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId,
estimateId
@@ -535,18 +566,18 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
private getSaleEstimateMail = async (
req: Request,
private getSaleEstimateMailState = async (
req: Request<{ id: number }>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: invoiceId } = req.params;
const { id: estimateId } = req.params;
try {
const data = await this.saleEstimatesApplication.getSaleEstimateMail(
const data = await this.saleEstimatesApplication.getSaleEstimateMailState(
tenantId,
invoiceId
estimateId
);
return res.status(200).send({ data });
} catch (error) {
@@ -562,8 +593,9 @@ export default class SalesEstimatesController extends BaseController {
const { tenantId } = req;
try {
const data =
await this.saleEstimatesApplication.getSaleEstimateState(tenantId);
const data = await this.saleEstimatesApplication.getSaleEstimateState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);

View File

@@ -11,6 +11,7 @@ import {
SaleInvoiceAction,
AbilitySubject,
SendInvoiceMailDTO,
DiscountType,
} from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
@@ -242,6 +243,10 @@ export default class SaleInvoicesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.tax_code')
.optional({ nullable: true })
@@ -281,6 +286,16 @@ export default class SaleInvoicesController extends BaseController {
check('payment_methods').optional({ nullable: true }).isArray(),
check('payment_methods.*.payment_integration_id').exists().toInt(),
check('payment_methods.*.enable').exists().isBoolean(),
// Discount
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.optional({ nullable: true })
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
// Adjustments
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}

View File

@@ -11,7 +11,7 @@ import {
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
import { AbilitySubject, DiscountType, SaleReceiptAction } from '@/interfaces';
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@@ -56,8 +56,18 @@ export default class SalesReceiptsController extends BaseController {
[
...this.specificReceiptValidationSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('body').isString().optional(),
body('attach_receipt').optional().isBoolean().toBoolean(),
],
@@ -154,6 +164,11 @@ export default class SalesReceiptsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.discount_type')
.default(DiscountType.Percentage)
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
@@ -166,8 +181,18 @@ export default class SalesReceiptsController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
// # Pdf template
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// # Discount
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type')
.optional({ nullable: true })
.isString()
.isIn([DiscountType.Percentage, DiscountType.Amount]),
// # Adjustment
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
];
}
@@ -353,6 +378,7 @@ export default class SalesReceiptsController extends BaseController {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
]);
// Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
@@ -368,6 +394,12 @@ export default class SalesReceiptsController extends BaseController {
});
res.send(pdfContent);
// Retrieves receipt in json format.
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent = await this.saleReceiptsApplication.getSaleReceiptHtml(
tenantId,
saleReceiptId
);
res.send({ htmlContent });
} else {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId,
@@ -392,8 +424,9 @@ export default class SalesReceiptsController extends BaseController {
// Retrieves receipt in pdf format.
try {
const data =
await this.saleReceiptsApplication.getSaleReceiptState(tenantId);
const data = await this.saleReceiptsApplication.getSaleReceiptState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
@@ -506,7 +539,7 @@ export default class SalesReceiptsController extends BaseController {
const { id: receiptId } = req.params;
try {
const data = await this.saleReceiptsApplication.getSaleReceiptMail(
const data = await this.saleReceiptsApplication.getSaleReceiptMailState(
tenantId,
receiptId
);

View File

@@ -0,0 +1,23 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.alterTable('sales_invoices', (table) => {
table.decimal('discount', 10, 2).nullable().after('credited_amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('sale_invoices', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,24 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('sales_estimates', (table) => {
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('sales_estimates', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,24 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('sales_receipts', (table) => {
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('sales_receipts', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,26 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('bills', (table) => {
// Discount.
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
// Adjustment.
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('bills', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,23 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('credit_notes', (table) => {
table.decimal('discount', 10, 2).nullable().after('exchange_rate');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('credit_notes', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,23 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('vendor_credits', (table) => {
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('vendor_credits', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.alterTable('items_entries', (table) => {
table.string('discount_type').defaultTo('percentage').after('discount');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('items_entries', (table) => {
table.dropColumn('discount_type');
});
};

View File

@@ -1,3 +1,14 @@
export const OtherExpensesAccount = {
name: 'Other Expenses',
slug: 'other-expenses',
account_type: 'other-expense',
code: '40011',
description: '',
active: 1,
index: 1,
predefined: 1,
};
export const TaxPayableAccount = {
name: 'Tax Payable',
slug: 'tax-payable',
@@ -39,8 +50,38 @@ export const StripeClearingAccount = {
code: '100020',
active: true,
index: 1,
predefined: true,
}
predefined: true,
};
export const DiscountExpenseAccount = {
name: 'Discount',
slug: 'discount',
account_type: 'other-income',
code: '40008',
active: true,
index: 1,
predefined: true,
};
export const PurchaseDiscountAccount = {
name: 'Purchase Discount',
slug: 'purchase-discount',
account_type: 'other-expense',
code: '40009',
active: true,
index: 1,
predefined: true,
};
export const OtherChargesAccount = {
name: 'Other Charges',
slug: 'other-charges',
account_type: 'other-income',
code: '40010',
active: true,
index: 1,
predefined: true,
};
export default [
{
@@ -231,17 +272,7 @@ export default [
},
// Expenses
{
name: 'Other Expenses',
slug: 'other-expenses',
account_type: 'other-expense',
parent_account_id: null,
code: '40001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
OtherExpensesAccount,
{
name: 'Cost of Goods Sold',
slug: 'cost-of-goods-sold',
@@ -358,4 +389,7 @@ export default [
},
UnearnedRevenueAccount,
PrepardExpenses,
DiscountExpenseAccount,
PurchaseDiscountAccount,
OtherChargesAccount,
];

View File

@@ -3,6 +3,7 @@ import { IDynamicListFilterDTO } from './DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IBillLandedCost } from './LandedCost';
import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface IBillDTO {
vendorId: number;
@@ -22,6 +23,13 @@ export interface IBillDTO {
projectId?: number;
isInclusiveTax?: boolean;
attachments?: AttachmentLinkDTO[];
// # Discount
discount?: number;
discountType?: DiscountType;
// # Adjustment
adjustment?: number;
}
export interface IBillEditDTO {

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import { IDynamicListFilter, IItemEntry } from '@/interfaces';
import { DiscountType, IDynamicListFilter, IItemEntry } from '@/interfaces';
import { ILedgerEntry } from './Ledger';
import { AttachmentLinkDTO } from './Attachments';
@@ -23,6 +23,9 @@ export interface ICreditNoteNewDTO {
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
}
export interface ICreditNoteEditDTO {

View File

@@ -13,14 +13,17 @@ export interface IItemEntry {
itemId: number;
description: string;
discountType?: string;
discount: number;
quantity: number;
rate: number;
amount: number;
total: number;
amountInclusingTax: number;
amountExludingTax: number;
totalExcludingTax?: number;
subtotalInclusingTax: number;
subtotalExcludingTax: number;
discountAmount: number;
landedCost: number;

View File

@@ -177,7 +177,9 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'branchId'
>;
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
export interface PaymentReceiveMailOpts extends CommonMailOptions {
attachPdf?: boolean;
}
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
@@ -239,7 +241,6 @@ export interface PaymentReceivedPdfTemplateAttributes {
paymentReceivedDateLabel: string;
}
export interface IPaymentReceivedState {
defaultTemplateId: number;
}
}

View File

@@ -3,6 +3,7 @@ import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface ISaleEstimate {
id?: number;
@@ -24,6 +25,14 @@ export interface ISaleEstimate {
branchId?: number;
warehouseId?: number;
total?: number;
totalLocal?: number;
discountAmount?: number;
discountPercentage?: number | null;
adjustment?: number;
}
export interface ISaleEstimateDTO {
customerId: number;
@@ -40,6 +49,13 @@ export interface ISaleEstimateDTO {
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[];
// # Discount
discount?: number;
discountType?: DiscountType;
// # Adjustment
adjustment?: number;
}
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {

View File

@@ -80,6 +80,18 @@ export interface ISaleInvoice {
pdfTemplateId?: number;
paymentMethods?: Array<PaymentIntegrationTransactionLink>;
adjustment?: number;
adjustmentLocal?: number | null;
discount?: number;
discountAmount?: number;
discountAmountLocal?: number | null;
}
export enum DiscountType {
Percentage = 'percentage',
Amount = 'amount',
}
export interface ISaleInvoiceDTO {
@@ -102,6 +114,13 @@ export interface ISaleInvoiceDTO {
isInclusiveTax?: boolean;
attachments?: AttachmentLinkDTO[];
// # Discount
discount?: number;
discountType?: DiscountType;
// # Adjustments
adjustments?: string;
}
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {

View File

@@ -2,6 +2,7 @@ import { Knex } from 'knex';
import { IItemEntry } from './ItemEntry';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface ISaleReceipt {
id?: number;
@@ -27,6 +28,20 @@ export interface ISaleReceipt {
localAmount?: number;
entries?: IItemEntry[];
subtotal?: number;
subtotalLocal?: number;
total?: number;
totalLocal?: number;
discountAmount: number;
discountPercentage?: number | null;
adjustment?: number;
adjustmentLocal?: number | null;
discountAmountLocal?: number | null;
}
export interface ISalesReceiptsFilter {
@@ -47,6 +62,11 @@ export interface ISaleReceiptDTO {
entries: any[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
}
export interface ISalesReceiptsService {

View File

@@ -1,4 +1,4 @@
import { IDynamicListFilter, IItemEntry, IItemEntryDTO } from '@/interfaces';
import { DiscountType, IDynamicListFilter, IItemEntry, IItemEntryDTO } from '@/interfaces';
import { Knex } from 'knex';
import { AttachmentLinkDTO } from './Attachments';
@@ -63,6 +63,11 @@ export interface IVendorCreditDTO {
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
}
export interface IVendorCreditCreateDTO extends IVendorCreditDTO {}

View File

@@ -1,12 +1,14 @@
import { Model, raw, mixin } from 'objection';
import { castArray, difference } from 'lodash';
import { castArray, defaultTo, difference } from 'lodash';
import moment from 'moment';
import * as R from 'ramda';
import TenantModel from 'models/TenantModel';
import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants';
import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
export default class Bill extends mixin(TenantModel, [
ModelSetting,
@@ -21,6 +23,11 @@ export default class Bill extends mixin(TenantModel, [
public taxAmountWithheld: number;
public exchangeRate: number;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Timestamps columns.
*/
@@ -47,6 +54,13 @@ export default class Bill extends mixin(TenantModel, [
'localAllocatedCostAmount',
'billableAmount',
'amountLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'adjustmentLocal',
'subtotal',
'subtotalLocal',
'subtotalExludingTax',
@@ -98,14 +112,53 @@ export default class Bill extends mixin(TenantModel, [
return this.taxAmountWithheld * this.exchangeRate;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount amount in local currency.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Adjustment amount in local currency.
* @returns {number | null}
*/
get adjustmentLocal() {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Invoice total. (Tax included)
* @returns {number}
*/
get total() {
return this.isInclusiveTax
? this.subtotal
: this.subtotal + this.taxAmountWithheld;
const adjustmentAmount = defaultTo(this.adjustment, 0);
return R.compose(
R.add(adjustmentAmount),
R.subtract(R.__, this.discountAmount),
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld))
)(this.subtotal);
}
/**

View File

@@ -5,12 +5,20 @@ import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants';
import ModelSearchable from './ModelSearchable';
import CreditNoteMeta from './CreditNote.Meta';
import { DiscountType } from '@/interfaces';
export default class CreditNote extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
@@ -35,8 +43,21 @@ export default class CreditNote extends mixin(TenantModel, [
'isPublished',
'isOpen',
'isClosed',
'creditsRemaining',
'creditsUsed',
'subtotal',
'subtotalLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'total',
'totalLocal',
'adjustmentLocal',
];
}
@@ -48,6 +69,72 @@ export default class CreditNote extends mixin(TenantModel, [
return this.amount * this.exchangeRate;
}
/**
* Credit note subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Credit note subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.subtotal * this.exchangeRate;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount amount in local currency.
* @returns {number}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Adjustment amount in local currency.
* @returns {number}
*/
get adjustmentLocal() {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Credit note total.
* @returns {number}
*/
get total() {
return this.subtotal - this.discountAmount + this.adjustment;
}
/**
* Credit note total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Detarmines whether the credit note is draft.
* @returns {boolean}
@@ -176,6 +263,7 @@ export default class CreditNote extends mixin(TenantModel, [
const Branch = require('models/Branch');
const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
const { PdfTemplate } = require('models/PdfTemplate');
return {
/**
@@ -266,6 +354,18 @@ export default class CreditNote extends mixin(TenantModel, [
query.where('model_ref', 'CreditNote');
},
},
/**
* Credit note may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'credit_notes.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}

View File

@@ -1,6 +1,12 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
import { DiscountType } from '@/interfaces';
// Subtotal (qty * rate) (tax inclusive)
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
// Discount (Is percentage ? amount * discount : discount)
// Total (Subtotal - Discount)
export default class ItemEntry extends TenantModel {
public taxRate: number;
@@ -8,7 +14,7 @@ export default class ItemEntry extends TenantModel {
public quantity: number;
public rate: number;
public isInclusiveTax: number;
public discountType: DiscountType;
/**
* Table name.
* @returns {string}
@@ -31,10 +37,24 @@ export default class ItemEntry extends TenantModel {
*/
static get virtualAttributes() {
return [
// Amount (qty * rate)
'amount',
'taxAmount',
'amountExludingTax',
'amountInclusingTax',
// Subtotal (qty * rate) + (tax inclusive)
'subtotalInclusingTax',
// Subtotal Tax Exclusive (Subtotal - Tax Amount)
'subtotalExcludingTax',
// Subtotal (qty * rate) + (tax inclusive)
'subtotal',
// Discount (Is percentage ? amount * discount : discount)
'discountAmount',
// Total (Subtotal - Discount)
'total',
];
}
@@ -45,7 +65,15 @@ export default class ItemEntry extends TenantModel {
* @returns {number}
*/
get total() {
return this.amountInclusingTax;
return this.subtotal - this.discountAmount;
}
/**
* Total (excluding tax).
* @returns {number}
*/
get totalExcludingTax() {
return this.subtotalExcludingTax - this.discountAmount;
}
/**
@@ -57,19 +85,27 @@ export default class ItemEntry extends TenantModel {
return this.quantity * this.rate;
}
/**
* Subtotal amount (tax inclusive).
* @returns {number}
*/
get subtotal() {
return this.subtotalInclusingTax;
}
/**
* Item entry amount including tax.
* @returns {number}
*/
get amountInclusingTax() {
get subtotalInclusingTax() {
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
}
/**
* Item entry amount excluding tax.
* Subtotal amount (tax exclusive).
* @returns {number}
*/
get amountExludingTax() {
get subtotalExcludingTax() {
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
}
@@ -78,7 +114,9 @@ export default class ItemEntry extends TenantModel {
* @returns {number}
*/
get discountAmount() {
return this.amount * (this.discount / 100);
return this.discountType === DiscountType.Percentage
? this.amount * (this.discount / 100)
: this.discount;
}
/**

View File

@@ -11,6 +11,10 @@ export default class PaymentReceive extends mixin(TenantModel, [
CustomViewBaseModel,
ModelSearchable,
]) {
amount!: number;
paymentAmount!: number;
exchangeRate!: number;
/**
* Table name.
*/
@@ -29,7 +33,7 @@ export default class PaymentReceive extends mixin(TenantModel, [
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
return ['localAmount', 'total'];
}
/**
@@ -40,6 +44,14 @@ export default class PaymentReceive extends mixin(TenantModel, [
return this.amount * this.exchangeRate;
}
/**
* Payment receive total.
* @returns {number}
*/
get total() {
return this.paymentAmount;
}
/**
* Resourcable model.
*/
@@ -57,6 +69,7 @@ export default class PaymentReceive extends mixin(TenantModel, [
const Account = require('models/Account');
const Branch = require('models/Branch');
const Document = require('models/Document');
const { PdfTemplate } = require('models/PdfTemplate');
return {
customer: {
@@ -131,6 +144,18 @@ export default class PaymentReceive extends mixin(TenantModel, [
query.where('model_ref', 'PaymentReceive');
},
},
/**
* Payment received may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'payment_receives.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}

View File

@@ -7,12 +7,30 @@ import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants';
import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
import { defaultTo } from 'lodash';
export default class SaleEstimate extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
public expirationDate!: string;
public deliveredAt!: string | null;
public approvedAt!: string | null;
public rejectedAt!: string | null;
public convertedToInvoiceId!: number | null;
public convertedToInvoiceAt!: string | null;
/**
* Table name
*/
@@ -33,6 +51,12 @@ export default class SaleEstimate extends mixin(TenantModel, [
static get virtualAttributes() {
return [
'localAmount',
'discountAmount',
'discountPercentage',
'total',
'totalLocal',
'subtotal',
'subtotalLocal',
'isDelivered',
'isExpired',
'isConvertedToInvoice',
@@ -49,6 +73,60 @@ export default class SaleEstimate extends mixin(TenantModel, [
return this.amount * this.exchangeRate;
}
/**
* Estimate subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;;
}
/**
* Estimate subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.localAmount;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage
? this.discount
: null;
}
/**
* Estimate total.
* @returns {number}
*/
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - this.discountAmount + adjustmentAmount;
}
/**
* Estimate total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Detarmines whether the sale estimate converted to sale invoice.
* @return {boolean}
@@ -184,6 +262,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const Document = require('models/Document');
const { PdfTemplate } = require('models/PdfTemplate');
return {
customer: {
@@ -252,6 +331,18 @@ export default class SaleEstimate extends mixin(TenantModel, [
query.where('model_ref', 'SaleEstimate');
},
},
/**
* Sale estimate may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'sales_estimates.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}

View File

@@ -1,5 +1,6 @@
import { mixin, Model, raw } from 'objection';
import { castArray, takeWhile } from 'lodash';
import * as R from 'ramda';
import { castArray, defaultTo, takeWhile } from 'lodash';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
@@ -7,6 +8,7 @@ import SaleInvoiceMeta from './SaleInvoice.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants';
import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
export default class SaleInvoice extends mixin(TenantModel, [
ModelSetting,
@@ -23,6 +25,9 @@ export default class SaleInvoice extends mixin(TenantModel, [
public writtenoffAt: Date;
public dueDate: Date;
public deliveredAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number | null;
/**
* Table name
@@ -67,10 +72,15 @@ export default class SaleInvoice extends mixin(TenantModel, [
'subtotalExludingTax',
'taxAmountWithheldLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'total',
'totalLocal',
'writtenoffAmountLocal',
'adjustmentLocal',
];
}
@@ -125,14 +135,52 @@ export default class SaleInvoice extends mixin(TenantModel, [
return this.taxAmountWithheld * this.exchangeRate;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Local discount amount.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Adjustment amount in local currency.
* @returns {number | null}
*/
get adjustmentLocal(): number | null {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Invoice total. (Tax included)
* @returns {number}
*/
get total() {
return this.isInclusiveTax
? this.subtotal
: this.subtotal + this.taxAmountWithheld;
const adjustmentAmount = defaultTo(this.adjustment, 0);
return R.compose(
R.add(adjustmentAmount),
R.subtract(R.__, this.discountAmount),
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld))
)(this.subtotal);
}
/**
@@ -604,7 +652,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
join: {
from: 'sales_invoices.pdfTemplateId',
to: 'pdf_templates.id',
}
},
},
};
}

View File

@@ -5,12 +5,23 @@ import SaleReceiptSettings from './SaleReceipt.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants';
import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
import { defaultTo } from 'lodash';
export default class SaleReceipt extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public closedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
@@ -29,7 +40,28 @@ export default class SaleReceipt extends mixin(TenantModel, [
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount', 'isClosed', 'isDraft'];
return [
'localAmount',
'subtotal',
'subtotalLocal',
'total',
'totalLocal',
'adjustment',
'adjustmentLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'paid',
'paidLocal',
'isClosed',
'isDraft',
];
}
/**
@@ -40,6 +72,90 @@ export default class SaleReceipt extends mixin(TenantModel, [
return this.amount * this.exchangeRate;
}
/**
* Receipt subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Receipt subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.localAmount;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount amount in local currency.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Receipt total.
* @returns {number}
*/
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - this.discountAmount + adjustmentAmount;
}
/**
* Receipt total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Adjustment amount in local currency.
* @returns {number}
*/
get adjustmentLocal() {
return this.adjustment * this.exchangeRate;
}
/**
* Receipt paid amount.
* @returns {number}
*/
get paid() {
return this.total;
}
/**
* Receipt paid amount in local currency.
* @returns {number}
*/
get paidLocal() {
return this.paid * this.exchangeRate;
}
/**
* Detarmine whether the sale receipt closed.
* @return {boolean}

View File

@@ -6,26 +6,26 @@ import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/VendorCredits/constants';
import ModelSearchable from './ModelSearchable';
import VendorCreditMeta from './VendorCredit.Meta';
import { DiscountType } from '@/interfaces';
export default class VendorCredit extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
static get tableName() {
return 'vendor_credits';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
}
/**
* Vendor credit amount in local currency.
* @returns {number}
@@ -34,6 +34,72 @@ export default class VendorCredit extends mixin(TenantModel, [
return this.amount * this.exchangeRate;
}
/**
* Vendor credit subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Vendor credit subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.subtotal * this.exchangeRate;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount() {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount amount in local currency.
* @returns {number | null}
*/
get discountAmountLocal() {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Adjustment amount in local currency.
* @returns {number | null}
*/
get adjustmentLocal() {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Vendor credit total.
* @returns {number}
*/
get total() {
return this.subtotal - this.discountAmount + this.adjustment;
}
/**
* Vendor credit total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Model modifiers.
*/
@@ -120,7 +186,24 @@ export default class VendorCredit extends mixin(TenantModel, [
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isDraft', 'isPublished', 'isOpen', 'isClosed', 'creditsRemaining'];
return [
'isDraft',
'isPublished',
'isOpen',
'isClosed',
'creditsRemaining',
'localAmount',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'adjustmentLocal',
'total',
'totalLocal',
];
}
/**

View File

@@ -3,7 +3,11 @@ import TenantRepository from '@/repositories/TenantRepository';
import { IAccount } from '@/interfaces';
import { Knex } from 'knex';
import {
DiscountExpenseAccount,
OtherChargesAccount,
OtherExpensesAccount,
PrepardExpenses,
PurchaseDiscountAccount,
StripeClearingAccount,
TaxPayableAccount,
UnearnedRevenueAccount,
@@ -188,9 +192,9 @@ export default class AccountRepository extends TenantRepository {
/**
* Finds or creates the unearned revenue.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateUnearnedRevenue(
extraAttrs: Record<string, string> = {},
@@ -219,9 +223,9 @@ export default class AccountRepository extends TenantRepository {
/**
* Finds or creates the prepard expenses account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreatePrepardExpenses(
extraAttrs: Record<string, string> = {},
@@ -249,12 +253,11 @@ 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
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateStripeClearing(
extraAttrs: Record<string, string> = {},
@@ -281,4 +284,114 @@ export default class AccountRepository extends TenantRepository {
}
return result;
}
/**
* Finds or creates the discount expense account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateDiscountAccount(
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: DiscountExpenseAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...DiscountExpenseAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreatePurchaseDiscountAccount(
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: PurchaseDiscountAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...PurchaseDiscountAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreateOtherChargesAccount(
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: OtherChargesAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...OtherChargesAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreateOtherExpensesAccount(
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: OtherExpensesAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...OtherExpensesAccount,
..._extraAttrs,
});
}
return result;
}
}

View File

@@ -238,6 +238,7 @@ export default class Ledger implements ILedger {
return {
credit: defaultTo(entry.credit, 0),
debit: defaultTo(entry.debit, 0),
exchangeRate: entry.exchangeRate,
currencyCode: entry.currencyCode,

View File

@@ -9,15 +9,18 @@ import {
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformLedgerEntryToTransaction } from './utils';
// Filter the blank entries.
const filterBlankEntry = (entry: ILedgerEntry) => Boolean(entry.credit || entry.debit);
@Service()
export class LedgerEntriesStorage {
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
/**
* Saves entries of the given ledger.
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} knex
* @param {number} tenantId
* @param {ILedger} ledger
* @param {Knex.Transaction} knex
* @returns {Promise<void>}
*/
public saveEntries = async (
@@ -26,7 +29,7 @@ export class LedgerEntriesStorage {
trx?: Knex.Transaction
) => {
const saveEntryQueue = async.queue(this.saveEntryTask, 10);
const entries = ledger.getEntries();
const entries = ledger.filter(filterBlankEntry).getEntries();
entries.forEach((entry) => {
saveEntryQueue.push({ tenantId, entry, trx });
@@ -57,8 +60,8 @@ export class LedgerEntriesStorage {
/**
* Saves the ledger entry to the account transactions repository.
* @param {number} tenantId
* @param {ILedgerEntry} entry
* @param {number} tenantId
* @param {ILedgerEntry} entry
* @returns {Promise<void>}
*/
private saveEntry = async (

View File

@@ -12,6 +12,7 @@ import {
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import { SaleReceipt } from '@/models';
@Service()
export default class CreditNoteGLEntries {
@@ -29,11 +30,15 @@ export default class CreditNoteGLEntries {
*/
private getCreditNoteGLedger = (
creditNote: ICreditNote,
receivableAccount: number
receivableAccount: number,
discountAccount: number,
adjustmentAccount: number
): Ledger => {
const ledgerEntries = this.getCreditNoteGLEntries(
creditNote,
receivableAccount
receivableAccount,
discountAccount,
adjustmentAccount
);
return new Ledger(ledgerEntries);
};
@@ -49,9 +54,16 @@ export default class CreditNoteGLEntries {
tenantId: number,
creditNote: ICreditNote,
payableAccount: number,
discountAccount: number,
adjustmentAccount: number,
trx?: Knex.Transaction
): Promise<void> => {
const ledger = this.getCreditNoteGLedger(creditNote, payableAccount);
const ledger = this.getCreditNoteGLedger(
creditNote,
payableAccount,
discountAccount,
adjustmentAccount
);
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
@@ -98,11 +110,18 @@ export default class CreditNoteGLEntries {
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
creditNoteWithItems.currencyCode
);
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{}
);
const adjustmentAccount =
await accountRepository.findOrCreateOtherChargesAccount({});
// Saves the credit note GL entries.
await this.saveCreditNoteGLEntries(
tenantId,
creditNoteWithItems,
ARAccount.id,
discountAccount.id,
adjustmentAccount.id,
trx
);
};
@@ -169,7 +188,7 @@ export default class CreditNoteGLEntries {
return {
...commonEntry,
credit: creditNote.localAmount,
credit: creditNote.totalLocal,
accountId: ARAccountId,
contactId: creditNote.customerId,
index: 1,
@@ -191,11 +210,11 @@ export default class CreditNoteGLEntries {
index: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
const localAmount = entry.amount * creditNote.exchangeRate;
const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate;
return {
...commonEntry,
debit: localAmount,
debit: totalLocal,
accountId: entry.sellAccountId || entry.item.sellAccountId,
note: entry.description,
index: index + 2,
@@ -206,6 +225,50 @@ export default class CreditNoteGLEntries {
}
);
/**
* Retrieves the credit note discount entry.
* @param {ICreditNote} creditNote
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getDiscountEntry = (
creditNote: ICreditNote,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
return {
...commonEntry,
credit: creditNote.discountAmountLocal,
accountId: discountAccountId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the credit note adjustment entry.
* @param {ICreditNote} creditNote
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
creditNote: ICreditNote,
adjustmentAccountId: number
): ILedgerEntry => {
const commonEntry = this.getCreditNoteCommonEntry(creditNote);
const adjustmentAmount = Math.abs(creditNote.adjustmentLocal);
return {
...commonEntry,
credit: creditNote.adjustmentLocal < 0 ? adjustmentAmount : 0,
debit: creditNote.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: adjustmentAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieve the credit note GL entries.
* @param {ICreditNote} creditNote - Credit note.
@@ -214,13 +277,21 @@ export default class CreditNoteGLEntries {
*/
public getCreditNoteGLEntries = (
creditNote: ICreditNote,
ARAccountId: number
ARAccountId: number,
discountAccountId: number,
adjustmentAccountId: number
): ILedgerEntry[] => {
const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId);
const getItemEntry = this.getCreditNoteItemEntry(creditNote);
const itemsEntries = creditNote.entries.map(getItemEntry);
return [AREntry, ...itemsEntries];
const discountEntry = this.getDiscountEntry(creditNote, discountAccountId);
const adjustmentEntry = this.getAdjustmentEntry(
creditNote,
adjustmentAccountId
);
return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries];
};
}

View File

@@ -18,6 +18,18 @@ export class CreditNoteTransformer extends Transformer {
'formattedAmount',
'formattedCreditsUsed',
'formattedSubtotal',
'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'entries',
'attachments',
];
@@ -34,7 +46,7 @@ export class CreditNoteTransformer extends Transformer {
/**
* Retrieve formatted created at date.
* @param credit
* @param credit
* @returns {string}
*/
protected formattedCreatedAt = (credit): string => {
@@ -83,6 +95,85 @@ export class CreditNoteTransformer extends Transformer {
return formatNumber(credit.amount, { money: false });
};
/**
* Retrieves formatted discount amount.
* @param credit
* @returns {string}
*/
protected discountAmountFormatted = (credit): string => {
return formatNumber(credit.discountAmount, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {ICreditNote} credit
* @returns {string}
*/
protected discountAmountLocalFormatted = (credit): string => {
return formatNumber(credit.discountAmountLocal, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param credit
* @returns {string}
*/
protected discountPercentageFormatted = (credit): string => {
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
};
/**
* Retrieves formatted adjustment amount.
* @param credit
* @returns {string}
*/
protected adjustmentFormatted = (credit): string => {
return this.formatMoney(credit.adjustment, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {ICreditNote} credit
* @returns {string}
*/
protected adjustmentLocalFormatted = (credit): string => {
return formatNumber(credit.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted total.
* @param credit
* @returns {string}
*/
protected totalFormatted = (credit): string => {
return formatNumber(credit.total, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the formatted total in local currency.
* @param credit
* @returns {string}
*/
protected totalLocalFormatted = (credit): string => {
return formatNumber(credit.totalLocal, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ICreditNote} credit

View File

@@ -24,7 +24,7 @@ export class CreateItem {
/**
* Authorize the creating item.
* @param {number} tenantId
* @param {number} tenantId
* @param {IItemDTO} itemDTO
*/
async authorize(tenantId: number, itemDTO: IItemDTO) {

View File

@@ -52,10 +52,18 @@ export class BillGLEntries {
{},
trx
);
// Find or create other expenses account.
const otherExpensesAccount =
await accountRepository.findOrCreateOtherExpensesAccount({}, trx);
// Find or create purchase discount account.
const purchaseDiscountAccount =
await accountRepository.findOrCreatePurchaseDiscountAccount({}, trx);
const billLedger = this.getBillLedger(
bill,
APAccount.id,
taxPayableAccount.id
taxPayableAccount.id,
purchaseDiscountAccount.id,
otherExpensesAccount.id
);
// Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx);
@@ -102,6 +110,7 @@ export class BillGLEntries {
return {
debit: 0,
credit: 0,
currencyCode: bill.currencyCode,
exchangeRate: bill.exchangeRate || 1,
@@ -130,13 +139,12 @@ export class BillGLEntries {
private getBillItemEntry = R.curry(
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
const localAmount = bill.exchangeRate * entry.amountExludingTax;
const totalLocal = bill.exchangeRate * entry.totalExcludingTax;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return {
...commonJournalMeta,
debit: localAmount + landedCostAmount,
debit: totalLocal + landedCostAmount,
accountId:
['inventory'].indexOf(entry.item.type) !== -1
? entry.item.inventoryAccountId
@@ -240,6 +248,52 @@ export class BillGLEntries {
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the purchase discount GL entry.
* @param {IBill} bill
* @param {number} purchaseDiscountAccountId
* @returns {ILedgerEntry}
*/
private getPurchaseDiscountEntry = (
bill: IBill,
purchaseDiscountAccountId: number
) => {
const commonEntry = this.getBillCommonEntry(bill);
return {
...commonEntry,
credit: bill.discountAmountLocal,
accountId: purchaseDiscountAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the purchase other charges GL entry.
* @param {IBill} bill
* @param {number} otherChargesAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
bill: IBill,
otherExpensesAccountId: number
) => {
const commonEntry = this.getBillCommonEntry(bill);
const adjustmentAmount = Math.abs(bill.adjustmentLocal);
return {
...commonEntry,
debit: bill.adjustmentLocal > 0 ? adjustmentAmount : 0,
credit: bill.adjustmentLocal < 0 ? adjustmentAmount : 0,
accountId: otherExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the given bill GL entries.
* @param {IBill} bill
@@ -249,7 +303,9 @@ export class BillGLEntries {
private getBillGLEntries = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
@@ -262,8 +318,24 @@ export class BillGLEntries {
);
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
const purchaseDiscountEntry = this.getPurchaseDiscountEntry(
bill,
purchaseDiscountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
bill,
otherExpensesAccountId
);
// Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
return [
payableEntry,
...itemsEntries,
...landedCostEntries,
...taxEntries,
purchaseDiscountEntry,
adjustmentEntry,
];
};
/**
@@ -275,14 +347,17 @@ export class BillGLEntries {
private getBillLedger = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
) => {
const entries = this.getBillGLEntries(
bill,
payableAccountId,
taxPayableAccountId
taxPayableAccountId,
purchaseDiscountAccountId,
otherExpensesAccountId
);
return new Ledger(entries);
};
}

View File

@@ -20,10 +20,21 @@ export class PurchaseInvoiceTransformer extends Transformer {
'formattedBalance',
'formattedDueAmount',
'formattedExchangeRate',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted',
'discountAmountFormatted',
'discountAmountLocalFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
@@ -160,6 +171,63 @@ export class PurchaseInvoiceTransformer extends Transformer {
});
};
/**
* Retrieves the formatted discount amount.
* @param {IBill} bill
* @returns {string}
*/
protected discountAmountFormatted = (bill): string => {
return formatNumber(bill.discountAmount, {
currencyCode: bill.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {IBill} bill
* @returns {string}
*/
protected discountAmountLocalFormatted = (bill): string => {
return formatNumber(bill.discountAmountLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount percentage.
* @param {IBill} bill
* @returns {string}
*/
protected discountPercentageFormatted = (bill): string => {
return bill.discountPercentage ? `${bill.discountPercentage}%` : '';
};
/**
* Retrieves the formatted adjustment amount.
* @param {IBill} bill
* @returns {string}
*/
protected adjustmentFormatted = (bill): string => {
return formatNumber(bill.adjustment, {
currencyCode: bill.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {IBill} bill
* @returns {string}
*/
protected adjustmentLocalFormatted = (bill): string => {
return formatNumber(bill.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the total formatted.
* @param {IBill} bill

View File

@@ -56,7 +56,7 @@ export default class VendorCreditGLEntries {
return {
...commonEntity,
debit: vendorCredit.localAmount,
debit: vendorCredit.totalLocal,
accountId: APAccountId,
contactId: vendorCredit.vendorId,
accountNormal: AccountNormal.CREDIT,
@@ -77,11 +77,11 @@ export default class VendorCreditGLEntries {
index: number
): ILedgerEntry => {
const commonEntity = this.getVendorCreditGLCommonEntry(vendorCredit);
const localAmount = entry.amount * vendorCredit.exchangeRate;
const totalLocal = entry.totalExcludingTax * vendorCredit.exchangeRate;
return {
...commonEntity,
credit: localAmount,
credit: totalLocal,
index: index + 2,
itemId: entry.itemId,
itemQuantity: entry.quantity,
@@ -94,6 +94,52 @@ export default class VendorCreditGLEntries {
}
);
/**
* Retrieves the vendor credit discount GL entry.
* @param {IVendorCredit} vendorCredit
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
public getDiscountEntry = (
vendorCredit: IVendorCredit,
purchaseDiscountAccountId: number
) => {
const commonEntry = this.getVendorCreditGLCommonEntry(vendorCredit);
return {
...commonEntry,
debit: vendorCredit.discountAmountLocal,
accountId: purchaseDiscountAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the vendor credit adjustment GL entry.
* @param {IVendorCredit} vendorCredit
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
public getAdjustmentEntry = (
vendorCredit: IVendorCredit,
otherExpensesAccountId: number
) => {
const commonEntry = this.getVendorCreditGLCommonEntry(vendorCredit);
const adjustmentAmount = Math.abs(vendorCredit.adjustmentLocal);
return {
...commonEntry,
credit: vendorCredit.adjustmentLocal > 0 ? adjustmentAmount : 0,
debit: vendorCredit.adjustmentLocal < 0 ? adjustmentAmount : 0,
accountId: otherExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieve the vendor credit GL entries.
* @param {IVendorCredit} vendorCredit -
@@ -102,7 +148,9 @@ export default class VendorCreditGLEntries {
*/
public getVendorCreditGLEntries = (
vendorCredit: IVendorCredit,
payableAccountId: number
payableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getVendorCreditPayableGLEntry(
vendorCredit,
@@ -111,7 +159,15 @@ export default class VendorCreditGLEntries {
const getItemEntry = this.getVendorCreditGLItemEntry(vendorCredit);
const itemsEntries = vendorCredit.entries.map(getItemEntry);
return [payableEntry, ...itemsEntries];
const discountEntry = this.getDiscountEntry(
vendorCredit,
purchaseDiscountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
vendorCredit,
otherExpensesAccountId
);
return [payableEntry, discountEntry, adjustmentEntry, ...itemsEntries];
};
/**
@@ -158,10 +214,17 @@ export default class VendorCreditGLEntries {
{},
trx
);
const purchaseDiscountAccount =
await accountRepository.findOrCreatePurchaseDiscountAccount({}, trx);
const otherExpensesAccount =
await accountRepository.findOrCreateOtherExpensesAccount({}, trx);
// Saves the vendor credit GL entries.
const ledgerEntries = this.getVendorCreditGLEntries(
vendorCredit,
APAccount.id
APAccount.id,
purchaseDiscountAccount.id,
otherExpensesAccount.id
);
const ledger = new Ledger(ledgerEntries);

View File

@@ -17,6 +17,15 @@ export class VendorCreditTransformer extends Transformer {
'formattedCreatedAt',
'formattedCreditsRemaining',
'formattedInvoicedAmount',
'discountAmountFormatted',
'discountPercentageFormatted',
'discountAmountLocalFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'totalFormatted',
'entries',
'attachments',
];
@@ -33,7 +42,7 @@ export class VendorCreditTransformer extends Transformer {
/**
* Retireve formatted created at date.
* @param vendorCredit
* @param vendorCredit
* @returns {string}
*/
protected formattedCreatedAt = (vendorCredit): string => {
@@ -71,6 +80,63 @@ export class VendorCreditTransformer extends Transformer {
});
};
/**
* Retrieves the formatted discount amount.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected discountAmountFormatted = (credit): string => {
return formatNumber(credit.discountAmount, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount amount in local currency.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected discountAmountLocalFormatted = (credit): string => {
return formatNumber(credit.discountAmountLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted discount percentage.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected discountPercentageFormatted = (credit): string => {
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
};
/**
* Retrieves the formatted adjustment amount.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected adjustmentFormatted = (credit): string => {
return formatNumber(credit.adjustment, {
currencyCode: credit.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted adjustment amount in local currency.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected adjustmentLocalFormatted = (credit): string => {
return formatNumber(credit.adjustmentLocal, {
currencyCode: this.context.organization.baseCurrency,
excerptZero: true,
});
};
/**
* Retrieves the formatted invoiced amount.
* @param credit
@@ -82,6 +148,15 @@ export class VendorCreditTransformer extends Transformer {
});
};
/**
* Retrieves the formatted total.
* @param {IVendorCredit} credit
* @returns {string}
*/
protected totalFormatted = (credit) => {
return formatNumber(credit.total, { currencyCode: credit.currencyCode });
};
/**
* Retrieves the entries of the bill.
* @param {IVendorCredit} vendorCredit

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import {
renderEstimateEmailTemplate,
EstimatePaymentEmailProps,
} from '@bigcapital/email-components';
import { GetSaleEstimate } from './GetSaleEstimate';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetEstimateMailTemplateAttributesTransformer } from './GetEstimateMailTemplateAttributesTransformer';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
@Service()
export class GetEstimateMailTemplate {
@Inject()
private getEstimateService: GetSaleEstimate;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
/**
* Retrieves the mail template attributes of the given estimate.
* Estimate template attributes are composed of the estimate and branding template attributes.
* @param {number} tenantId
* @param {number} estimateId - Estimate id.
* @returns {Promise<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
estimateId: number
): Promise<EstimatePaymentEmailProps> {
const estimate = await this.getEstimateService.getEstimate(
tenantId,
estimateId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
estimate.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
estimate,
new GetEstimateMailTemplateAttributesTransformer(),
{
estimate,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Rertieves the mail template html content.
* @param {number} tenantId
* @param {number} estimateId
* @param overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
estimateId: number,
overrideAttributes?: Partial<any>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
estimateId
);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderEstimateEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,205 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetEstimateMailTemplateAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'estimateAmount',
'primaryColor',
'estimateAmount',
'estimateMessage',
'dueDate',
'dueDateLabel',
'estimateNumber',
'estimateNumberLabel',
'total',
'totalLabel',
'subtotal',
'subtotalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'dueAmount',
'dueAmountLabel',
'viewEstimateButtonLabel',
'viewEstimateButtonUrl',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color.
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Estimate number.
* @returns {string}
*/
public estimateNumber(): string {
return this.options.estimate.estimateNumber;
}
/**
* Estimate number label.
* @returns {string}
*/
public estimateNumberLabel(): string {
return 'Estimate No: {estimateNumber}';
}
/**
* Expiration date.
* @returns {string}
*/
public expirationDate(): string {
return this.options.estimate.formattedExpirationDate;
}
/**
* Expiration date label.
* @returns {string}
*/
public expirationDateLabel(): string {
return 'Expiration Date: {expirationDate}';
}
/**
* Estimate total.
*/
public total(): string {
return this.options.estimate.totalFormatted;
}
/**
* Estimate total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Estimate discount.
* @returns {string}
*/
public discount(): string {
return this.options.estimate?.discountAmountFormatted;
}
/**
* Estimate discount label.
* @returns {string}
*/
public discountLabel(): string {
return 'Discount';
}
/**
* Estimate adjustment.
* @returns {string}
*/
public adjustment(): string {
return this.options.estimate?.adjustmentFormatted;
}
/**
* Estimate adjustment label.
* @returns {string}
*/
public adjustmentLabel(): string {
return 'Adjustment';
}
/**
* Estimate subtotal.
*/
public subtotal(): string {
return this.options.estimate.formattedSubtotal;
}
/**
* Estimate subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Estimate mail items attributes.
*/
public items(): any[] {
return this.item(
this.options.estimate.entries,
new GetEstimateMailTemplateEntryAttributesTransformer()
);
}
}
class GetEstimateMailTemplateEntryAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return ['label', 'quantity', 'rate', 'total'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public label(entry): string {
return entry?.item?.name;
}
public quantity(entry): string {
return entry?.quantity;
}
public rate(entry): string {
return entry?.rateFormatted;
}
public total(entry): string {
return entry?.totalFormatted;
}
}

View File

@@ -0,0 +1,52 @@
import { Inject, Service } from 'typedi';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetSaleEstimateMailStateTransformer } from './GetSaleEstimateMailStateTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetSaleEstimateMailState {
@Inject()
private estimateMail: SendSaleEstimateMail;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the estimate mail state of the given sale estimate.
* Estimate mail state includes the mail options, branding attributes and the estimate details.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailState>}
*/
async getEstimateMailState(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailState> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const saleEstimate = await SaleEstimate.query()
.findById(saleEstimateId)
.withGraphFetched('customer')
.withGraphFetched('entries.item')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions = await this.estimateMail.getMailOptions(
tenantId,
saleEstimateId
);
const transformed = await this.transformer.transform(
tenantId,
saleEstimate,
new GetSaleEstimateMailStateTransformer(),
{
mailOptions,
}
);
return transformed;
}
}

View File

@@ -0,0 +1,180 @@
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer {
public excludeAttributes = (): string[] => {
return ['*'];
};
public includeAttributes = (): string[] => {
return [
'estimateDate',
'estimateDateFormatted',
'expirationDate',
'expirationDateFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'adjustmentLabel',
'estimateNumber',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the invoice.
* @returns {string}
*/
protected customerName = (invoice) => {
return invoice.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (invoice) => {
return invoice.pdfTemplate?.companyLogoUri || null;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (invoice) => {
return invoice.pdfTemplate?.attributes?.primaryColor || null;
};
/**
* Retrieves the estimate number.
*/
protected estimateDateFormatted = (estimate) => {
return this.formattedEstimateDate(estimate);
};
/**
* Retrieves the expiration date of the estimate.
* @param estimate
* @returns {string}
*/
protected expirationDateFormatted = (estimate) => {
return this.formattedExpirationDate(estimate);
};
/**
* Retrieves the total amount of the estimate.
* @param estimate
* @returns
*/
protected total(estimate) {
return estimate.amount;
}
/**
* Retrieves the subtotal amount of the estimate.
* @param estimate
* @returns {number}
*/
protected subtotal(estimate) {
return estimate.amount;
}
/**
* Retrieves the discount label of the estimate.
* @param estimate
* @returns {string}
*/
protected discountLabel(estimate) {
return estimate.discountType === 'percentage'
? `Discount [${estimate.discountPercentageFormatted}]`
: 'Discount';
}
/**
* Retrieves the formatted subtotal of the estimate.
* @param estimate
* @returns {string}
*/
protected subtotalFormatted = (estimate) => {
return this.formattedSubtotal(estimate);
};
/**
* Retrieves the estimate entries.
* @param invoice
* @returns {Array}
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
new GetSaleEstimateMailStateEntryTransformer(),
{
currencyCode: invoice.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleEstimateMailStateEntryTransformer extends ItemEntryTransformer {
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Item name.
* @param entry
* @returns
*/
public name = (entry) => {
return entry.item.name;
};
public includeAttributes = (): string[] => {
return [
'name',
'quantity',
'unitPrice',
'unitPriceFormatted',
'total',
'totalFormatted',
];
};
}

View File

@@ -18,6 +18,11 @@ export class SaleEstimateTransfromer extends Transformer {
'formattedDeliveredAtDate',
'formattedApprovedAtDate',
'formattedRejectedAtDate',
'discountAmountFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'totalFormatted',
'totalLocalFormatted',
'formattedCreatedAt',
'entries',
'attachments',
@@ -98,6 +103,62 @@ export class SaleEstimateTransfromer extends Transformer {
return formatNumber(estimate.amount, { money: false });
};
/**
* Retrieves formatted discount amount.
* @param estimate
* @returns {string}
*/
protected discountAmountFormatted = (estimate: ISaleEstimate): string => {
return formatNumber(estimate.discountAmount, {
currencyCode: estimate.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param estimate
* @returns {string}
*/
protected discountPercentageFormatted = (estimate: ISaleEstimate): string => {
return estimate.discountPercentage
? `${estimate.discountPercentage}%`
: '';
};
/**
* Retrieves formatted adjustment amount.
* @param estimate
* @returns {string}
*/
protected adjustmentFormatted = (estimate: ISaleEstimate): string => {
return this.formatMoney(estimate.adjustment, {
currencyCode: estimate.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the formatted estimate total.
* @returns {string}
*/
protected totalFormatted = (estimate: ISaleEstimate): string => {
return this.formatMoney(estimate.total, {
currencyCode: estimate.currencyCode,
});
};
/**
* Retrieves the formatted estimate total in local currency.
* @param estimate
* @returns {string}
*/
protected totalLocalFormatted = (estimate: ISaleEstimate): string => {
return this.formatMoney(estimate.totalLocal, {
currencyCode: estimate.currencyCode,
});
};
/**
* Retrieves the entries of the sale estimate.
* @param {ISaleEstimate} estimate

View File

@@ -21,6 +21,7 @@ import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import { GetSaleEstimateState } from './GetSaleEstimateState';
import { GetSaleEstimateMailState } from './GetSaleEstimateMailState';
@Service()
export class SaleEstimatesApplication {
@@ -57,6 +58,9 @@ export class SaleEstimatesApplication {
@Inject()
private sendEstimateMailService: SendSaleEstimateMail;
@Inject()
private getSaleEstimateMailStateService: GetSaleEstimateMailState;
@Inject()
private getSaleEstimateStateService: GetSaleEstimateState;
@@ -220,6 +224,18 @@ export class SaleEstimatesApplication {
);
}
/**
* Retrieve the HTML content of the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
*/
public getSaleEstimateHtml(tenantId: number, saleEstimateId: number) {
return this.saleEstimatesPdfService.saleEstimateHtml(
tenantId,
saleEstimateId
);
}
/**
* Send the reminder mail of the given sale estimate.
* @param {number} tenantId
@@ -238,6 +254,22 @@ export class SaleEstimatesApplication {
);
}
/**
* Retrieves the sale estimate mail state.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailOptions>}
*/
public getSaleEstimateMailState(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailOptions> {
return this.getSaleEstimateMailStateService.getEstimateMailState(
tenantId,
saleEstimateId
);
}
/**
* Retrieves the default mail options of the given sale estimate.
* @param {number} tenantId

View File

@@ -1,13 +1,12 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleEstimate } from './GetSaleEstimate';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
import { transformEstimateToPdfTemplate } from './utils';
import { EstimatePdfBrandingAttributes } from './constants';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { renderEstimatePaperTemplateHtml, EstimatePaperTemplateProps } from '@bigcapital/pdf-templates';
@Service()
export class SaleEstimatesPdf {
@@ -17,9 +16,6 @@ export class SaleEstimatesPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getSaleEstimate: GetSaleEstimate;
@@ -29,6 +25,22 @@ export class SaleEstimatesPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve sale estimate html content.
* @param {number} tenantId -
* @param {number} invoiceId -
*/
public async saleEstimateHtml(
tenantId: number,
estimateId: number
): Promise<string> {
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
estimateId
);
return renderEstimatePaperTemplateHtml({ ...brandingAttributes });
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -42,15 +54,10 @@ export class SaleEstimatesPdf {
tenantId,
saleEstimateId
);
const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId,
saleEstimateId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/estimate-regular',
brandingAttributes
);
// Retireves the sale estimate html.
const htmlContent = await this.saleEstimateHtml(tenantId, saleEstimateId);
// Converts the html content to pdf.
const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
@@ -88,7 +95,7 @@ export class SaleEstimatesPdf {
async getEstimateBrandingAttributes(
tenantId: number,
estimateId: number
): Promise<EstimatePdfBrandingAttributes> {
): Promise<EstimatePaperTemplateProps> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const saleEstimate = await this.getSaleEstimate.getEstimate(
tenantId,

View File

@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformEstimateToMailDataArgs } from './utils';
import { GetEstimateMailTemplate } from './GetEstimateMailTemplate';
@Service()
export class SendSaleEstimateMail {
@@ -32,12 +33,15 @@ export class SendSaleEstimateMail {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject('agenda')
private agenda: any;
@Inject()
private getEstimateMailTemplate: GetEstimateMailTemplate;
@Inject()
private eventPublisher: EventPublisher;
@Inject('agenda')
private agenda: any;
/**
* Triggers the reminder mail of the given sale estimate.
* @param {number} tenantId -
@@ -76,7 +80,13 @@ export class SendSaleEstimateMail {
tenantId,
estimateId
);
return transformEstimateToMailDataArgs(estimate);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
return {
...commonArgs,
...transformEstimateToMailDataArgs(estimate),
};
};
/**
@@ -132,9 +142,45 @@ export class SendSaleEstimateMail {
mailOptions,
formatterArgs
);
return { ...formattedOptions };
// Retrieves the estimate mail template.
const message = await this.getEstimateMailTemplate.getMailTemplate(
tenantId,
saleEstimateId,
{
message: formattedOptions.message,
preview: formattedOptions.message,
}
);
return { ...formattedOptions, message };
};
/**
* Retrieves the formatted mail options.
* @param {number} tenantId
* @param {number} saleEstimateId
* @param {SaleEstimateMailOptionsDTO} messageOptions
* @returns
*/
public async getFormattedMailOptions(
tenantId: number,
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO
): Promise<SaleEstimateMailOptions> {
const defaultMessageOptions = await this.getMailOptions(
tenantId,
saleEstimateId
);
const parsedMessageOptions = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOptions
);
return this.formatMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
);
}
/**
* Sends the mail notification of the given sale estimate.
* @param {number} tenantId
@@ -147,20 +193,10 @@ export class SendSaleEstimateMail {
saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO
): Promise<void> {
const localMessageOpts = await this.getMailOptions(
tenantId,
saleEstimateId
);
// Overrides and validates the given mail options.
const parsedMessageOptions = mergeAndValidateMailOptions(
localMessageOpts,
messageOptions
) as SaleEstimateMailOptions;
const formattedOptions = await this.formatMailOptions(
const formattedOptions = await this.getFormattedMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
messageOptions
);
const mail = new Mail()
.setSubject(formattedOptions.subject)
@@ -182,7 +218,6 @@ export class SendSaleEstimateMail {
},
]);
}
const eventPayload = {
tenantId,
saleEstimateId,

View File

@@ -1,18 +1,17 @@
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
'Estimate {Estimate Number} is awaiting your approval';
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
<p>
Estimate <strong>#{Estimate Number}</strong><br />
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
Amount : <strong>{Estimate Amount}</strong></br />
</p>
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `Hi {Customer Name},
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Here's estimate # {Estimate Number} for {Estimate Amount}
This estimate is valid until {Estimate Expiration Date}, and were happy to discuss any adjustments you or questions may have.
Please find your estimate attached to this email for your reference.
If you have any questions, please let us know.
Thanks,
{Company Name}`;
export const ERRORS = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
@@ -255,18 +254,27 @@ export interface EstimatePdfBrandingAttributes {
companyAddress: string;
billedToLabel: string;
// # Total
total: string;
totalLabel: string;
showTotal: boolean;
// # Discount
discount: string;
showDiscount: boolean;
discountLabel: string;
// # Subtotal
subtotal: string;
subtotalLabel: string;
showSubtotal: boolean;
// # Customer Note
showCustomerNote: boolean;
customerNote: string;
customerNoteLabel: string;
// # Terms & Conditions
showTermsConditions: boolean;
termsConditions: string;
termsConditionsLabel: string;

View File

@@ -1,9 +1,9 @@
import { EstimatePaperTemplateProps } from '@bigcapital/pdf-templates';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { EstimatePdfBrandingAttributes } from './constants';
export const transformEstimateToPdfTemplate = (
estimate
): Partial<EstimatePdfBrandingAttributes> => {
): Partial<EstimatePaperTemplateProps> => {
return {
expirationDate: estimate.formattedExpirationDate,
estimateNumebr: estimate.estimateNumber,
@@ -13,13 +13,20 @@ export const transformEstimateToPdfTemplate = (
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
total: estimate.formattedSubtotal,
total: estimate.totalFormatted,
subtotal: estimate.formattedSubtotal,
adjustment: estimate.adjustmentFormatted,
customerNote: estimate.note,
termsConditions: estimate.termsConditions,
customerAddress: contactAddressTextFormat(estimate.customer),
showLineDiscount: estimate.entries.some((entry) => entry.discountFormatted),
discount: estimate.discountAmountFormatted,
discountLabel: estimate.discountPercentageFormatted
? `Discount [${estimate.discountPercentageFormatted}]`
: 'Discount',
};
};

View File

@@ -154,6 +154,6 @@ export class CommandSaleInvoiceDTOTransformer {
* @returns {number}
*/
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
return sumBy(entries, (e) => e.amount);
return sumBy(entries, (e) => e.total);
};
}

View File

@@ -23,6 +23,15 @@ export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
'invoiceNumber',
'invoiceNumberLabel',
'subtotal',
'subtotalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'total',
'totalLabel',
@@ -76,6 +85,30 @@ export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
return 'Invoice # {invoiceNumber}';
}
public subtotal(): string {
return this.options.invoice?.subtotalFormatted;
}
public subtotalLabel(): string {
return 'Subtotal';
}
public discount(): string {
return this.options.invoice?.discountAmountFormatted;
}
public discountLabel(): string {
return 'Discount';
}
public adjustment(): string {
return this.options.invoice?.adjustmentFormatted;
}
public adjustmentLabel(): string {
return 'Adjustment';
}
public total(): string {
return this.options.invoice?.totalFormatted;
}

View File

@@ -28,6 +28,15 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
'total',
'totalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'subtotal',
'subtotalFormatted',
@@ -76,6 +85,17 @@ export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
return invoice.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the discount label of the estimate.
* @param estimate
* @returns {string}
*/
protected discountLabel(invoice) {
return invoice.discountType === 'percentage'
? `Discount [${invoice.discountPercentageFormatted}]`
: 'Discount';
}
/**
*
* @param invoice

View File

@@ -44,18 +44,31 @@ export class SaleInvoiceGLEntries {
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode, {}, trx
saleInvoice.currencyCode,
{},
trx
);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
{},
trx
);
// Find or create the discount expense account.
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{},
trx
);
// Find or create the other charges account.
const otherChargesAccount =
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieves the ledger of the invoice.
const ledger = this.getInvoiceGLedger(
saleInvoice,
ARAccount.id,
taxPayableAccount.id
taxPayableAccount.id,
discountAccount.id,
otherChargesAccount.id
);
// Commits the ledger entries to the storage as UOW.
await this.ledegrRepository.commit(tenantId, ledger, trx);
@@ -107,12 +120,16 @@ export class SaleInvoiceGLEntries {
public getInvoiceGLedger = (
saleInvoice: ISaleInvoice,
ARAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
discountAccountId: number,
otherChargesAccountId: number
): ILedger => {
const entries = this.getInvoiceGLEntries(
saleInvoice,
ARAccountId,
taxPayableAccountId
taxPayableAccountId,
discountAccountId,
otherChargesAccountId
);
return new Ledger(entries);
};
@@ -127,6 +144,7 @@ export class SaleInvoiceGLEntries {
): Partial<ILedgerEntry> => ({
credit: 0,
debit: 0,
currencyCode: saleInvoice.currencyCode,
exchangeRate: saleInvoice.exchangeRate,
@@ -181,7 +199,7 @@ export class SaleInvoiceGLEntries {
index: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;
const localAmount = entry.totalExcludingTax * saleInvoice.exchangeRate;
return {
...commonEntry,
@@ -249,6 +267,50 @@ export class SaleInvoiceGLEntries {
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the invoice discount GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getInvoiceDiscountEntry = (
saleInvoice: ISaleInvoice,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
return {
...commonEntry,
debit: saleInvoice.discountAmountLocal,
accountId: discountAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
} as ILedgerEntry;
};
/**
* Retrieves the invoice adjustment GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
saleInvoice: ISaleInvoice,
otherChargesAccountId: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
const adjustmentAmount = Math.abs(saleInvoice.adjustmentLocal);
return {
...commonEntry,
debit: saleInvoice.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: saleInvoice.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: otherChargesAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the invoice GL entries.
* @param {ISaleInvoice} saleInvoice
@@ -258,7 +320,9 @@ export class SaleInvoiceGLEntries {
public getInvoiceGLEntries = (
saleInvoice: ISaleInvoice,
ARAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
discountAccountId: number,
otherChargesAccountId: number
): ILedgerEntry[] => {
const receivableEntry = this.getInvoiceReceivableEntry(
saleInvoice,
@@ -271,6 +335,20 @@ export class SaleInvoiceGLEntries {
saleInvoice,
taxPayableAccountId
);
return [receivableEntry, ...creditEntries, ...taxEntries];
const discountEntry = this.getInvoiceDiscountEntry(
saleInvoice,
discountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
saleInvoice,
otherChargesAccountId
);
return [
receivableEntry,
...creditEntries,
...taxEntries,
discountEntry,
adjustmentEntry,
];
};
}

View File

@@ -1,4 +1,4 @@
import { IItemEntry } from '@/interfaces';
import { DiscountType, IItemEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils';
@@ -8,7 +8,13 @@ export class ItemEntryTransformer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
return [
'quantityFormatted',
'rateFormatted',
'totalFormatted',
'discountFormatted',
'discountAmountFormatted',
];
};
/**
@@ -43,4 +49,34 @@ export class ItemEntryTransformer extends Transformer {
money: false,
});
};
/**
* Retrieves the formatted discount of item entry.
* @param {IItemEntry} entry
* @returns {string}
*/
protected discountFormatted = (entry: IItemEntry): string => {
if (!entry.discount) {
return '';
}
return entry.discountType === DiscountType.Percentage
? `${entry.discount}%`
: formatNumber(entry.discount, {
currencyCode: this.context.currencyCode,
money: false,
});
};
/**
* Retrieves the formatted discount amount of item entry.
* @param {IItemEntry} entry
* @returns {string}
*/
protected discountAmountFormatted = (entry: IItemEntry): string => {
return formatNumber(entry.discountAmount, {
currencyCode: this.context.currencyCode,
money: false,
excerptZero: true,
});
};
}

View File

@@ -1,15 +1,15 @@
import { Inject, Service } from 'typedi';
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
import {
renderInvoicePaperTemplateHtml,
InvoicePaperTemplateProps,
} from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleInvoice } from './GetSaleInvoice';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformInvoiceToPdfTemplate } from './utils';
import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
@Service()
export class SaleInvoicePdf {
@@ -102,7 +102,7 @@ export class SaleInvoicePdf {
async getInvoiceBrandingAttributes(
tenantId: number,
invoiceId: number
): Promise<InvoicePdfTemplateAttributes> {
): Promise<InvoicePaperTemplateProps> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const invoice = await this.getInvoiceService.getSaleInvoice(

View File

@@ -3,6 +3,7 @@ import { formatNumber } from 'utils';
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
import { ItemEntryTransformer } from './ItemEntryTransformer';
import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
import { DiscountType } from '@/interfaces';
export class SaleInvoiceTransformer extends Transformer {
/**
@@ -23,6 +24,9 @@ export class SaleInvoiceTransformer extends Transformer {
'subtotalExludingTaxFormatted',
'taxAmountWithheldFormatted',
'taxAmountWithheldLocalFormatted',
'discountAmountFormatted',
'discountPercentageFormatted',
'adjustmentFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
@@ -158,6 +162,41 @@ export class SaleInvoiceTransformer extends Transformer {
});
};
/**
* Retrieves formatted discount amount.
* @param invoice
* @returns {string}
*/
protected discountAmountFormatted = (invoice): string => {
return formatNumber(invoice.discountAmount, {
currencyCode: invoice.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param invoice
* @returns {string}
*/
protected discountPercentageFormatted = (invoice): string => {
return invoice.discountType === DiscountType.Percentage
? `${invoice.discount}%`
: '';
};
/**
* Retrieves formatted adjustment amount.
* @param invoice
* @returns {string}
*/
protected adjustmentFormatted = (invoice): string => {
return this.formatMoney(invoice.adjustment, {
currencyCode: invoice.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted total in foreign currency.
* @param invoice

View File

@@ -1,6 +1,7 @@
import { pickBy } from 'lodash';
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
import { ISaleInvoice } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { InvoicePaperTemplateProps } from '@bigcapital/pdf-templates';
export const mergePdfTemplateWithDefaultAttributes = (
brandingTemplate?: Record<string, any>,
@@ -18,7 +19,7 @@ export const mergePdfTemplateWithDefaultAttributes = (
export const transformInvoiceToPdfTemplate = (
invoice: ISaleInvoice
): Partial<InvoicePdfTemplateAttributes> => {
): Partial<InvoicePaperTemplateProps> => {
return {
dueDate: invoice.dueDateFormatted,
dateIssue: invoice.invoiceDateFormatted,
@@ -28,6 +29,11 @@ export const transformInvoiceToPdfTemplate = (
subtotal: invoice.subtotalFormatted,
paymentMade: invoice.paymentAmountFormatted,
dueAmount: invoice.dueAmountFormatted,
discount: invoice.discountAmountFormatted,
adjustment: invoice.adjustmentFormatted,
discountLabel: invoice.discountPercentageFormatted
? `Discount [${invoice.discountPercentageFormatted}]`
: 'Discount',
termsConditions: invoice.termsConditions,
statement: invoice.invoiceMessage,
@@ -37,13 +43,14 @@ export const transformInvoiceToPdfTemplate = (
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
taxes: invoice.taxes.map((tax) => ({
label: tax.name,
amount: tax.taxRateAmountFormatted,
})),
showLineDiscount: invoice.entries.some((entry) => entry.discountFormatted),
customerAddress: contactAddressTextFormat(invoice.customer),
};
};

View File

@@ -0,0 +1,52 @@
import { PaymentReceiveMailOpts } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetPaymentReceivedMailStateTransformer } from './GetPaymentReceivedMailStateTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { Inject, Service } from 'typedi';
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
@Service()
export class GetPaymentReceivedMailState {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private paymentReceivedMail: SendPaymentReceiveMailNotification;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the default payment mail options.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public getMailOptions = async (
tenantId: number,
paymentId: number
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findById(paymentId)
.withGraphFetched('customer')
.withGraphFetched('entries.invoice')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions = await this.paymentReceivedMail.getMailOptions(
tenantId,
paymentId
);
const transformed = await this.transformer.transform(
tenantId,
paymentReceive,
new GetPaymentReceivedMailStateTransformer(),
{
mailOptions,
}
);
return transformed;
};
}

View File

@@ -0,0 +1,186 @@
import { PaymentReceiveEntry } from '@/models';
import { PaymentReceiveTransfromer } from './PaymentReceivedTransformer';
import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer';
export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransfromer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'paymentDate',
'paymentDateFormatted',
'paymentAmount',
'paymentAmountFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'paymentNumber',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the payment.
* @returns {string}
*/
protected customerName = (payment) => {
return payment.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (payment) => {
return payment.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (payment) => {
return payment.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the formatted payment date.
* @returns {string}
*/
protected paymentDateFormatted = (payment) => {
return this.formatDate(payment.paymentDate);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected total = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected totalFormatted = (payment) => {
return this.formatMoney(payment.amount);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected subtotal = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected subtotalFormatted = (payment) => {
return this.formatMoney(payment.amount);
};
/**
* Retrieves the payment number.
* @param payment
* @returns {string}
*/
protected paymentNumber = (payment) => {
return payment.paymentReceiveNo;
};
/**
* Retrieves the payment entries.
* @param {IPaymentReceived} payment
* @returns {IPaymentReceivedEntry[]}
*/
protected entries = (payment) => {
return this.item(payment.entries, new GetPaymentReceivedEntryMailState());
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
export class GetPaymentReceivedEntryMailState extends PaymentReceivedEntryTransfromer {
/**
* Include these attributes to payment receive entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['paidAmount', 'invoiceNumber'];
};
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the paid amount.
* @param entry
* @returns {string}
*/
public paidAmount = (entry) => {
return this.paymentAmountFormatted(entry);
};
/**
* Retrieves the invoice number.
* @param entry
* @returns {string}
*/
public invoiceNumber = (entry) => {
return entry.invoice.invoiceNo;
};
}

View File

@@ -0,0 +1,75 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { GetPaymentReceived } from './GetPaymentReceived';
import { GetPaymentReceivedMailTemplateAttrsTransformer } from './GetPaymentReceivedMailTemplateAttrsTransformer';
import {
PaymentReceivedEmailTemplateProps,
renderPaymentReceivedEmailTemplate,
} from '@bigcapital/email-components';
@Service()
export class GetPaymentReceivedMailTemplate {
@Inject()
private getPaymentReceivedService: GetPaymentReceived;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the mail template attributes of the given payment received.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceivedId - Payment received id.
* @returns {Promise<PaymentReceivedEmailTemplateProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
paymentReceivedId: number
): Promise<PaymentReceivedEmailTemplateProps> {
const paymentReceived =
await this.getPaymentReceivedService.getPaymentReceive(
tenantId,
paymentReceivedId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
paymentReceived.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
paymentReceived,
new GetPaymentReceivedMailTemplateAttrsTransformer(),
{
paymentReceived,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @param {Partial<PaymentReceivedEmailTemplateProps>} overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
paymentReceivedId: number,
overrideAttributes?: Partial<PaymentReceivedEmailTemplateProps>
): Promise<string> {
const mailTemplateAttributes = await this.getMailTemplateAttributes(
tenantId,
paymentReceivedId
);
const mergedAttributes = {
...mailTemplateAttributes,
...overrideAttributes,
};
return renderPaymentReceivedEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,149 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetPaymentReceivedMailTemplateAttrsTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'primaryColor',
'total',
'totalLabel',
'subtotal',
'subtotalLabel',
'paymentNumberLabel',
'paymentNumber',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Total.
* @returns {string}
*/
public total(): string {
return this.options.paymentReceived.formattedAmount;
}
/**
* Total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Subtotal.
* @returns {string}
*/
public subtotal(): string {
return this.options.paymentReceived.formattedAmount;
}
/**
* Subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Payment number label.
* @returns {string}
*/
public paymentNumberLabel(): string {
return 'Payment # {paymentNumber}';
}
/**
* Payment number.
* @returns {string}
*/
public paymentNumber(): string {
return this.options.paymentReceived.paymentReceiveNumber;
}
/**
* Items.
* @returns
*/
public items() {
return this.item(
this.options.paymentReceived.entries,
new GetPaymentReceivedMailTemplateItemAttrsTransformer()
);
}
}
class GetPaymentReceivedMailTemplateItemAttrsTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = () => {
return ['label', 'total'];
};
/**
* Excluded attributes.
* @returns {string[]}
*/
public excludeAttributes = () => {
return ['*'];
};
/**
*
* @param entry
* @returns
*/
public label(entry) {
return entry.invoice.invoiceNo;
}
/**
*
* @param entry
* @returns
*/
public total(entry) {
return entry.paymentAmountFormatted;
}
}

View File

@@ -1,13 +1,13 @@
import { Inject, Service } from 'typedi';
import { renderPaymentReceivedPaperTemplateHtml } from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetPaymentReceived } from './GetPaymentReceived';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
import { transformPaymentReceivedToPdfTemplate } from './utils';
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class GetPaymentReceivedPdf {
@@ -17,9 +17,6 @@ export default class GetPaymentReceivedPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getPaymentService: GetPaymentReceived;
@@ -29,6 +26,23 @@ export default class GetPaymentReceivedPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves payment received html content.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @returns {Promise<string>}
*/
public async getPaymentReceivedHtml(
tenantId: number,
paymentReceivedId: number
): Promise<string> {
const brandingAttributes = await this.getPaymentBrandingAttributes(
tenantId,
paymentReceivedId
);
return renderPaymentReceivedPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -39,15 +53,10 @@ export default class GetPaymentReceivedPdf {
tenantId: number,
paymentReceivedId: number
): Promise<[Buffer, string]> {
const brandingAttributes = await this.getPaymentBrandingAttributes(
const htmlContent = await this.getPaymentReceivedHtml(
tenantId,
paymentReceivedId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/payment-receive-standard',
brandingAttributes
);
const filename = await this.getPaymentReceivedFilename(
tenantId,
paymentReceivedId

View File

@@ -20,6 +20,7 @@ import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify';
import GetPaymentReceivedPdf from './GetPaymentReceivedPdf';
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
import { GetPaymentReceivedState } from './GetPaymentReceivedState';
import { GetPaymentReceivedMailState } from './GetPaymentReceivedMailState';
@Service()
export class PaymentReceivesApplication {
@@ -53,6 +54,9 @@ export class PaymentReceivesApplication {
@Inject()
private getPaymentReceivedStateService: GetPaymentReceivedState;
@Inject()
private getPaymentReceivedMailStateService: GetPaymentReceivedMailState;
/**
* Creates a new payment receive.
* @param {number} tenantId
@@ -204,12 +208,15 @@ export class PaymentReceivesApplication {
/**
* Retrieves the default mail options of the given payment transaction.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment received id.
* @returns {Promise<void>}
*/
public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) {
return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId);
return this.getPaymentReceivedMailStateService.getMailOptions(
tenantId,
paymentReceiveId
);
}
/**
@@ -228,6 +235,22 @@ export class PaymentReceivesApplication {
);
};
/**
* Retrieves the given payment receive html document.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @returns {Promise<string>}
*/
public getPaymentReceivedHtml = (
tenantId: number,
paymentReceiveId: number
) => {
return this.getPaymentReceivePdfService.getPaymentReceivedHtml(
tenantId,
paymentReceiveId
);
};
/**
* Retrieves the create/edit initial state of the payment received.
* @param {number} tenantId - The ID of the tenant.

View File

@@ -15,8 +15,9 @@ import { GetPaymentReceived } from './GetPaymentReceived';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformPaymentReceivedToMailDataArgs } from './utils';
import { GetPaymentReceivedMailTemplate } from './GetPaymentReceivedMailTemplate';
import events from '@/subscribers/events';
@Service()
export class SendPaymentReceiveMailNotification {
@@ -29,12 +30,15 @@ export class SendPaymentReceiveMailNotification {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject('agenda')
private agenda: any;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private paymentMailTemplate: GetPaymentReceivedMailTemplate;
@Inject('agenda')
private agenda: any;
/**
* Sends the mail of the given payment receive.
* @param {number} tenantId
@@ -62,37 +66,6 @@ export class SendPaymentReceiveMailNotification {
} as PaymentReceiveMailPresendEvent);
}
/**
* Retrieves the default payment mail options.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment receive id.
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public getMailOptions = async (
tenantId: number,
paymentId: number
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findById(paymentId)
.throwIfNotFound();
const formatArgs = await this.textFormatter(tenantId, paymentId);
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceive.customerId
);
return {
...mailOptions,
subject: DEFAULT_PAYMENT_MAIL_SUBJECT,
message: DEFAULT_PAYMENT_MAIL_CONTENT,
...formatArgs,
};
};
/**
* Retrieves the formatted text of the given sale invoice.
* @param {number} tenantId - Tenant id.
@@ -108,7 +81,82 @@ export class SendPaymentReceiveMailNotification {
tenantId,
invoiceId
);
return transformPaymentReceivedToMailDataArgs(payment);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
const paymentArgs = transformPaymentReceivedToMailDataArgs(payment);
return {
...commonArgs,
...paymentArgs,
};
};
/**
* Retrieves the mail options of the given payment received.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceivedId - Payment received id.
* @param {string} defaultSubject - Default subject of the mail.
* @param {string} defaultContent - Default content of the mail.
* @returns
*/
public getMailOptions = async (
tenantId: number,
paymentReceivedId: number,
defaultSubject: string = DEFAULT_PAYMENT_MAIL_SUBJECT,
defaultContent: string = DEFAULT_PAYMENT_MAIL_CONTENT
): Promise<PaymentReceiveMailOpts> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceived = await PaymentReceive.query().findById(
paymentReceivedId
);
const formatArgs = await this.textFormatter(tenantId, paymentReceivedId);
// Retrieves the default mail options.
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceived.customerId
);
return {
...mailOptions,
message: defaultContent,
subject: defaultSubject,
attachPdf: true,
formatArgs,
};
};
/**
* Formats the mail options of the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {PaymentReceiveMailOpts} mailOptions
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public formattedMailOptions = async (
tenantId: number,
paymentReceiveId: number,
mailOptions: PaymentReceiveMailOpts
): Promise<PaymentReceiveMailOpts> => {
const formatterArgs = await this.textFormatter(tenantId, paymentReceiveId);
const formattedOptions =
await this.contactMailNotification.formatMailOptions(
tenantId,
mailOptions,
formatterArgs
);
// Retrieves the mail template.
const message = await this.paymentMailTemplate.getMailTemplate(
tenantId,
paymentReceiveId,
{
message: formattedOptions.message,
preview: formattedOptions.message,
}
);
return { ...formattedOptions, message };
};
/**
@@ -136,10 +184,10 @@ export class SendPaymentReceiveMailNotification {
messageDTO
);
// Formats the message options.
return this.contactMailNotification.formatMailOptions(
return this.formattedMailOptions(
tenantId,
parsedMessageOpts,
formatterArgs
paymentReceiveId,
parsedMessageOpts
);
};

View File

@@ -1,18 +1,15 @@
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Payment Received for {Customer Name} from {Company Name}';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
<p>
Payment Date : <strong>{Payment Date}</strong><br />
Amount : <strong>{Payment Amount}</strong></br />
</p>
' Payment Confirmation from {Company Name} Thank You!';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `Dear {Customer Name}
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!
Payment Transaction: {Payment Number}
Payment Date : {Payment Date}
Amount : {Payment Amount}
Regards,
{Company Name}`;
export const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
import { GetSaleReceiptMailStateTransformer } from './GetSaleReceiptMailStateTransformer';
@Service()
export class GetSaleReceiptMailState {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private receiptMail: SaleReceiptMailNotification;
/**
* Retrieves the sale receipt mail state of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
*/
public async getMailState(tenantId: number, saleReceiptId: number) {
const { SaleReceipt } = this.tenancy.models(tenantId);
const saleReceipt = await SaleReceipt.query()
.findById(saleReceiptId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.throwIfNotFound();
const mailOptions = await this.receiptMail.getMailOptions(
tenantId,
saleReceiptId
);
return this.transformer.transform(
tenantId,
saleReceipt,
new GetSaleReceiptMailStateTransformer(),
{
mailOptions,
}
);
}
}

View File

@@ -0,0 +1,221 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
import { DiscountType } from '@/interfaces';
import { SaleReceiptTransformer } from './SaleReceiptTransformer';
export class GetSaleReceiptMailStateTransformer extends SaleReceiptTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
'total',
'totalFormatted',
'discountAmount',
'discountAmountFormatted',
'discountPercentage',
'discountPercentageFormatted',
'discountLabel',
'adjustment',
'adjustmentFormatted',
'subtotal',
'subtotalFormatted',
'receiptDate',
'receiptDateFormatted',
'closedAtDate',
'closedAtDateFormatted',
'receiptNumber',
'entries',
];
};
/**
* Retrieves the customer name of the invoice.
* @returns {string}
*/
protected customerName = (receipt) => {
return receipt.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (receipt) => {
return receipt.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (receipt) => {
return receipt.pdfTemplate?.attributes?.primaryColor;
};
/**
* Retrieves the total amount.
* @param receipt
* @returns
*/
protected total = (receipt) => {
return receipt.total;
};
/**
* Retrieves the formatted total amount.
* @param receipt
* @returns {string}
*/
protected totalFormatted = (receipt) => {
return this.formatMoney(receipt.total, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the discount label of the estimate.
* @param estimate
* @returns {string}
*/
protected discountLabel(receipt) {
return receipt.discountType === DiscountType.Percentage
? `Discount [${receipt.discountPercentageFormatted}]`
: 'Discount';
}
/**
* Retrieves the subtotal of the receipt.
* @param receipt
* @returns
*/
protected subtotal = (receipt) => {
return receipt.subtotal;
};
/**
* Retrieves the formatted subtotal of the receipt.
* @param receipt
* @returns
*/
protected subtotalFormatted = (receipt) => {
return this.formatMoney(receipt.subtotal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the receipt date.
* @param receipt
* @returns
*/
protected receiptDate = (receipt): string => {
return receipt.receiptDate;
};
/**
* Retrieves the formatted receipt date.
* @param {ISaleReceipt} invoice
* @returns {string}
*/
protected receiptDateFormatted = (receipt): string => {
return this.formatDate(receipt.receiptDate);
};
/**
*
* @param receipt
* @returns
*/
protected closedAtDate = (receipt): string => {
return receipt.closedAt;
};
/**
* Retrieve formatted estimate closed at date.
* @param {ISaleReceipt} invoice
* @returns {String}
*/
protected closedAtDateFormatted = (receipt): string => {
return this.formatDate(receipt.closedAt);
};
/**
*
* @param invoice
* @returns
*/
protected entries = (receipt) => {
return this.item(
receipt.entries,
new GetSaleReceiptEntryMailStateTransformer(),
{
currencyCode: receipt.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleReceiptEntryMailStateTransformer extends ItemEntryTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
public name = (entry) => {
return entry.item.name;
};
public includeAttributes = (): string[] => {
return [
'name',
'quantity',
'quantityFormatted',
'rate',
'rateFormatted',
'total',
'totalFormatted',
];
};
}

View File

@@ -0,0 +1,75 @@
import {
ReceiptEmailTemplateProps,
renderReceiptEmailTemplate,
} from '@bigcapital/email-components';
import { Inject, Service } from 'typedi';
import { GetSaleReceipt } from './GetSaleReceipt';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetSaleReceiptMailTemplateAttributesTransformer } from './GetSaleReceiptMailTemplateAttributesTransformer';
@Service()
export class GetSaleReceiptMailTemplate {
@Inject()
private getReceiptService: GetSaleReceipt;
@Inject()
private transformer: TransformerInjectable;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
/**
* Retrieves the mail template attributes of the given estimate.
* Estimate template attributes are composed of the estimate and branding template attributes.
* @param {number} tenantId
* @param {number} receiptId - Receipt id.
* @returns {Promise<EstimatePaymentEmailProps>}
*/
public async getMailTemplateAttributes(
tenantId: number,
receiptId: number
): Promise<ReceiptEmailTemplateProps> {
const receipt = await this.getReceiptService.getSaleReceipt(
tenantId,
receiptId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
receipt.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
receipt,
new GetSaleReceiptMailTemplateAttributesTransformer(),
{
receipt,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId
* @param {number} estimateId
* @param overrideAttributes
* @returns
*/
public async getMailTemplate(
tenantId: number,
estimateId: number,
overrideAttributes?: Partial<any>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
estimateId
);
const mergedAttributes = {
...attributes,
...overrideAttributes,
};
return renderReceiptEmailTemplate(mergedAttributes);
}
}

View File

@@ -0,0 +1,201 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetSaleReceiptMailTemplateAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'primaryColor',
'receiptAmount',
'receiptMessage',
'date',
'dateLabel',
'receiptNumber',
'receiptNumberLabel',
'total',
'totalLabel',
'discount',
'discountLabel',
'adjustment',
'adjustmentLabel',
'subtotal',
'subtotalLabel',
'paidAmount',
'paidAmountLabel',
'items',
];
};
/**
* Exclude all attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Company logo uri.
* @returns {string}
*/
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
/**
* Company name.
* @returns {string}
*/
public companyName(): string {
return this.context.organization.name;
}
/**
* Primary color
* @returns {string}
*/
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
/**
* Receipt number.
* @returns {string}
*/
public receiptNumber(): string {
return this.options.receipt.receiptNumber;
}
/**
* Receipt number label.
* @returns {string}
*/
public receiptNumberLabel(): string {
return 'Receipt # {receiptNumber}';
}
/**
* Date.
* @returns {string}
*/
public date(): string {
return this.options.receipt.date;
}
/**
* Date label.
* @returns {string}
*/
public dateLabel(): string {
return 'Date';
}
/**
* Receipt total.
*/
public total(): string {
return this.options.receipt.totalFormatted;
}
/**
* Receipt total label.
* @returns {string}
*/
public totalLabel(): string {
return 'Total';
}
/**
* Receipt discount.
* @returns {string}
*/
public discount(): string {
return this.options.receipt?.discountAmountFormatted;
}
/**
* Receipt discount label.
* @returns {string}
*/
public discountLabel(): string {
return 'Discount';
}
/**
* Receipt adjustment.
* @returns {string}
*/
public adjustment(): string {
return this.options.receipt?.adjustmentFormatted;
}
/**
* Receipt adjustment label.
* @returns {string}
*/
public adjustmentLabel(): string {
return 'Adjustment';
}
/**
* Receipt subtotal.
* @returns {string}
*/
public subtotal(): string {
return this.options.receipt.subtotalFormatted;
}
/**
* Receipt subtotal label.
* @returns {string}
*/
public subtotalLabel(): string {
return 'Subtotal';
}
/**
* Receipt mail items attributes.
*/
public items(): any[] {
return this.item(
this.options.receipt.entries,
new GetSaleReceiptMailTemplateEntryAttributesTransformer()
);
}
}
class GetSaleReceiptMailTemplateEntryAttributesTransformer extends Transformer {
public includeAttributes = (): string[] => {
return ['label', 'quantity', 'rate', 'total'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public label(entry): string {
return entry?.item?.name;
}
public quantity(entry): string {
return entry?.quantity;
}
public rate(entry): string {
return entry?.rateFormatted;
}
public total(entry): string {
return entry?.totalFormatted;
}
}

View File

@@ -8,7 +8,7 @@ export class GetSaleReceiptState {
private tenancy: HasTenancyService;
/**
* Retireves the sale receipt state.
* Retrieves the sale receipt state.
* @param {Number} tenantId -
* @return {Promise<ISaleReceiptState>}
*/

View File

@@ -18,6 +18,7 @@ import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
import { GetSaleReceiptState } from './GetSaleReceiptState';
import { GetSaleReceiptMailState } from './GetSaleReceiptMailState';
@Service()
export class SaleReceiptApplication {
@@ -51,6 +52,9 @@ export class SaleReceiptApplication {
@Inject()
private getSaleReceiptStateService: GetSaleReceiptState;
@Inject()
private getSaleReceiptMailStateService: GetSaleReceiptMailState;
/**
* Creates a new sale receipt with associated entries.
* @param {number} tenantId
@@ -152,6 +156,19 @@ export class SaleReceiptApplication {
);
}
/**
* Retrieves the given sale receipt html.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns {Promise<string>}
*/
public getSaleReceiptHtml(tenantId: number, saleReceiptId: number) {
return this.getSaleReceiptPdfService.saleReceiptHtml(
tenantId,
saleReceiptId
);
}
/**
* Notify receipt customer by SMS of the given sale receipt.
* @param {number} tenantId
@@ -221,4 +238,20 @@ export class SaleReceiptApplication {
public getSaleReceiptState(tenantId: number): Promise<ISaleReceiptState> {
return this.getSaleReceiptStateService.getSaleReceiptState(tenantId);
}
/**
* Retrieves the mail state of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @returns
*/
public getSaleReceiptMailState(
tenantId: number,
saleReceiptId: number
): Promise<ISaleReceiptState> {
return this.getSaleReceiptMailStateService.getMailState(
tenantId,
saleReceiptId
);
}
}

View File

@@ -31,13 +31,27 @@ export class SaleReceiptGLEntries {
trx?: Knex.Transaction
): Promise<void> => {
const { SaleReceipt } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const saleReceipt = await SaleReceipt.query(trx)
.findById(saleReceiptId)
.withGraphFetched('entries.item');
// Find or create the discount expense account.
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{},
trx
);
// Find or create the other charges account.
const otherChargesAccount =
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieve the income entries ledger.
const incomeLedger = this.getIncomeEntriesLedger(saleReceipt);
const incomeLedger = this.getIncomeEntriesLedger(
saleReceipt,
discountAccount.id,
otherChargesAccount.id
);
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(tenantId, incomeLedger, trx);
@@ -87,8 +101,16 @@ export class SaleReceiptGLEntries {
* @param {ISaleReceipt} saleReceipt
* @returns {Ledger}
*/
private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => {
const entries = this.getIncomeGLEntries(saleReceipt);
private getIncomeEntriesLedger = (
saleReceipt: ISaleReceipt,
discountAccountId: number,
otherChargesAccountId: number
): Ledger => {
const entries = this.getIncomeGLEntries(
saleReceipt,
discountAccountId,
otherChargesAccountId
);
return new Ledger(entries);
};
@@ -121,10 +143,10 @@ export class SaleReceiptGLEntries {
};
/**
* Retrieve receipt income item GL entry.
* @param {ISaleReceipt} saleReceipt -
* @param {IItemEntry} entry -
* @param {number} index -
* Retrieve receipt income item G/L entry.
* @param {ISaleReceipt} saleReceipt -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getReceiptIncomeItemEntry = R.curry(
@@ -134,11 +156,11 @@ export class SaleReceiptGLEntries {
index: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
const itemIncome = entry.amount * saleReceipt.exchangeRate;
const totalLocal = entry.totalExcludingTax * saleReceipt.exchangeRate;
return {
...commonEntry,
credit: itemIncome,
credit: totalLocal,
accountId: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
@@ -161,24 +183,76 @@ export class SaleReceiptGLEntries {
return {
...commonEntry,
debit: saleReceipt.localAmount,
debit: saleReceipt.totalLocal,
accountId: saleReceipt.depositAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the discount GL entry.
* @param {ISaleReceipt} saleReceipt
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getDiscountEntry = (
saleReceipt: ISaleReceipt,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
return {
...commonEntry,
debit: saleReceipt.discountAmountLocal,
accountId: discountAccountId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the adjustment GL entry.
* @param {ISaleReceipt} saleReceipt
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
saleReceipt: ISaleReceipt,
adjustmentAccountId: number
): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
const adjustmentAmount = Math.abs(saleReceipt.adjustmentLocal);
return {
...commonEntry,
debit: saleReceipt.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: saleReceipt.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: adjustmentAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the income GL entries.
* @param {ISaleReceipt} saleReceipt -
* @returns {ILedgerEntry[]}
*/
private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => {
private getIncomeGLEntries = (
saleReceipt: ISaleReceipt,
discountAccountId: number,
otherChargesAccountId: number
): ILedgerEntry[] => {
const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt);
const creditEntries = saleReceipt.entries.map(getItemEntry);
const depositEntry = this.getReceiptDepositEntry(saleReceipt);
return [depositEntry, ...creditEntries];
const discountEntry = this.getDiscountEntry(saleReceipt, discountAccountId);
const adjustmentEntry = this.getAdjustmentEntry(
saleReceipt,
otherChargesAccountId
);
return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry];
};
}

View File

@@ -17,6 +17,7 @@ import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformReceiptToMailDataArgs } from './utils';
import { GetSaleReceiptMailTemplate } from './GetSaleReceiptMailTemplate';
@Service()
export class SaleReceiptMailNotification {
@@ -32,6 +33,9 @@ export class SaleReceiptMailNotification {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject()
private getReceiptMailTemplate: GetSaleReceiptMailTemplate;
@Inject()
private eventPublisher: EventPublisher;
@@ -111,7 +115,13 @@ export class SaleReceiptMailNotification {
tenantId,
receiptId
);
return transformReceiptToMailDataArgs(receipt);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
return {
...commonArgs,
...transformReceiptToMailDataArgs(receipt),
};
};
/**
@@ -133,7 +143,15 @@ export class SaleReceiptMailNotification {
mailOptions,
formatterArgs
)) as SaleReceiptMailOpts;
return formattedOptions;
const message = await this.getReceiptMailTemplate.getMailTemplate(
tenantId,
receiptId,
{
message: formattedOptions.message,
}
);
return { ...formattedOptions, message };
}
/**

View File

@@ -13,11 +13,24 @@ export class SaleReceiptTransformer extends Transformer {
*/
public includeAttributes = (): string[] => {
return [
'formattedSubtotal',
'discountAmountFormatted',
'discountPercentageFormatted',
'discountAmountLocalFormatted',
'subtotalFormatted',
'subtotalLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'adjustmentFormatted',
'adjustmentLocalFormatted',
'formattedAmount',
'formattedReceiptDate',
'formattedClosedAtDate',
'formattedCreatedAt',
'paidFormatted',
'entries',
'attachments',
];
@@ -43,7 +56,7 @@ export class SaleReceiptTransformer extends Transformer {
/**
* Retrieve formatted receipt created at date.
* @param receipt
* @param receipt
* @returns {string}
*/
protected formattedCreatedAt = (receipt: ISaleReceipt): string => {
@@ -55,8 +68,41 @@ export class SaleReceiptTransformer extends Transformer {
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected formattedSubtotal = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, { money: false });
protected subtotalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.subtotal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the estimate formatted subtotal in local currency.
* @param {ISaleReceipt} receipt
* @returns {string}
*/
protected subtotalLocalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.subtotalLocal, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the receipt formatted total.
* @param receipt
* @returns {string}
*/
protected totalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.total, { currencyCode: receipt.currencyCode });
};
/**
* Retrieves the receipt formatted total in local currency.
* @param receipt
* @returns {string}
*/
protected totalLocalFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.totalLocal, {
currencyCode: receipt.currencyCode,
});
};
/**
@@ -64,12 +110,57 @@ export class SaleReceiptTransformer extends Transformer {
* @param {ISaleReceipt} estimate
* @returns {string}
*/
protected formattedAmount = (receipt: ISaleReceipt): string => {
protected amountFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.amount, {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves formatted discount amount.
* @param receipt
* @returns {string}
*/
protected discountAmountFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.discountAmount, {
currencyCode: receipt.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted discount percentage.
* @param receipt
* @returns {string}
*/
protected discountPercentageFormatted = (receipt: ISaleReceipt): string => {
return receipt.discountPercentage ? `${receipt.discountPercentage}%` : '';
};
/**
* Retrieves formatted paid amount.
* @param receipt
* @returns {string}
*/
protected paidFormatted = (receipt: ISaleReceipt): string => {
return formatNumber(receipt.paid, {
currencyCode: receipt.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves formatted adjustment amount.
* @param receipt
* @returns {string}
*/
protected adjustmentFormatted = (receipt: ISaleReceipt): string => {
return this.formatMoney(receipt.adjustment, {
currencyCode: receipt.currencyCode,
excerptZero: true,
});
};
/**
* Retrieves the entries of the credit note.
* @param {ISaleReceipt} credit

View File

@@ -1,11 +1,13 @@
import { Inject, Service } from 'typedi';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import {
renderReceiptPaperTemplateHtml,
ReceiptPaperTemplateProps,
} from '@bigcapital/pdf-templates';
import { GetSaleReceipt } from './GetSaleReceipt';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
import { transformReceiptToBrandingTemplateAttributes } from './utils';
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@@ -17,9 +19,6 @@ export class SaleReceiptsPdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getSaleReceiptService: GetSaleReceipt;
@@ -29,6 +28,19 @@ export class SaleReceiptsPdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves sale receipt html content.
* @param {number} tennatId
* @param {number} saleReceiptId
*/
public async saleReceiptHtml(tennatId: number, saleReceiptId: number) {
const brandingAttributes = await this.getReceiptBrandingAttributes(
tennatId,
saleReceiptId
);
return renderReceiptPaperTemplateHtml(brandingAttributes);
}
/**
* Retrieves sale invoice pdf content.
* @param {number} tenantId -
@@ -41,16 +53,9 @@ export class SaleReceiptsPdf {
): Promise<[Buffer, string]> {
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
const brandingAttributes = await this.getReceiptBrandingAttributes(
tenantId,
saleReceiptId
);
// Converts the receipt template to html content.
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/receipt-regular',
brandingAttributes
);
const htmlContent = await this.saleReceiptHtml(tenantId, saleReceiptId);
// Renders the html content to pdf document.
const content = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
@@ -87,12 +92,12 @@ export class SaleReceiptsPdf {
* Retrieves receipt branding attributes.
* @param {number} tenantId
* @param {number} receiptId
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
* @returns {Promise<ReceiptPaperTemplateProps>}
*/
public async getReceiptBrandingAttributes(
tenantId: number,
receiptId: number
): Promise<ISaleReceiptBrandingTemplateAttributes> {
): Promise<ReceiptPaperTemplateProps> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(

View File

@@ -1,18 +1,17 @@
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
'Receipt {Receipt Number} from {Company Name}';
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
<p>
Receipt <strong>#{Receipt Number}</strong><br />
Amount : <strong>{Receipt Amount}</strong></br />
</p>
export const DEFAULT_RECEIPT_MAIL_CONTENT = `Hi {Customer Name},
<p>
<i>Regards</i><br />
<i>{Company Name}</i>
</p>
`;
Here's receipt # {Receipt Number} for Receipt {Receipt Amount}
The receipt paid on {Receipt Date}, and the total amount paid is {Receipt Amount}.
Please find your sale receipt attached to this email for your reference
If you have any questions, please let us know.
Thanks,
{Company Name}`;
export const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',

View File

@@ -1,24 +1,30 @@
import {
ISaleReceipt,
ISaleReceiptBrandingTemplateAttributes,
} from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
import { ReceiptPaperTemplateProps } from '@bigcapital/pdf-templates';
export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt
): Partial<ISaleReceiptBrandingTemplateAttributes> => {
saleReceipt
): Partial<ReceiptPaperTemplateProps> => {
return {
total: saleReceipt.formattedAmount,
subtotal: saleReceipt.formattedSubtotal,
total: saleReceipt.totalFormatted,
subtotal: saleReceipt.subtotalFormatted,
lines: saleReceipt.entries?.map((entry) => ({
item: entry.item.name,
description: entry.description,
rate: entry.rateFormatted,
quantity: entry.quantityFormatted,
discount: entry.discountFormatted,
total: entry.totalFormatted,
})),
receiptNumber: saleReceipt.receiptNumber,
receiptDate: saleReceipt.formattedReceiptDate,
discount: saleReceipt.discountAmountFormatted,
discountLabel: saleReceipt.discountPercentageFormatted
? `Discount [${saleReceipt.discountPercentageFormatted}]`
: 'Discount',
showLineDiscount: saleReceipt.entries.some(
(entry) => entry.discountFormatted
),
adjustment: saleReceipt.adjustmentFormatted,
customerAddress: contactAddressTextFormat(saleReceipt.customer),
};
};

View File

@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
import { keyBy, sumBy } from 'lodash';
import { ItemEntry } from '@/models';
import HasTenancyService from '../Tenancy/TenancyService';
import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces';
import { IItemEntry } from '@/interfaces';
@Service()
export class ItemEntriesTaxTransactions {

View File

@@ -0,0 +1,27 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable('api_keys', (table) => {
table.increments('id').primary();
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.integer('user_id').unsigned().index().references('id').inTable('users');
table.string('name');
table.text('key');
table.dateTime('expires_at').nullable();
table.dateTime('revoked_at').nullable();
table.timestamps();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTableIfExists('api_keys');
};

View File

@@ -413,7 +413,10 @@ export const formatSmsMessage = (message: string, args) => {
const variable = `{${key}}`;
const value = _.defaultTo(args[key], '');
formattedMessage = formattedMessage.replace(variable, value);
formattedMessage = formattedMessage.replace(
new RegExp(variable, 'g'),
value
);
});
return formattedMessage;
};

View File

@@ -5,6 +5,7 @@
"dependencies": {
"@bigcapital/utils": "*",
"@bigcapital/pdf-templates": "*",
"@bigcapital/email-components": "*",
"@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.3.7",
"@blueprintjs-formik/select": "^0.3.5",

View File

@@ -62,6 +62,9 @@ export function DataTable(props) {
initialPageIndex = 0,
initialPageSize = 20,
// Hidden columns.
initialHiddenColumns = [],
updateDebounceTime = 200,
selectionColumnWidth = 42,
@@ -115,6 +118,7 @@ export function DataTable(props) {
columnResizing: {
columnWidths: initialColumnsWidths || {},
},
hiddenColumns: initialHiddenColumns,
},
manualPagination,
pageCount: controlledPageCount,

View File

@@ -11,7 +11,6 @@ import QuickPaymentMadeFormDialog from '@/containers/Dialogs/QuickPaymentMadeFor
import AllocateLandedCostDialog from '@/containers/Dialogs/AllocateLandedCostDialog';
import InvoicePdfPreviewDialog from '@/containers/Dialogs/InvoicePdfPreviewDialog';
import EstimatePdfPreviewDialog from '@/containers/Dialogs/EstimatePdfPreviewDialog';
import ReceiptPdfPreviewDialog from '@/containers/Dialogs/ReceiptPdfPreviewDialog';
import MoneyInDialog from '@/containers/CashFlow/MoneyInDialog';
import MoneyOutDialog from '@/containers/CashFlow/MoneyOutDialog';
import BadDebtDialog from '@/containers/Dialogs/BadDebtDialog';
@@ -45,9 +44,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
import { DialogsName } from '@/constants/dialogs';
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog';
import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog';
@@ -81,7 +77,6 @@ export default function DialogsContainer() {
/>
<InvoicePdfPreviewDialog dialogName={DialogsName.InvoicePdfForm} />
<EstimatePdfPreviewDialog dialogName={DialogsName.EstimatePdfForm} />
<ReceiptPdfPreviewDialog dialogName={DialogsName.ReceiptPdfForm} />
<MoneyInDialog dialogName={DialogsName.MoneyInForm} />
<MoneyOutDialog dialogName={DialogsName.MoneyOutForm} />
@@ -143,9 +138,6 @@ export default function DialogsContainer() {
<InvoiceExchangeRateChangeDialog
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
/>
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ExportDialog dialogName={DialogsName.Export} />
<RuleFormDialog dialogName={DialogsName.BankRuleForm} />
<DisconnectBankAccountDialog

View File

@@ -32,6 +32,9 @@ import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/Branding
import { DRAWERS } from '@/constants/drawers';
import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer';
import { EstimateSendMailDrawer } from '@/containers/Sales/Estimates/EstimateSendMailDrawer';
import { ReceiptSendMailDrawer } from '@/containers/Sales/Receipts/ReceiptSendMailDrawer';
import { PaymentReceivedSendMailDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedMailDrawer';
/**
* Drawers container of the dashboard.
@@ -81,6 +84,9 @@ export default function DrawersContainer() {
/>
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
<InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} />
<EstimateSendMailDrawer name={DRAWERS.ESTIMATE_SEND_MAIL} />
<ReceiptSendMailDrawer name={DRAWERS.RECEIPT_SEND_MAIL} />
<PaymentReceivedSendMailDrawer name={DRAWERS.PAYMENT_RECEIVED_SEND_MAIL} />
</div>
);
}

View File

@@ -2,18 +2,22 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { Money } from '@/components';
import '@/style/components/BigAmount.scss';
export function PageFormBigNumber({ label, amount, currencyCode }) {
interface PageFormBigNumberProps {
label: string;
amount: string | number;
}
export function PageFormBigNumber({
label,
amount,
}: PageFormBigNumberProps) {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
<div class="big-amount">
<span class="big-amount__label">{label}</span>
<h1 class="big-amount__number">
<Money amount={amount} currency={currencyCode} />
</h1>
<h1 class="big-amount__number">{amount}</h1>
</div>
</div>
);

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { x } from '@xstyled/emotion';
export const TotalLineBorderStyle = {
None: 'None',
SingleDark: 'SingleDark',
@@ -32,14 +32,14 @@ export function TotalLines({
export function TotalLine({ title, value, borderStyle, textStyle, className }) {
return (
<TotalLineRoot
<TotalLinePrimitive
borderStyle={borderStyle}
textStyle={textStyle}
className={className}
>
<div class="title">{title}</div>
<div class="amount">{value}</div>
</TotalLineRoot>
</TotalLinePrimitive>
);
}
@@ -63,7 +63,7 @@ export const TotalLinesRoot = styled.div`
`}
`;
export const TotalLineRoot = styled.div`
export const TotalLinePrimitive = styled.div`
display: table-row;
.amount,
@@ -96,5 +96,17 @@ export const TotalLineRoot = styled.div`
.amount {
text-align: right;
width: 25%;
}
`;
const TotalLineAmount = (props) => {
return <x.div display={'table-cell'} padding={'8px'} textAlign={'right'} {...props} />;
};
export const TotalLineTitle = (props) => {
return <x.div display={'table-cell'} padding={'8px'} {...props} />;
};
TotalLinePrimitive.Amount = TotalLineAmount;
TotalLinePrimitive.Title = TotalLineTitle;

View File

@@ -34,5 +34,8 @@ export enum DRAWERS {
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT',
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL'
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL',
ESTIMATE_SEND_MAIL = 'ESTIMATE_SEND_MAIL',
RECEIPT_SEND_MAIL = 'RECEIPT_SEND_MAIL',
PAYMENT_RECEIVED_SEND_MAIL = 'PAYMENT_RECEIVED_SEND_MAIL',
}

View File

@@ -1,11 +1,10 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { CLASSES } from '@/constants/classes';
import { safeSumBy } from '@/utils';
import { PageFormBigNumber, FormattedMessage as T } from '@/components';
import MakeJournalEntriesHeaderFields from './MakeJournalEntriesHeaderFields';
import { useManualJournalTotalFormatted } from './utils';
export default function MakeJournalEntriesHeader() {
return (
@@ -21,19 +20,9 @@ export default function MakeJournalEntriesHeader() {
* @returns {React.ReactNode}
*/
function MakeJournalHeaderBigNumber() {
const {
values: { entries, currency_code },
} = useFormikContext();
const totalCredit = safeSumBy(entries, 'credit');
const totalDebit = safeSumBy(entries, 'debit');
const total = Math.max(totalCredit, totalDebit);
const totalFormatted = useManualJournalTotalFormatted();
return (
<PageFormBigNumber
label={<T id={'amount'} />}
amount={total}
currencyCode={currency_code}
/>
<PageFormBigNumber label={<T id={'amount'} />} amount={totalFormatted} />
);
}

View File

@@ -8,10 +8,14 @@ import {
TotalLineBorderStyle,
TotalLineTextStyle,
} from '@/components';
import { useJournalTotals } from './utils';
import {
useManualJournalSubtotalFormatted,
useManualJournalTotalFormatted,
} from './utils';
export function MakeJournalFormFooterRight() {
const { formattedSubtotal, formattedTotal } = useJournalTotals();
const formattedSubtotal = useManualJournalSubtotalFormatted();
const formattedTotal = useManualJournalTotalFormatted();
return (
<MakeJouranlTotalLines>
@@ -29,7 +33,7 @@ export function MakeJournalFormFooterRight() {
);
}
const MakeJouranlTotalLines =styled(TotalLines)`
const MakeJouranlTotalLines = styled(TotalLines)`
width: 100%;
color: #555555;
`;

View File

@@ -226,34 +226,73 @@ export const useSetPrimaryBranchToForm = () => {
}, [isBranchesSuccess, setFieldValue, branches]);
};
/**
* Retreives the Journal totals.
*/
export const useJournalTotals = () => {
const {
values: { entries, currency_code: currencyCode },
} = useFormikContext();
export const useManualJournalCreditTotal = () => {
const { values } = useFormikContext();
const totalCredit = safeSumBy(values.entries, 'credit');
// Retrieves the invoice entries total.
const totalCredit = safeSumBy(entries, 'credit');
const totalDebit = safeSumBy(entries, 'debit');
return totalCredit;
};
const total = Math.max(totalCredit, totalDebit);
// Retrieves the formatted total money.
const formattedTotal = React.useMemo(
() => formattedAmount(total, currencyCode),
[total, currencyCode],
);
// Retrieves the formatted subtotal.
const formattedSubtotal = React.useMemo(
() => formattedAmount(total, currencyCode, { money: false }),
[total, currencyCode],
);
export const useManualJournalCreditTotalFormatted = () => {
const totalCredit = useManualJournalCreditTotal();
const { values } = useFormikContext();
return {
formattedTotal,
formattedSubtotal,
};
return formattedAmount(totalCredit, values.currency_code);
};
export const useManualJournalDebitTotal = () => {
const { values } = useFormikContext();
const totalDebit = safeSumBy(values.entries, 'debit');
return totalDebit;
};
export const useManualJournalDebitTotalFormatted = () => {
const totalDebit = useManualJournalDebitTotal();
const { values } = useFormikContext();
return formattedAmount(totalDebit, values.currency_code);
};
export const useManualJournalSubtotal = () => {
const totalCredit = useManualJournalCreditTotal();
const totalDebit = useManualJournalDebitTotal();
return Math.max(totalCredit, totalDebit);
};
export const useManualJournalSubtotalFormatted = () => {
const subtotal = useManualJournalSubtotal();
const { values } = useFormikContext();
return formattedAmount(subtotal, values.currency_code);
};
export const useManualJournalTotalDifference = () => {
const totalCredit = useManualJournalCreditTotal();
const totalDebit = useManualJournalDebitTotal();
return Math.abs(totalCredit - totalDebit);
};
export const useManualJournalTotalDifferenceFormatted = () => {
const difference = useManualJournalTotalDifference();
const { values } = useFormikContext();
return formattedAmount(difference, values.currency_code);
};
export const useManualJournalTotal = () => {
const total = useManualJournalSubtotal();
return total;
};
export const useManualJournalTotalFormatted = () => {
const total = useManualJournalTotal();
const { values } = useFormikContext();
return formattedAmount(total, values.currency_code);
};
/**

View File

@@ -20,6 +20,10 @@ export default function BillDetailTable() {
<CommercialDocEntriesTable
columns={columns}
data={entries}
initialHiddenColumns={
// If any entry has no discount, hide the discount column.
entries?.some((e) => e.discount_formatted) ? [] : ['discount']
}
styleName={TableStyle.Constrant}
/>
);

View File

@@ -31,6 +31,23 @@ export function BillDetailTableFooter() {
textStyle={TotalLineTextStyle.Regular}
/>
))}
{bill.discount_amount > 0 && (
<TotalLine
title={
bill.discount_percentage_formatted
? `Discount [${bill.discount_percentage_formatted}]`
: 'Discount'
}
value={bill.discount_amount_formatted}
textStyle={TotalLineTextStyle.Regular}
/>
)}
{bill.adjustment_formatted && (
<TotalLine
title={'Adjustment'}
value={bill.adjustment_formatted}
/>
)}
<TotalLine
title={<T id={'bill.details.total'} />}
value={bill.total_formatted}

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