feat: localization invoice, estimate and receipt pdf templates.

This commit is contained in:
a.bouhuolia
2021-08-17 18:10:35 +02:00
parent 5c47d1549b
commit f7c3244145
23 changed files with 1938 additions and 82 deletions

View File

@@ -37,4 +37,4 @@ LICENSES_AUTH_PASSWORD=root
AGENDASH_AUTH_USER=agendash AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123
BROWSER_WS_ENDPOINT=ws://localhost:3000/ BROWSER_WS_ENDPOINT=ws://localhost:4080/

View File

@@ -9,7 +9,7 @@
"copy-18n": "cpy --cwd=src/locales --parents '**/*.json' ../../build/locales", "copy-18n": "cpy --cwd=src/locales --parents '**/*.json' ../../build/locales",
"clear": "rimraf build", "clear": "rimraf build",
"build:ts": "tsc -p tsconfig.json", "build:ts": "tsc -p tsconfig.json",
"build:resources": "gulp --gulpfile=scripts/gulpfile.js styles", "build:resources": "gulp --gulpfile=scripts/gulpfile.js styles styles-rtl",
"build": "npm-run-all clear build:ts copy-18n" "build": "npm-run-all clear build:ts copy-18n"
}, },
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>", "author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
@@ -81,6 +81,7 @@
"ramda": "^0.27.1", "ramda": "^0.27.1",
"rate-limiter-flexible": "^2.1.14", "rate-limiter-flexible": "^2.1.14",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rtl-detect": "^1.0.4",
"ts-transformer-keys": "^0.4.2", "ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0", "tsyringe": "^4.3.0",
"uniqid": "^5.2.0", "uniqid": "^5.2.0",
@@ -102,13 +103,17 @@
"eslint-plugin-import": "^2.19.1", "eslint-plugin-import": "^2.19.1",
"faker": "^4.1.0", "faker": "^4.1.0",
"getopts": "^2.2.5", "getopts": "^2.2.5",
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"knex-factory": "0.0.6", "knex-factory": "0.0.6",
"merge-stream": "^2.0.0",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"nyc": "^14.1.1", "nyc": "^14.1.1",
"regenerator-runtime": "^0.13.7", "regenerator-runtime": "^0.13.7",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rtlcss": "^3.3.0",
"sass": "^1.37.5", "sass": "^1.37.5",
"sinon": "^7.4.2", "sinon": "^7.4.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -21,3 +21,15 @@ th {
border-style: solid; border-style: solid;
border-width: 0; border-width: 0;
} }
body{
margin: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
direction: ltr;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,57 +1,18 @@
@import "../base.scss"; @import "../base.scss";
@import "../fonts.scss";
body { body {
background: #f8f9fa; background: #f8f9fa;
font-family: 'Noto Sans', sans-serif;
text-align: left; text-align: left;
html[lang^='ar'] & {
font-family: "Segoe UI";
}
html[lang^='en'] & {
font-family: "Noto Sans";
}
@media print { @media print {
background: #fff; background: #fff;
} }
} }
.page {
background: white;
display: block;
margin: 0.5cm auto;
box-shadow: rgba(122, 136, 146, 0.15) 0px 1px 3px 1px;
width: 21cm;
height: 29.7cm;
@media print {
margin: 0;
box-shadow: 0 0 0;
width: 100%;
height: auto;
}
&[size="A4"] {
width: 21cm;
height: 29.7cm;
}
&[size="A4"][layout="landscape"] {
width: 29.7cm;
height: 21cm;
}
&[size="A3"] {
width: 29.7cm;
height: 42cm;
}
&[size="A3"][layout="landscape"] {
width: 42cm;
height: 29.7cm;
}
&[size="A5"] {
width: 14.8cm;
height: 21cm;
}
&[size="A5"][layout="landscape"] {
width: 21cm;
height: 14.8cm;
}
}

View File

@@ -2,7 +2,7 @@
.invoice{ .invoice{
text-align: left; text-align: left;
padding: 45px; padding: 45px 40px;
&__header{ &__header{
display: flex; display: flex;

View File

@@ -1,4 +1,4 @@
html html(lang=locale)
head head
title My Site - #{title} title My Site - #{title}
block head block head

View File

@@ -2,7 +2,10 @@ extends ../PaperTemplateLayout.pug
block head block head
style style
include ../../css/modules/estimate.css if (isRtl)
include ../../css/modules/estimate-rtl.css
else
include ../../css/modules/estimate.css
block content block content
div.estimate div.estimate
@@ -18,7 +21,7 @@ block content
div.estimate__meta div.estimate__meta
div.estimate__meta-item.estimate__meta-item--amount div.estimate__meta-item.estimate__meta-item--amount
span.label #{__('estimate.paper.due_amount')} span.label #{__('estimate.paper.amount')}
span.value #{saleEstimate.formattedAmount} span.value #{saleEstimate.formattedAmount}
div.estimate__meta-item.estimate__meta-item--billed-to div.estimate__meta-item.estimate__meta-item--billed-to

View File

@@ -2,7 +2,10 @@ extends ../PaperTemplateLayout.pug
block head block head
style style
include ../../css/modules/invoice.css if (isRtl)
include ../../css/modules/invoice-rtl.css
else
include ../../css/modules/invoice.css
block content block content
div.invoice div.invoice
@@ -19,7 +22,7 @@ block content
div.invoice__meta div.invoice__meta
div.invoice__meta-item.invoice__meta-item--amount div.invoice__meta-item.invoice__meta-item--amount
span.label #{__('estimate.paper.due_amount')} span.label #{__('invoice.paper.due_amount')}
span.value #{saleInvoice.formattedAmount} span.value #{saleInvoice.formattedAmount}
div.invoice__meta-item.invoice__meta-item--billed-to div.invoice__meta-item.invoice__meta-item--billed-to

View File

@@ -2,7 +2,10 @@ extends ../PaperTemplateLayout.pug
block head block head
style style
include ../../css/modules/receipt.css if (isRtl)
include ../../css/modules/receipt-rtl.css
else
include ../../css/modules/receipt.css
block content block content
div.receipt div.receipt

View File

@@ -37,20 +37,20 @@ module.exports = {
clean: ['style.css', 'style.min.css', 'style-rtl.css', 'style-rtl.min.css'], clean: ['style.css', 'style.min.css', 'style-rtl.css', 'style-rtl.min.css'],
build: [ build: [
{ {
src: `${RESOURCES_PATH}/scss/modules/invoice.scss`, // Path to main .scss file. src: `${RESOURCES_PATH}/scss/modules/invoice.scss`,
dest: `${RESOURCES_PATH}/css/modules`, // Path to place the compiled CSS file. dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it. // sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source. // minify: true, // Allow to enable/disable minify the source.
}, },
{ {
src: `${RESOURCES_PATH}/scss/modules/estimate.scss`, // Path to main .scss file. src: `${RESOURCES_PATH}/scss/modules/estimate.scss`,
dest: `${RESOURCES_PATH}/css/modules`, // Path to place the compiled CSS file. dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it. // sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source. // minify: true, // Allow to enable/disable minify the source.
}, },
{ {
src: `${RESOURCES_PATH}/scss/modules/receipt.scss`, // Path to main .scss file. src: `${RESOURCES_PATH}/scss/modules/receipt.scss`,
dest: `${RESOURCES_PATH}/css/modules`, // Path to place the compiled CSS file. dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it. // sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source. // minify: true, // Allow to enable/disable minify the source.
}, },
@@ -61,11 +61,19 @@ module.exports = {
// minify: true, // minify: true,
// }, // },
], ],
// RTL builds.
rtl: [ rtl: [
// RTL builds.
{ {
src: './style.css', src: `${RESOURCES_PATH}/css/modules/invoice.css`,
dest: './', // The source files will be converted and suffixed to `-rtl` in this destination. dest: `${RESOURCES_PATH}/css/modules`,
},
{
src: `${RESOURCES_PATH}/css/modules/estimate.css`,
dest: `${RESOURCES_PATH}/css/modules`,
},
{
src: `${RESOURCES_PATH}/css/modules/receipt.css`,
dest: `${RESOURCES_PATH}/css/modules`,
}, },
], ],

View File

@@ -2,6 +2,13 @@ const gulp = require('gulp');
const sass = require('sass'); const sass = require('sass');
const gulpSass = require('gulp-sass')(sass); // Gulp pluign for Sass compilation. const gulpSass = require('gulp-sass')(sass); // Gulp pluign for Sass compilation.
const mergeStream = require('merge-stream'); const mergeStream = require('merge-stream');
const rename = require('gulp-rename'); // Renames files E.g. style.css -> style.min.css
// Style related.
const postcss = require('gulp-postcss'); // Transforming styles with JS plugins
const rtlcss = require('rtlcss'); // Convert LTR CSS to RTL.
const config = require('./gulpConfig'); const config = require('./gulpConfig');
gulp.task('styles', () => { gulp.task('styles', () => {
@@ -13,3 +20,31 @@ gulp.task('styles', () => {
}); });
return mergeStream(builds); return mergeStream(builds);
}); });
/**
* Task: `styles-rtl`
*
* This task does the following.
* 1. Gets the source css files.
* 2. Covert LTR CSS to RTL.
* 3. Suffix all CSS files to `-rtl`.
* 4. Reloads css files via browser sync stream.
* 5. Combine matching media queries for `.min.css` version.
* 6. Minify all CSS files.
* 7. Reload minified css files via browser sync stream.
*/
gulp.task('styles-rtl', () => {
const builds = config.style.rtl.map((build) => {
return gulp
.src(build.src)
.pipe(
postcss([
rtlcss(config.style.rtlcss), // Convert LTR CSS to RTL.
]),
)
.pipe(rename({ suffix: '-rtl' })) // Append "-rtl" to the filename.
.pipe(gulp.dest(build.dest));
});
return mergeStream(builds);
});

View File

@@ -1,13 +1,15 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import i18n from 'i18n'; import i18n from 'i18n';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { injectI18nUtils } from './TenantDependencyInjection';
/** /**
* I18n from organization settings. * I18n from organization settings.
*/ */
export default (req: Request, res: Response, next: NextFunction) => { export default (req: Request, res: Response, next: NextFunction) => {
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const { settings } = req; const { settings, tenantId } = req;
if (!req.user) { if (!req.user) {
throw new Error('Should load this middleware after `JWTAuth`.'); throw new Error('Should load this middleware after `JWTAuth`.');
@@ -16,9 +18,8 @@ export default (req: Request, res: Response, next: NextFunction) => {
throw new Error('Should load this middleware after `SettingsMiddleware`.'); throw new Error('Should load this middleware after `SettingsMiddleware`.');
} }
// Get the organization language from settings. // Get the organization language from settings.
const language = settings.get({ const language = settings.get({ group: 'organization', key: 'language' });
group: 'organization', key: 'language',
});
if (language) { if (language) {
i18n.setLocale(req, language); i18n.setLocale(req, language);
} }
@@ -26,5 +27,10 @@ export default (req: Request, res: Response, next: NextFunction) => {
language, language,
user: req.user, user: req.user,
}); });
const tenantServices = Container.get(HasTenancyService);
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils(req));
next(); next();
}; };

View File

@@ -11,15 +11,11 @@ export default async (req: Request, res: Response, next: NextFunction) => {
if (tenantContainer && !tenantContainer.has('settings')) { if (tenantContainer && !tenantContainer.has('settings')) {
const { settingRepository } = tenantContainer.get('repositories'); const { settingRepository } = tenantContainer.get('repositories');
Logger.info('[settings_middleware] initialize settings store.');
const settings = new SettingsStore(settingRepository); const settings = new SettingsStore(settingRepository);
tenantContainer.set('settings', settings); tenantContainer.set('settings', settings);
} }
Logger.info('[settings_middleware] get settings instance from container.');
const settings = tenantContainer.get('settings'); const settings = tenantContainer.get('settings');
Logger.info('[settings_middleware] load settings from storage or cache.');
await settings.load(); await settings.load();
req.settings = settings; req.settings = settings;

View File

@@ -3,6 +3,7 @@ import { ITenant } from 'interfaces';
import { Request } from 'express'; import { Request } from 'express';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import TenantsManagerService from 'services/Tenancy/TenantsManager'; import TenantsManagerService from 'services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect';
export default (req: Request, tenant: ITenant) => { export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant; const { id: tenantId, organizationId } = tenant;
@@ -20,7 +21,7 @@ export default (req: Request, tenant: ITenant) => {
const tenantContainer = tenantServices.tenantContainer(tenantId); const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', { __: req.__ }); tenantContainer.set('i18n', injectI18nUtils(req));
req.knex = knexInstance; req.knex = knexInstance;
req.organizationId = organizationId; req.organizationId = organizationId;
@@ -30,3 +31,17 @@ export default (req: Request, tenant: ITenant) => {
req.repositories = repositories; req.repositories = repositories;
req.cache = cacheInstance; req.cache = cacheInstance;
} }
export const injectI18nUtils =(req) => {
const locale = req.getLocale();
const direction = rtlDetect.getLangDir(locale);
return {
locale,
__: req.__,
direction,
isRtl: direction === 'rtl',
isLtr: direction === 'ltr',
}
}

View File

@@ -160,5 +160,32 @@
"Liabilities and Equity": "التزامات وحقوق الملكية", "Liabilities and Equity": "التزامات وحقوق الملكية",
"Closing balance": "الرصيد الختامي", "Closing balance": "الرصيد الختامي",
"Opening balance": "الرصيد الفتاحي", "Opening balance": "الرصيد الفتاحي",
"Total {{accountName}}": "إجمالي {{accountName}}" "Total {{accountName}}": "إجمالي {{accountName}}",
"invoice.paper.invoice": "فاتورة",
"invoice.paper.due_amount": "القيمة المستحقة",
"invoice.paper.billed_to": "فاتورة إلي",
"invoice.paper.invoice_date": "تاريخ الفاتورة",
"invoice.paper.invoice_number": "رقم الفاتورة",
"invoice.paper.due_date": "تاريخ الاستحقاق",
"invoice.paper.conditions_title": "الشروط والأحكام",
"invoice.paper.notes_title": "ملاحظات",
"item_entry.paper.item_name": "اسم الصنف",
"item_entry.paper.rate": "السعر",
"item_entry.paper.quantity": "الكمية",
"item_entry.paper.total": "إجمالي",
"estimate.paper.estimate": "عرض أسعار",
"estimate.paper.billed_to": "عرض أسعار إلي",
"estimate.paper.estimate_date": "تاريخ العرض",
"estimate.paper.estimate_number": "رقم العرض",
"estimate.paper.expiration_date": "تاريخ انتهاء الصلاحية",
"estimate.paper.conditions_title": "الشروط والأحكام",
"estimate.paper.notes_title": "ملاحظات",
"estimate.paper.amount": "قيمة العرض",
"receipt.paper.receipt": "إيصال",
"receipt.paper.billed_to": "الإيصال إلي",
"receipt.paper.receipt_date": "تاريخ الإيصال",
"receipt.paper.receipt_number": "رقم الإيصال",
"receipt.paper.conditions_title": "الشروط والأحكام",
"receipt.paper.notes_title": "ملاحظات",
"receipt.paper.receipt_amount": "قيمة الإيصال"
} }

View File

@@ -161,6 +161,7 @@
"Opening Balance": "Opening balance", "Opening Balance": "Opening balance",
"Total {{accountName}}": "Total {{accountName}}", "Total {{accountName}}": "Total {{accountName}}",
"invoice.paper.invoice": "Invoice", "invoice.paper.invoice": "Invoice",
"invoice.paper.due_amount": "Due amount",
"invoice.paper.billed_to": "Billed to", "invoice.paper.billed_to": "Billed to",
"invoice.paper.invoice_date": "Invoice date", "invoice.paper.invoice_date": "Invoice date",
"invoice.paper.invoice_number": "Invoice No.", "invoice.paper.invoice_number": "Invoice No.",
@@ -178,7 +179,7 @@
"estimate.paper.expiration_date": "Expiration date", "estimate.paper.expiration_date": "Expiration date",
"estimate.paper.conditions_title": "Conditions & terms", "estimate.paper.conditions_title": "Conditions & terms",
"estimate.paper.notes_title": "Notes", "estimate.paper.notes_title": "Notes",
"estimate.paper.due_amount": "Due amount", "estimate.paper.amount": "Estimate amount",
"receipt.paper.receipt": "Receipt", "receipt.paper.receipt": "Receipt",
"receipt.paper.billed_to": "Billed to", "receipt.paper.billed_to": "Billed to",
"receipt.paper.receipt_date": "Receipt date", "receipt.paper.receipt_date": "Receipt date",