mirror of
https://github.com/frappe/books.git
synced 2024-11-14 09:24:04 +00:00
feat: Print
- PrintPreview - print.html bundle for print - Download PDF
This commit is contained in:
parent
32b3793793
commit
69cb2447d8
@ -1,9 +1,9 @@
|
|||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
staticPath: './static',
|
staticPath: './static',
|
||||||
distPath: './dist',
|
distPath: './dist',
|
||||||
dev: {
|
dev: {
|
||||||
entryHtml: 'src/index.html',
|
|
||||||
entry: {
|
entry: {
|
||||||
app: './src/main.js'
|
app: './src/main.js'
|
||||||
},
|
},
|
||||||
@ -11,9 +11,9 @@ module.exports = {
|
|||||||
srcDir: './src',
|
srcDir: './src',
|
||||||
outputDir: './dist',
|
outputDir: './dist',
|
||||||
assetsPublicPath: '/',
|
assetsPublicPath: '/',
|
||||||
devServerPort: 8000,
|
devServerPort: 8080,
|
||||||
env: {
|
env: {
|
||||||
PORT: process.env.PORT || 8000
|
PORT: process.env.PORT || 8080
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
@ -23,12 +23,22 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
electron: {
|
electron: {
|
||||||
entry: {
|
entry: {
|
||||||
app: './src/main-electron.js'
|
app: './src/main-electron.js',
|
||||||
|
print: './src/print.js'
|
||||||
},
|
},
|
||||||
paths: {
|
paths: {
|
||||||
mainDev: 'src-electron/main.dev.js',
|
mainDev: 'src-electron/main.dev.js',
|
||||||
main: 'src-electron/main.js',
|
main: 'src-electron/main.js',
|
||||||
renderer: 'src/electron.js'
|
renderer: 'src/electron.js'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
configureWebpack(config) {
|
||||||
|
config.plugins.push(
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
chunks: ['print'],
|
||||||
|
filename: 'static/print.html',
|
||||||
|
template: 'src/print.html'
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
153
models/doctype/SalesInvoice/InvoiceTemplate.vue
Normal file
153
models/doctype/SalesInvoice/InvoiceTemplate.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<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" :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" 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 Row from '@/components/Row';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InvoiceTemplate',
|
||||||
|
props: ['doc'],
|
||||||
|
components: {
|
||||||
|
Row
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
accountingSettings: null,
|
||||||
|
printSettings: 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 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>
|
@ -1,5 +1,7 @@
|
|||||||
const frappe = require('frappejs');
|
const frappe = require('frappejs');
|
||||||
const utils = require('../../../accounting/utils');
|
const utils = require('../../../accounting/utils');
|
||||||
|
const router = require('@/router').default;
|
||||||
|
const InvoiceTemplate = require('./InvoiceTemplate.vue').default;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'SalesInvoice',
|
name: 'SalesInvoice',
|
||||||
@ -112,7 +114,7 @@ module.exports = {
|
|||||||
formula: doc => {
|
formula: doc => {
|
||||||
if (doc.submitted) return;
|
if (doc.submitted) return;
|
||||||
return doc.grandTotal;
|
return doc.grandTotal;
|
||||||
},
|
},
|
||||||
readOnly: 1
|
readOnly: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -178,7 +180,7 @@ module.exports = {
|
|||||||
date: new Date().toISOString().slice(0, 10),
|
date: new Date().toISOString().slice(0, 10),
|
||||||
paymentType: 'Receive',
|
paymentType: 'Receive',
|
||||||
for: [
|
for: [
|
||||||
{
|
{
|
||||||
referenceType: doc.doctype,
|
referenceType: doc.doctype,
|
||||||
referenceName: doc.name,
|
referenceName: doc.name,
|
||||||
amount: doc.outstandingAmount
|
amount: doc.outstandingAmount
|
||||||
@ -197,7 +199,14 @@ module.exports = {
|
|||||||
doc.revert();
|
doc.revert();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
|
label: 'Print',
|
||||||
|
condition: doc => doc.submitted,
|
||||||
|
action(doc) {
|
||||||
|
router.push(`/print/${doc.doctype}/${doc.name}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
utils.ledgerLink
|
utils.ledgerLink
|
||||||
]
|
],
|
||||||
|
printTemplate: InvoiceTemplate
|
||||||
};
|
};
|
||||||
|
22
src/components/DropdownWithAction.vue
Normal file
22
src/components/DropdownWithAction.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<Dropdown v-bind="$attrs">
|
||||||
|
<template v-slot="{ toggleDropdown }">
|
||||||
|
<div @click="toggleDropdown()">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropdown from './Dropdown';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DropdownWithAction',
|
||||||
|
components: {
|
||||||
|
Dropdown
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
103
src/pages/PrintView/PrintView.vue
Normal file
103
src/pages/PrintView/PrintView.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<PageHeader>
|
||||||
|
<a
|
||||||
|
class="cursor-pointer font-semibold flex items-center"
|
||||||
|
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">
|
||||||
|
<DropdownWithAction class="text-base" :items="actions" right>
|
||||||
|
<Button class="text-gray-900 text-xs ml-2" :icon="true">
|
||||||
|
<feather-icon name="more-horizontal" class="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownWithAction>
|
||||||
|
</template>
|
||||||
|
</PageHeader>
|
||||||
|
<div class="flex justify-center flex-1 mb-8 mt-6">
|
||||||
|
<div
|
||||||
|
v-if="doc"
|
||||||
|
class="border rounded-lg shadow h-full flex flex-col justify-between"
|
||||||
|
style="width: 600px"
|
||||||
|
ref="printContainer"
|
||||||
|
>
|
||||||
|
<component :is="printTemplate" v-bind="{ doc }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
import SearchBar from '@/components/SearchBar';
|
||||||
|
import DropdownWithAction from '@/components/DropdownWithAction';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { getPDFForElectron } from 'frappejs/server/pdf';
|
||||||
|
import { remote } from 'electron';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PrintView',
|
||||||
|
props: ['doctype', 'name'],
|
||||||
|
components: {
|
||||||
|
PageHeader,
|
||||||
|
SearchBar,
|
||||||
|
DropdownWithAction,
|
||||||
|
Button
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
doc: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
meta() {
|
||||||
|
return frappe.getMeta(this.doctype);
|
||||||
|
},
|
||||||
|
printTemplate() {
|
||||||
|
return this.meta.printTemplate;
|
||||||
|
},
|
||||||
|
actions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Download PDF',
|
||||||
|
action: () => {
|
||||||
|
this.makePDF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async makePDF() {
|
||||||
|
let destination = await this.getSavePath();
|
||||||
|
let html = this.$refs.printContainer.innerHTML;
|
||||||
|
getPDFForElectron(this.doctype, this.name, destination, html);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSavePath() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
remote.dialog.showSaveDialog(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
|
title: this._('Select folder'),
|
||||||
|
defaultPath: `${this.name}.pdf`
|
||||||
|
},
|
||||||
|
filePath => {
|
||||||
|
if (filePath) {
|
||||||
|
if (!filePath.endsWith('.pdf')) {
|
||||||
|
filePath = filePath + '.pdf';
|
||||||
|
}
|
||||||
|
resolve(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,93 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="bg-light">
|
|
||||||
<page-header :breadcrumbs="breadcrumbs" />
|
|
||||||
<component :is="printComponent" v-if="doc" :doc="doc" @send="send" @makePDF="makePDF" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import PageHeader from '@/components/PageHeader';
|
|
||||||
import SalesInvoicePrint from '@/../models/doctype/SalesInvoice/SalesInvoicePrint';
|
|
||||||
import GSTR3BPrintView from '@/../models/doctype/GSTR3B/GSTR3BPrintView';
|
|
||||||
import EmailSend from '../Email/EmailSend';
|
|
||||||
|
|
||||||
const printComponents = {
|
|
||||||
SalesInvoice: SalesInvoicePrint,
|
|
||||||
GSTR3B: GSTR3BPrintView
|
|
||||||
};
|
|
||||||
export default {
|
|
||||||
name: 'PrintView',
|
|
||||||
props: ['doctype', 'name'],
|
|
||||||
components: {
|
|
||||||
PageHeader
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
breadcrumbs() {
|
|
||||||
if (this.doc)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: this.meta.label || this.doctype,
|
|
||||||
route: '#/list/' + this.doctype
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: this.doc._notInserted
|
|
||||||
? 'New ' + this.meta.label || this.doctype
|
|
||||||
: this.doc.name,
|
|
||||||
route: `#/edit/${this.doctype}/${this.name}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Print',
|
|
||||||
route: ``
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
meta() {
|
|
||||||
return frappe.getMeta(this.doctype);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
doc: undefined,
|
|
||||||
printComponent: undefined,
|
|
||||||
showInvoiceCustomizer: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
|
||||||
this.printComponent = printComponents[this.doctype];
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
makePDF(html) {
|
|
||||||
frappe.call({
|
|
||||||
method: 'print-pdf',
|
|
||||||
args: {
|
|
||||||
doctype: this.doctype,
|
|
||||||
name: this.name,
|
|
||||||
html
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async send(html) {
|
|
||||||
let doc = await frappe.getNewDoc('Email');
|
|
||||||
let emailFields = frappe.getMeta('Email').fields;
|
|
||||||
var file_path = this.name;
|
|
||||||
doc['fromEmailAddress'] = this.selectedId;
|
|
||||||
this.makePDF(html);
|
|
||||||
doc['filePath'] = this.name + '.pdf';
|
|
||||||
this.$modal.show({
|
|
||||||
component: EmailSend,
|
|
||||||
props: {
|
|
||||||
doctype: doc.doctype,
|
|
||||||
name: doc.name
|
|
||||||
},
|
|
||||||
modalProps: {
|
|
||||||
title: `Send ${this.doctype}`,
|
|
||||||
footerMessage: `${this.doctype} attached along..`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
doc.on('afterInsert', data => {
|
|
||||||
this.$modal.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -6,7 +6,6 @@
|
|||||||
:value="doc.logo"
|
:value="doc.logo"
|
||||||
@change="
|
@change="
|
||||||
value => {
|
value => {
|
||||||
window.console.log(value)
|
|
||||||
doc.set('logo', value);
|
doc.set('logo', value);
|
||||||
doc.update();
|
doc.update();
|
||||||
}
|
}
|
||||||
|
13
src/print.html
Normal file
13
src/print.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Print</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="printTarget"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
1
src/print.js
Normal file
1
src/print.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import './styles/index.css';
|
@ -4,7 +4,7 @@ import Router from 'vue-router';
|
|||||||
import ListView from '@/pages/ListView/ListView';
|
import ListView from '@/pages/ListView/ListView';
|
||||||
import Dashboard from '@/pages/Dashboard/Dashboard';
|
import Dashboard from '@/pages/Dashboard/Dashboard';
|
||||||
import FormView from '@/pages/FormView/FormView';
|
import FormView from '@/pages/FormView/FormView';
|
||||||
import PrintView from '@/pages/PrintView';
|
import PrintView from '@/pages/PrintView/PrintView';
|
||||||
import QuickEditForm from '@/pages/QuickEditForm';
|
import QuickEditForm from '@/pages/QuickEditForm';
|
||||||
|
|
||||||
import Report from '@/pages/Report.vue';
|
import Report from '@/pages/Report.vue';
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Frappe Accounting - Print</title>
|
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue
Block a user