2
0
mirror of https://github.com/frappe/books.git synced 2024-11-14 01:14:03 +00:00

feat: Invoice Template Customizer

- Template, Color and Font fields in PrintSettings
- font-manager for loading system fonts
- 3 Invoice Templates: Default, Minimal and Business
- Delete unused old Templates
- Dont show Delete button in Singles
- Rename font family "Inter var experimental" to "Inter"
This commit is contained in:
Faris Ansari 2020-02-03 23:20:54 +05:30
parent 5316756843
commit 138b0673a8
21 changed files with 569 additions and 749 deletions

View File

@ -1,3 +1,8 @@
const theme = require('@/theme');
const fontManager = require('font-manager');
const uniq = require('lodash/uniq');
let fonts = [];
module.exports = { module.exports = {
name: 'PrintSettings', name: 'PrintSettings',
label: 'Print Settings', label: 'Print Settings',
@ -54,21 +59,63 @@ module.exports = {
fieldname: 'template', fieldname: 'template',
label: 'Template', label: 'Template',
fieldtype: 'Select', fieldtype: 'Select',
options: ['Basic', 'Modern'], options: ['Default', 'Minimal', 'Business'],
default: 'Basic' default: 'Default'
}, },
{ {
fieldname: 'color', fieldname: 'color',
label: 'Theme Color', label: 'Color',
fieldtype: 'Data' placeholder: 'Select Color',
fieldtype: 'Color',
colors: [
'red',
'orange',
'yellow',
'green',
'teal',
'blue',
'indigo',
'purple',
'pink'
]
.map(color => {
let label = color[0].toUpperCase() + color.slice(1);
return {
label,
value: theme.colors[color]['500']
};
})
.concat({
label: 'Black',
value: theme.colors['black']
})
}, },
{ {
fieldname: 'font', fieldname: 'font',
label: 'Font', label: 'Font',
fieldtype: 'Select', fieldtype: 'AutoComplete',
options: ['Inter', 'Roboto'], getList() {
return new Promise(resolve => {
if (fonts.length > 0) {
resolve(fonts);
} else {
fontManager.getAvailableFonts(_fonts => {
fonts = ['Inter'].concat(uniq(_fonts.map(f => f.family)).sort());
resolve(fonts);
});
}
});
},
default: 'Inter' default: 'Inter'
} }
], ],
quickEditFields: ['email', 'phone', 'address', 'gstin'] quickEditFields: [
'template',
'color',
'font',
'email',
'phone',
'address',
'gstin'
]
}; };

View File

@ -1,153 +1,24 @@
<template> <template>
<div> <component :is="printComponent" :doc="doc" :print-settings="printSettings" />
<div>
<div class="px-6 pt-6" v-if="printSettings && accountingSettings">
<div class="flex text-sm text-gray-900 border-b pb-4">
<div class="w-1/3">
<div v-if="printSettings.displayLogo">
<img
class="h-12 max-w-32 object-contain"
:src="printSettings.logo"
/>
</div>
<div class="text-xl text-gray-700 font-semibold" v-else>
{{ accountingSettings.companyName }}
</div>
</div>
<div class="w-1/3">
<div>{{ printSettings.email }}</div>
<div class="mt-1">{{ printSettings.phone }}</div>
</div>
<div class="w-1/3">
<div v-if="address">{{ address.addressDisplay }}</div>
</div>
</div>
</div>
<div class="mt-8 px-6">
<div class="flex justify-between">
<div class="w-1/3">
<h1 class="text-2xl font-semibold">
{{ doc.name }}
</h1>
<div class="py-2 text-base">
{{ frappe.format(doc.date, 'Date') }}
</div>
</div>
<div class="w-1/3">
<div class="py-1 text-right text-lg font-semibold">
{{ doc[partyField.fieldname] }}
</div>
<div v-if="partyDoc" class="mt-1 text-xs text-gray-600 text-right">
{{ partyDoc.addressDisplay }}
</div>
<div
v-if="partyDoc && partyDoc.gstin"
class="mt-1 text-xs text-gray-600 text-right"
>
GSTIN: {{ partyDoc.gstin }}
</div>
</div>
</div>
</div>
<div class="mt-2 px-6 text-base">
<div>
<Row class="text-gray-600 w-full" :ratio="ratio">
<div class="py-4">
{{ _('No') }}
</div>
<div class="py-4" v-for="df in itemFields" :key="df.fieldname" :class="textAlign(df)">
{{ df.label }}
</div>
</Row>
<Row class="text-gray-900 w-full" v-for="row in doc.items" :key="row.name" :ratio="ratio">
<div class="py-4">
{{ row.idx + 1 }}
</div>
<div class="py-4" v-for="df in itemFields" :key="df.fieldname" :class="textAlign(df)">
{{ frappe.format(row[df.fieldname], df) }}
</div>
</Row>
</div>
</div>
</div>
<div class="px-6 mt-2 flex justify-end text-base">
<div class="w-64">
<div class="flex pl-2 justify-between py-3 border-b">
<div>{{ _('Subtotal') }}</div>
<div>{{ frappe.format(doc.netTotal, 'Currency') }}</div>
</div>
<div
class="flex pl-2 justify-between py-3"
v-for="tax in doc.taxes"
:key="tax.name"
>
<div>{{ tax.account }} ({{ tax.rate }}%)</div>
<div>{{ frappe.format(tax.amount, 'Currency') }}</div>
</div>
<div
class="flex pl-2 justify-between py-3 border-t text-green-600 font-semibold text-base"
>
<div>{{ _('Grand Total') }}</div>
<div>{{ frappe.format(doc.grandTotal, 'Currency') }}</div>
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import Row from '@/components/Row'; import Default from './Templates/Default';
import Minimal from './Templates/Minimal';
import Business from './Templates/Business';
export default { export default {
name: 'InvoiceTemplate', name: 'InvoiceTemplate',
props: ['doc'], props: ['doc', 'printSettings'],
components: {
Row
},
data() {
return {
accountingSettings: null,
printSettings: null
};
},
computed: { computed: {
meta() { printComponent() {
return this.doc && this.doc.meta; let type = this.printSettings.template || 'Default';
}, return {
address() { Default,
return this.printSettings && this.printSettings.getLink('address'); Minimal,
}, Business
partyDoc() { }[type];
return this.doc.getLink(this.partyField.fieldname);
},
partyField() {
let fieldname = {
SalesInvoice: 'customer',
PurchaseInvoice: 'supplier'
}[this.doc.doctype];
return this.meta.getField(fieldname);
},
itemFields() {
let itemsMeta = frappe.getMeta(`${this.doc.doctype}Item`);
return itemsMeta.tableFields.map(fieldname =>
itemsMeta.getField(fieldname)
);
},
ratio() {
return [0.3].concat(this.itemFields.map(_ => 1));
} }
},
methods: {
textAlign(df) {
return ['Currency', 'Int', 'Float'].includes(df.fieldtype)
? 'text-right'
: '';
}
},
async mounted() {
this.printSettings = await frappe.getSingle('PrintSettings');
this.accountingSettings = await frappe.getSingle('AccountingSettings');
await this.doc.loadLink(this.partyField.fieldname);
} }
}; };
</script> </script>

View File

@ -119,7 +119,7 @@ module.exports = {
}, },
{ {
fieldname: 'terms', fieldname: 'terms',
label: 'Terms', label: 'Notes',
fieldtype: 'Text' fieldtype: 'Text'
} }
], ],

View File

@ -1,28 +0,0 @@
async function getCompanyDetails() {
let companyDetails = {
name: null,
address: {}
};
let companySettings = await frappe.getDoc('CompanySettings');
companyDetails.name = companySettings.companyName;
let companyAddress = await getAddress(companySettings.companyAddress);
companyDetails.address = companyAddress;
return companyDetails;
}
async function getCustomerAddress(customer) {
let customers = await frappe.db.getAll({ doctype: 'Party', fields:['name, address'], filters: { name: customer }});
let customerDetails = await frappe.getDoc('Party', customers[0].name);
return await getAddress(customerDetails.address);
}
async function getAddress(addressName) {
return await frappe.getDoc('Address', addressName);
}
module.exports = {
getCompanyDetails,
getCustomerAddress
}

View File

@ -0,0 +1,25 @@
<script>
import frappe from 'frappejs';
export default {
name: 'Base',
props: ['doc', 'printSettings'],
data: () => ({ party: null, companyAddress: null }),
methods: {
format(row, fieldname) {
let value = row.get(fieldname);
return frappe.format(value, row.meta.getField(fieldname));
}
},
async mounted() {
await this.doc.loadLink(this.partyField);
this.party = this.doc.getLink(this.partyField);
await this.printSettings.loadLink('address');
this.companyAddress = this.printSettings.getLink('address');
},
computed: {
partyField() {
return this.doc.doctype === 'SalesInvoice' ? 'customer' : 'supplier';
}
}
};
</script>

View File

@ -0,0 +1,125 @@
<template>
<div class="bg-white border" :style="{ 'font-family': printSettings.font }">
<div class="bg-gray-100 px-12 py-10">
<div class="flex items-center">
<div class="flex items-center rounded h-16">
<div class="mr-4" v-if="printSettings.displayLogo">
<img
class="h-12 max-w-32 object-contain"
:src="printSettings.logo"
/>
</div>
</div>
<div>
<div
class="font-semibold text-xl"
:style="{ color: printSettings.color }"
>
{{ frappe.AccountingSettings.companyName }}
</div>
<div class="text-sm text-gray-800" v-if="companyAddress">
{{ companyAddress.addressDisplay }}
</div>
</div>
</div>
<div class="mt-8 text-lg">
<div class="flex">
<div class="w-1/3 font-semibold">
{{ doc.doctype === 'SalesInvoice' ? 'Invoice' : 'Bill' }}
</div>
<div class="w-2/3 text-gray-800">
<div class="font-semibold">
{{ doc.name }}
</div>
<div>
{{ frappe.format(doc.date, 'Date') }}
</div>
</div>
</div>
<div class="mt-4 flex">
<div class="w-1/3 font-semibold">
{{ doc.doctype === 'SalesInvoice' ? 'Customer' : 'Supplier' }}
</div>
<div class="w-2/3 text-gray-800" v-if="party">
<div class="font-semibold">
{{ party.name }}
</div>
<div>
{{ party.addressDisplay }}
</div>
</div>
</div>
</div>
</div>
<div class="px-12 py-12 text-lg">
<div class="mb-4 flex font-semibold ">
<div class="w-4/12">Item</div>
<div class="w-2/12 text-right">Quantity</div>
<div class="w-3/12 text-right">Rate</div>
<div class="w-3/12 text-right">Amount</div>
</div>
<div
class="flex py-1 text-gray-800"
v-for="row in doc.items"
:key="row.name"
>
<div class="w-4/12">{{ row.item }}</div>
<div class="w-2/12 text-right">{{ format(row, 'quantity') }}</div>
<div class="w-3/12 text-right">{{ format(row, 'rate') }}</div>
<div class="w-3/12 text-right">{{ format(row, 'amount') }}</div>
</div>
<div class="mt-12">
<div class="flex -mx-3">
<div class="flex flex-1 justify-between p-3 bg-gray-100">
<div>
<div class="text-gray-800">{{ _('Subtotal') }}</div>
<div class="text-xl mt-2">
{{ frappe.format(doc.netTotal, 'Currency') }}
</div>
</div>
<div v-for="tax in doc.taxes" :key="tax.name">
<div class="text-gray-800">
{{ tax.account }} ({{ tax.rate }}%)
</div>
<div class="text-xl mt-2">
{{ frappe.format(tax.amount, 'Currency') }}
</div>
</div>
</div>
<div
class="p-3 text-right text-white"
:style="{ backgroundColor: printSettings.color }"
>
<div>
<div>{{ _('Grand Total') }}</div>
<div class="text-2xl mt-2 font-semibold">
{{ frappe.format(doc.grandTotal, 'Currency') }}
</div>
</div>
</div>
</div>
<div class="mt-12" v-if="doc.terms">
<template>
<div
class="uppercase text-sm tracking-widest font-semibold text-gray-800"
>
Notes
</div>
<div class="mt-4 text-lg whitespace-pre-line">
{{ doc.terms }}
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import Base from './Base';
export default {
name: 'Business',
extends: Base
};
</script>

View File

@ -1,38 +0,0 @@
<template>
<div v-if="detailsPresent">
<p :style="[$.bold]" style="font-size: 1.3em">{{ companyDetails.name }}</p>
<p :style="$.paraStyle">{{ companyDetails.address.addressLine1 }}</p>
<p :style="$.paraStyle">{{ companyDetails.address.addressLine2 }}</p>
<p :style="$.paraStyle">
{{ companyDetails.address.city + ' ' + companyDetails.address.state }}
</p>
<p :style="$.paraStyle">
{{ companyDetails.address.country + ' - ' + companyDetails.address.postalCode }}
</p>
</div>
</template>
<script>
import addressDetails from './AddressDetails';
import styles from './InvoiceStyles';
export default {
name: 'CompanyAddress',
data() {
return {
$: styles,
detailsPresent: true,
companyDetails: {
name: null,
address: {}
}
}
},
async created() {
this.$ = styles;
try {
this.companyDetails = await addressDetails.getCompanyDetails();
} catch(e) {
this.detailsPresent = false;
}
}
}
</script>

View File

@ -1,37 +0,0 @@
<template>
<div v-if="this.detailsPresent">
<p :style="[$.bold, $.mediumFontSize]">Billed To</p>
<p :style="[$.bold, $.paraStyle]">{{ customer }}</p>
<p :style="$.paraStyle">{{ customerAddress.addressLine1 }}</p>
<p :style="$.paraStyle">{{ customerAddress.addressLine2 }}</p>
<p :style="$.paraStyle">
{{ customerAddress.city + ' ' + customerAddress.state }}
</p>
<p :style="$.paraStyle">
{{ customerAddress.country + ' - ' + customerAddress.postalCode }}
</p>
</div>
</template>
<script>
import addressDetails from './AddressDetails';
import styles from './InvoiceStyles';
export default {
name: 'CustomerAddress',
props: ['customer'],
data() {
return {
$: styles,
detailsPresent: true,
customerAddress: {}
}
},
async created() {
this.$ = styles;
try {
this.customerAddress = await addressDetails.getCustomerAddress(this.customer);
} catch(e) {
this.detailsPresent = false;
}
}
}
</script>

View File

@ -0,0 +1,169 @@
<template>
<div class="border">
<div>
<div class="px-6 pt-6" v-if="printSettings && accountingSettings">
<div class="flex text-sm text-gray-900 border-b pb-4">
<div class="w-1/3">
<div v-if="printSettings.displayLogo">
<img
class="h-12 max-w-32 object-contain"
:src="printSettings.logo"
/>
</div>
<div class="text-xl text-gray-700 font-semibold" v-else>
{{ accountingSettings.companyName }}
</div>
</div>
<div class="w-1/3">
<div>{{ printSettings.email }}</div>
<div class="mt-1">{{ printSettings.phone }}</div>
</div>
<div class="w-1/3">
<div v-if="address">{{ address.addressDisplay }}</div>
</div>
</div>
</div>
<div class="mt-8 px-6">
<div class="flex justify-between">
<div class="w-1/3">
<h1 class="text-2xl font-semibold">
{{ doc.name }}
</h1>
<div class="py-2 text-base">
{{ frappe.format(doc.date, 'Date') }}
</div>
</div>
<div class="w-1/3">
<div class="py-1 text-right text-lg font-semibold">
{{ doc[partyField.fieldname] }}
</div>
<div v-if="partyDoc" class="mt-1 text-xs text-gray-600 text-right">
{{ partyDoc.addressDisplay }}
</div>
<div
v-if="partyDoc && partyDoc.gstin"
class="mt-1 text-xs text-gray-600 text-right"
>
GSTIN: {{ partyDoc.gstin }}
</div>
</div>
</div>
</div>
<div class="mt-2 px-6 text-base">
<div>
<Row class="text-gray-600 w-full" :ratio="ratio">
<div class="py-4">
{{ _('No') }}
</div>
<div
class="py-4"
v-for="df in itemFields"
:key="df.fieldname"
:class="textAlign(df)"
>
{{ df.label }}
</div>
</Row>
<Row
class="text-gray-900 w-full"
v-for="row in doc.items"
:key="row.name"
:ratio="ratio"
>
<div class="py-4">
{{ row.idx + 1 }}
</div>
<div
class="py-4"
v-for="df in itemFields"
:key="df.fieldname"
:class="textAlign(df)"
>
{{ frappe.format(row[df.fieldname], df) }}
</div>
</Row>
</div>
</div>
</div>
<div class="px-6 mt-2 flex justify-end text-base">
<div class="w-64">
<div class="flex pl-2 justify-between py-3 border-b">
<div>{{ _('Subtotal') }}</div>
<div>{{ frappe.format(doc.netTotal, 'Currency') }}</div>
</div>
<div
class="flex pl-2 justify-between py-3"
v-for="tax in doc.taxes"
:key="tax.name"
>
<div>{{ tax.account }} ({{ tax.rate }}%)</div>
<div>{{ frappe.format(tax.amount, 'Currency') }}</div>
</div>
<div
class="flex pl-2 justify-between py-3 border-t text-green-600 font-semibold text-base"
>
<div>{{ _('Grand Total') }}</div>
<div>{{ frappe.format(doc.grandTotal, 'Currency') }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import frappe from 'frappejs';
import Row from '@/components/Row';
export default {
name: 'Default',
props: ['doc', 'printSettings'],
components: {
Row
},
data() {
return {
accountingSettings: null
};
},
computed: {
meta() {
return this.doc && this.doc.meta;
},
address() {
return this.printSettings && this.printSettings.getLink('address');
},
partyDoc() {
return this.doc.getLink(this.partyField.fieldname);
},
partyField() {
let fieldname = {
SalesInvoice: 'customer',
PurchaseInvoice: 'supplier'
}[this.doc.doctype];
return this.meta.getField(fieldname);
},
itemFields() {
let itemsMeta = frappe.getMeta(`${this.doc.doctype}Item`);
return ['item', 'quantity', 'rate', 'amount'].map(fieldname =>
itemsMeta.getField(fieldname)
);
},
ratio() {
return [0.3].concat(this.itemFields.map(() => 1));
}
},
methods: {
textAlign(df) {
return ['Currency', 'Int', 'Float'].includes(df.fieldtype)
? 'text-right'
: '';
}
},
async mounted() {
this.accountingSettings = await frappe.getSingle('AccountingSettings');
await this.doc.loadLink(this.partyField.fieldname);
}
};
</script>
<style></style>

View File

@ -1,54 +0,0 @@
module.exports = {
bold: {
fontWeight: 'bold'
},
font: {
fontFamily: null
},
regularFontSize: {
fontSize: '0.8rem'
},
mediumFontSize: {
fontSize: '1rem'
},
paraStyle: {
margin: '0.5rem',
marginLeft: 0,
marginRight: 0
},
bgColor: {
backgroundColor: null
},
showBorderBottom: {
borderBottom: null
},
hideBorderTop: {
borderTop: '0px solid black'
},
showBorderRight: {
borderRight: '1px solid #e0e0d1'
},
tablePadding: {
paddingTop: '3%',
paddingBottom: '3%'
},
fontColor: {
color: null
},
headerColor: {
backgroundColor: null,
color: 'white'
},
showNoticeBorderBottom: {
borderBottom: null
},
showBorderTop: {
borderTop: null
},
headerFontColor: {
color: null
},
showBorderTop: {
borderTop: null
}
}

View File

@ -1,128 +0,0 @@
<template>
<div :style="$.font" style="font-family: sans-serif;">
<div class="row no-gutters pl-5 pr-5 mt-5">
<div :style="$.regularFontSize" class="col-6">
<company-address />
</div>
<div :style="$.regularFontSize" class="col-6 text-right">
<h2 :style="$.headerFontColor">INVOICE</h2>
<p :style="$.paraStyle">
<strong>{{ doc.name }}</strong>
</p>
<p :style="$.paraStyle">{{ frappe.format(doc.date, 'Date') }}</p>
</div>
</div>
<div class="row pl-5 mt-5">
<div :style="$.regularFontSize" class="col-6 mt-1">
<customer-address :customer="doc.customer" />
</div>
</div>
<div :style="$.regularFontSize" class="row pl-5 pr-5 mt-5">
<div class="col-12">
<table class="table p-0">
<thead>
<tr :style="$.showBorderBottom">
<th :style="$.hideBorderTop" class="text-left pl-0" style="width: 10%">{{ _("NO") }}</th>
<th :style="$.hideBorderTop" class="text-left" style="width: 50%">{{ _("ITEM") }}</th>
<th :style="$.hideBorderTop" class="text-left pl-0" style="width: 15%">{{ _("RATE") }}</th>
<th :style="$.hideBorderTop" class="text-left" style="width: 10%">{{ _("QTY") }}</th>
<th
:style="$.hideBorderTop"
class="text-right pr-1"
style="width: 30%"
>{{ _("AMOUNT") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in doc.items" :key="row.idx">
<td class="text-left pl-1">{{ row.idx + 1 }}</td>
<td class="text-left">{{ row.item }}</td>
<td class="text-left pl-0">{{ row.rate }}</td>
<td class="text-left">{{ row.quantity }}</td>
<td class="text-right pr-1">{{ row.amount }}</td>
</tr>
<tr>
<td colspan="2" class="text-left pl-1"></td>
<td colspan="2" :style="$.bold" class="text-left pl-0">SUBTOTAL</td>
<td :style="$.bold" class="text-right pr-1">{{ doc.netTotal }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="$.bold"
class="text-left pl-0"
>{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.bold" class="text-right pr-1">{{ tax.amount }}</td>
</tr>
<tr>
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="[$.bold, $.mediumFontSize, $.showBorderTop]"
class="text-left pl-0"
>TOTAL</td>
<td
:style="[$.bold, $.mediumFontSize, $.showBorderTop]"
class="text-right pr-1"
style="color: green;"
>{{ doc.grandTotal }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row pl-5 pr-5 mt-5">
<div :style="$.regularFontSize" class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showBorderBottom]">
<td :style="$.hideBorderTop" class="pl-0">NOTICE</td>
</tr>
<tr>
<td class="pl-0">{{ doc.terms }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoiceTemplate1',
components: {
CompanyAddress,
CustomerAddress
},
props: ['doc', 'themeColor', 'font'],
data() {
return {
$: Styles
};
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.headerFontColor.color = this.themeColor;
this.$.showBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.showBorderTop.borderTop = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
};
</script>

View File

@ -1,132 +0,0 @@
<template>
<div :style="[$.regularFontSize, $.font]" style="font-family: sans-serif;">
<div class="row no-gutters p-5" :style="$.headerColor">
<div class="col-8 text-left">
<h1>INVOICE</h1>
</div>
<div class="col-4 text-right">
<company-address />
</div>
</div>
<div class="row p-5 mt-4">
<div class="col-4">
<customer-address :customer="doc.customer" />
</div>
<div class="col-4">
<p :style="[$.bold, $.mediumFontSize]">Invoice Number</p>
<p :style="$.paraStyle">{{ doc.name }}</p>
<br />
<p :style="[$.bold, $.mediumFontSize]">Date</p>
<p :style="$.paraStyle">{{doc.date}}</p>
</div>
<div class="col-4 text-right">
<p :style="[$.bold, $.mediumFontSize]">Invoice Total</p>
<h2 :style="$.fontColor">{{ doc.grandTotal }}</h2>
</div>
</div>
<div class="row pl-5 pr-5 mt-3">
<div class="col-12">
<table class="table table-borderless p-0">
<thead>
<tr :style="[$.showBorderTop, $.fontColor]">
<th class="text-left pl-0" style="width: 10%">{{ _("NO") }}</th>
<th class="text-left" style="width: 50%">{{ _("ITEM") }}</th>
<th class="text-left pl-0" style="width: 15%">{{ _("RATE") }}</th>
<th class="text-left" style="width: 10%">{{ _("QTY") }}</th>
<th class="text-right pr-1" style="width: 30%">{{ _("AMOUNT") }}</th>
</tr>
</thead>
<tbody>
<tr :style="$.showBorderBottom" v-for="row in doc.items" :key="row.idx">
<td class="text-left pl-1">{{ row.idx + 1 }}</td>
<td class="text-left">{{ row.item }}</td>
<td class="text-left pl-0">{{ row.rate }}</td>
<td class="text-left">{{ row.quantity }}</td>
<td class="text-right pr-1">{{ row.amount }}</td>
</tr>
<tr>
<td colspan="5" style="padding: 4%"></td>
</tr>
<tr>
<td colspan="2" class="text-left pl-1"></td>
<td colspan="2" :style="[$.bold, $.fontColor]" class="text-left pl-0">SUBTOTAL</td>
<td :style="$.bold" class="text-right pr-1">{{ doc.netTotal }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="[$.bold, $.fontColor]"
class="text-left pl-0"
>{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.bold" class="text-right pr-1">{{ tax.amount }}</td>
</tr>
<tr>
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="[$.bold, $.fontColor, $.mediumFontSize]"
class="text-left pl-0"
>TOTAL</td>
<td :style="[$.bold, $.mediumFontSize]" class="text-right pr-1">{{ doc.grandTotal }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row pl-5 pr-5 mt-5">
<div class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showNoticeBorderBottom]">
<td :style="$.hideBorderTop" class="pl-0">NOTICE</td>
</tr>
<tr>
<td class="pl-0">{{ doc.terms }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'font'],
components: {
CompanyAddress,
CustomerAddress
},
data() {
return {
$: Styles
};
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.fontColor.color = this.themeColor;
this.$.headerColor.backgroundColor = this.themeColor;
this.$.showBorderBottom.borderBottom = '0.1rem solid #e0e0d1';
this.$.showNoticeBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.showBorderTop.borderTop = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
};
</script>

View File

@ -1,145 +0,0 @@
<template>
<div :style="[$.regularFontSize, $.font]" style="font-family: sans-serif;">
<div class="row no-gutters mt-5">
<div class="col-6" :style="$.bgColor"></div>
<div class="col-4 text-center" style="vertical-align: middle">
<h1>INVOICE</h1>
</div>
<div class="col-2" :style="$.bgColor"></div>
</div>
<div class="row no-gutters mt-5">
<div class="col-6 text-left pl-5">
<company-address />
</div>
<div class="col-6 pr-5 text-right">
<p :style="[$.bold, $.paraStyle, $.mediumFontSize]">{{ doc.name }}</p>
<p :style="$.paraStyle">{{ frappe.format(doc.date, 'Date') }}</p>
</div>
</div>
<div class="row no-gutters mt-5">
<div class="col-6 text-left pl-5">
<customer-address :customer="doc.customer" />
</div>
<div class="col-6"></div>
</div>
<div class="row mt-5 no-gutters">
<div class="col-12">
<table class="table">
<tbody>
<tr>
<td
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 15"
class="pl-5"
>{{ _("NO") }}</td>
<td
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 40%"
>{{ _("ITEM") }}</td>
<td
class="text-left"
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 20%"
>{{ _("RATE") }}</td>
<td
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 10%"
>{{ _("QTY") }}</td>
<td
class="text-right pr-5"
:style="[$.bold, $.tablePadding]"
style="width: 20%"
>{{ _("AMOUNT") }}</td>
</tr>
<tr v-for="row in doc.items" :key="row.idx">
<td :style="$.tablePadding" class="pl-5 pr-5">{{ row.idx + 1 }}</td>
<td :style="$.tablePadding">{{ row.item }}</td>
<td
:style="$.tablePadding"
class="text-left"
>{{ frappe.format(row.rate, 'Currency') }}</td>
<td :style="$.tablePadding">{{ row.quantity }}</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ row.amount }}</td>
</tr>
<tr>
<td colspan="5" style="padding: 4%"></td>
</tr>
<tr>
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="$.tablePadding" colspan="2">SUBTOTAL</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ doc.netTotal }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td
:style="$.tablePadding"
med
colspan="2"
>{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ tax.amount }}</td>
</tr>
<tr>
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="[$.bold, $.tablePadding, $.mediumFontSize]" colspan="2">TOTAL</td>
<td
:style="[$.bold, $.tablePadding, $.mediumFontSize]"
class="text-right pr-5"
>{{ doc.grandTotal }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row mt-5">
<div :style="$.regularFontSize" class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showBorderBottom]">
<td :style="$.hideBorderTop" class="pl-5">NOTICE</td>
</tr>
<tr>
<td class="pl-5">{{ doc.terms }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'font'],
components: {
CompanyAddress,
CustomerAddress
},
data() {
return {
$: Styles
};
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.bgColor.backgroundColor = this.themeColor;
this.$.showBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
};
</script>

View File

@ -0,0 +1,121 @@
<template>
<div class="bg-white border" :style="{ 'font-family': printSettings.font }">
<div class="flex items-center justify-between px-12 py-10 border-b">
<div class="flex items-center">
<div class="flex items-center rounded h-16">
<div class="mr-4" v-if="printSettings.displayLogo">
<img
class="h-12 max-w-32 object-contain"
:src="printSettings.logo"
/>
</div>
</div>
<div>
<div
class="font-semibold text-xl"
:style="{ color: printSettings.color }"
>
{{ frappe.AccountingSettings.companyName }}
</div>
<div>
{{ frappe.format(doc.date, 'Date') }}
</div>
</div>
</div>
<div class="text-right">
<div
class="font-semibold text-xl"
:style="{ color: printSettings.color }"
>
{{ doc.doctype === 'SalesInvoice' ? 'Invoice' : 'Bill' }}
</div>
<div>
{{ doc.name }}
</div>
</div>
</div>
<div class="flex px-12 py-10 border-b">
<div class="w-1/2" v-if="party">
<div
class="uppercase text-sm font-semibold tracking-widest text-gray-800"
>
To
</div>
<div class="mt-4 text-black leading-relaxed text-lg">
{{ party.name }} <br />
{{ party.addressDisplay }}
</div>
</div>
<div class="w-1/2" v-if="companyAddress">
<div
class="uppercase text-sm font-semibold tracking-widest text-gray-800"
>
From
</div>
<div class="mt-4 text-black leading-relaxed text-lg">
{{ companyAddress.addressDisplay }}
</div>
</div>
</div>
<div class="px-12 py-10 border-b">
<div
class="mb-4 flex uppercase text-sm tracking-widest font-semibold text-gray-800"
>
<div class="w-4/12">Item</div>
<div class="w-2/12 text-right">Quantity</div>
<div class="w-3/12 text-right">Rate</div>
<div class="w-3/12 text-right">Amount</div>
</div>
<div class="flex py-1 text-lg" v-for="row in doc.items" :key="row.name">
<div class="w-4/12">{{ row.item }}</div>
<div class="w-2/12 text-right">{{ format(row, 'quantity') }}</div>
<div class="w-3/12 text-right">{{ format(row, 'rate') }}</div>
<div class="w-3/12 text-right">{{ format(row, 'amount') }}</div>
</div>
</div>
<div class="flex px-12 py-10">
<div class="w-1/2">
<template v-if="doc.terms">
<div
class="uppercase text-sm tracking-widest font-semibold text-gray-800"
>
Notes
</div>
<div class="mt-4 text-lg whitespace-pre-line">
{{ doc.terms }}
</div>
</template>
</div>
<div class="w-1/2 text-lg">
<div class="flex pl-2 justify-between py-1">
<div>{{ _('Subtotal') }}</div>
<div>{{ frappe.format(doc.netTotal, 'Currency') }}</div>
</div>
<div
class="flex pl-2 justify-between py-1"
v-for="tax in doc.taxes"
:key="tax.name"
>
<div>{{ tax.account }} ({{ tax.rate }}%)</div>
<div>{{ frappe.format(tax.amount, 'Currency') }}</div>
</div>
<div
class="flex pl-2 justify-between py-1 font-semibold"
:style="{ color: printSettings.color }"
>
<div>{{ _('Grand Total') }}</div>
<div>{{ frappe.format(doc.grandTotal, 'Currency') }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Base from './Base';
export default {
name: 'Minimal',
extends: Base
};
</script>

View File

@ -20,6 +20,7 @@
"@popperjs/core": "^2.0.3", "@popperjs/core": "^2.0.3",
"core-js": "^3.4.3", "core-js": "^3.4.3",
"electron-store": "^5.1.0", "electron-store": "^5.1.0",
"font-manager": "https://github.com/q-lukasz/font-manager#issue-45--compile-error-mac-os-x--node-13-7",
"frappe-charts": "^1.3.0", "frappe-charts": "^1.3.0",
"frappejs": "https://github.com/frappe/frappejs", "frappejs": "https://github.com/frappe/frappejs",
"knex": "^0.20.4", "knex": "^0.20.4",

View File

@ -3,16 +3,13 @@
<PageHeader> <PageHeader>
<BackLink slot="title" /> <BackLink slot="title" />
<template slot="actions"> <template slot="actions">
<Button class="text-gray-900 text-xs" @click="openInvoiceSettings">
{{ _('Customise') }}
</Button>
<Button <Button
v-if="doc.submitted" v-if="doc.submitted"
class="text-gray-900 text-xs ml-2" class="text-gray-900 text-xs ml-2"
:icon="true" :icon="true"
@click="$router.push(`/print/${doc.doctype}/${doc.name}`)" @click="$router.push(`/print/${doc.doctype}/${doc.name}`)"
> >
<feather-icon name="printer" class="w-4 h-4" /> Print
</Button> </Button>
<DropdownWithActions class="ml-2" :actions="actions" /> <DropdownWithActions class="ml-2" :actions="actions" />
<Button <Button
@ -32,11 +29,7 @@
> >
</template> </template>
</PageHeader> </PageHeader>
<div <div class="flex justify-center flex-1 mb-8 mt-2" v-if="meta">
class="flex justify-center flex-1 mb-8 mt-6"
v-if="meta"
:class="doc.submitted && 'pointer-events-none'"
>
<div <div
class="border rounded-lg shadow h-full flex flex-col justify-between" class="border rounded-lg shadow h-full flex flex-col justify-between"
style="width: 600px" style="width: 600px"

View File

@ -1,37 +1,56 @@
<template> <template>
<div class="flex flex-col"> <div class="flex">
<PageHeader> <div class="flex flex-col flex-1">
<a <PageHeader class="bg-white z-10">
class="cursor-pointer font-semibold flex items-center" <BackLink slot="title" />
slot="title"
@click="$router.back()"
>
<feather-icon name="chevron-left" class="w-5 h-5" />
<span class="ml-1">{{ _('Back') }}</span>
</a>
<template slot="actions"> <template slot="actions">
<Button
class="text-gray-900 text-xs ml-2"
@click="showCustomiser = !showCustomiser"
>
{{ _('Customise') }}
</Button>
<Button class="text-gray-900 text-xs ml-2" @click="makePDF"> <Button class="text-gray-900 text-xs ml-2" @click="makePDF">
{{ _('Save as PDF') }} {{ _('Save as PDF') }}
</Button> </Button>
</template> </template>
</PageHeader> </PageHeader>
<div class="flex justify-center flex-1 mb-8 mt-6">
<div <div
v-if="doc" v-if="doc && printSettings"
class="border rounded-lg shadow h-full flex flex-col justify-between" class="flex justify-center flex-1 -mt-32 overflow-auto relative"
style="width: 600px" >
<div
class="h-full shadow-lg mb-12 absolute"
style="width: 21cm; min-height: 29.7cm; height: max-content; transform: scale(0.755);"
ref="printContainer" ref="printContainer"
> >
<component :is="printTemplate" v-bind="{ doc }" /> <component
class="flex-1"
:is="printTemplate"
v-bind="{ doc, printSettings }"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="border-l w-80" v-if="showCustomiser">
<div class="mt-4 px-4 flex items-center justify-between">
<h2 class="font-semibold">{{ _('Customise') }}</h2>
<Button :icon="true" @click="showCustomiser = false">
<feather-icon name="x" class="w-4 h-4" />
</Button>
</div>
<TwoColumnForm class="mt-4" :doc="printSettings" :autosave="true" />
</div>
</div>
</template> </template>
<script> <script>
import frappe from 'frappejs';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar'; import SearchBar from '@/components/SearchBar';
import DropdownWithAction from '@/components/DropdownWithAction'; import DropdownWithAction from '@/components/DropdownWithAction';
import Button from '@/components/Button'; import Button from '@/components/Button';
import BackLink from '@/components/BackLink';
import TwoColumnForm from '@/components/TwoColumnForm';
import { makePDF } from '@/utils'; import { makePDF } from '@/utils';
import { remote } from 'electron'; import { remote } from 'electron';
@ -42,15 +61,20 @@ export default {
PageHeader, PageHeader,
SearchBar, SearchBar,
DropdownWithAction, DropdownWithAction,
Button Button,
BackLink,
TwoColumnForm
}, },
data() { data() {
return { return {
doc: null doc: null,
showCustomiser: false,
printSettings: null
}; };
}, },
async mounted() { async mounted() {
this.doc = await frappe.getDoc(this.doctype, this.name); this.doc = await frappe.getDoc(this.doctype, this.name);
this.printSettings = await frappe.getSingle('PrintSettings');
}, },
computed: { computed: {
meta() { meta() {
@ -66,7 +90,6 @@ export default {
let html = this.$refs.printContainer.innerHTML; let html = this.$refs.printContainer.innerHTML;
makePDF(html, destination); makePDF(html, destination);
}, },
getSavePath() { getSavePath() {
return new Promise(resolve => { return new Promise(resolve => {
remote.dialog.showSaveDialog( remote.dialog.showSaveDialog(

View File

@ -1,5 +1,5 @@
@font-face { @font-face {
font-family: 'Inter var experimental'; font-family: 'Inter';
font-weight: 100 900; font-weight: 100 900;
font-display: swap; font-display: swap;
font-style: oblique 0deg 10deg; font-style: oblique 0deg 10deg;

View File

@ -262,7 +262,7 @@ export function getActionsForDocument(doc) {
component: { component: {
template: `<span class="text-red-700">{{ _('Delete') }}</span>` template: `<span class="text-red-700">{{ _('Delete') }}</span>`
}, },
condition: doc => !doc.isNew() && !doc.submitted, condition: doc => !doc.isNew() && !doc.submitted && !doc.meta.isSingle,
action: () => action: () =>
deleteDocWithPrompt(doc).then(res => { deleteDocWithPrompt(doc).then(res => {
if (res) { if (res) {

View File

@ -1,7 +1,7 @@
module.exports = { module.exports = {
theme: { theme: {
fontFamily: { fontFamily: {
sans: ['Inter var experimental', 'sans-serif'] sans: ['Inter', 'sans-serif']
}, },
fontSize: { fontSize: {
xs: '11px', xs: '11px',
@ -29,6 +29,7 @@ module.exports = {
'7': '1.75rem', '7': '1.75rem',
'14': '3.5rem', '14': '3.5rem',
'18': '4.5rem', '18': '4.5rem',
'28': '7rem',
'72': '18rem', '72': '18rem',
'80': '20rem' '80': '20rem'
}, },

View File

@ -4830,6 +4830,12 @@ follow-redirects@^1.0.0:
dependencies: dependencies:
debug "^3.0.0" debug "^3.0.0"
"font-manager@https://github.com/q-lukasz/font-manager#issue-45--compile-error-mac-os-x--node-13-7":
version "0.3.1"
resolved "https://github.com/q-lukasz/font-manager#523d3b3c2b535a5003c2a631bf6d2ec342cc6c8f"
dependencies:
nan ">=2.14.0"
for-in@^1.0.1, for-in@^1.0.2: for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -7288,7 +7294,7 @@ mz@^2.4.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nan@^2.12.1: nan@>=2.14.0, nan@^2.12.1:
version "2.14.0" version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==