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:
parent
5316756843
commit
138b0673a8
@ -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'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -119,7 +119,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'terms',
|
fieldname: 'terms',
|
||||||
label: 'Terms',
|
label: 'Notes',
|
||||||
fieldtype: 'Text'
|
fieldtype: 'Text'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -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
|
|
||||||
}
|
|
25
models/doctype/SalesInvoice/Templates/Base.vue
Normal file
25
models/doctype/SalesInvoice/Templates/Base.vue
Normal 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>
|
125
models/doctype/SalesInvoice/Templates/Business.vue
Normal file
125
models/doctype/SalesInvoice/Templates/Business.vue
Normal 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>
|
@ -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>
|
|
@ -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>
|
|
169
models/doctype/SalesInvoice/Templates/Default.vue
Normal file
169
models/doctype/SalesInvoice/Templates/Default.vue
Normal 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>
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
121
models/doctype/SalesInvoice/Templates/Minimal.vue
Normal file
121
models/doctype/SalesInvoice/Templates/Minimal.vue
Normal 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>
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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'
|
||||||
},
|
},
|
||||||
|
@ -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==
|
||||||
|
Loading…
Reference in New Issue
Block a user