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

feat: Print

- PrintPreview
- print.html bundle for print
- Download PDF
This commit is contained in:
Faris Ansari 2019-11-28 00:07:38 +05:30
parent 32b3793793
commit 69cb2447d8
11 changed files with 321 additions and 117 deletions

View File

@ -1,9 +1,9 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
staticPath: './static',
distPath: './dist',
dev: {
entryHtml: 'src/index.html',
entry: {
app: './src/main.js'
},
@ -11,9 +11,9 @@ module.exports = {
srcDir: './src',
outputDir: './dist',
assetsPublicPath: '/',
devServerPort: 8000,
devServerPort: 8080,
env: {
PORT: process.env.PORT || 8000
PORT: process.env.PORT || 8080
}
},
node: {
@ -23,12 +23,22 @@ module.exports = {
},
electron: {
entry: {
app: './src/main-electron.js'
app: './src/main-electron.js',
print: './src/print.js'
},
paths: {
mainDev: 'src-electron/main.dev.js',
main: 'src-electron/main.js',
renderer: 'src/electron.js'
}
},
configureWebpack(config) {
config.plugins.push(
new HtmlWebpackPlugin({
chunks: ['print'],
filename: 'static/print.html',
template: 'src/print.html'
})
);
}
}
};

View 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>

View File

@ -1,5 +1,7 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
const router = require('@/router').default;
const InvoiceTemplate = require('./InvoiceTemplate.vue').default;
module.exports = {
name: 'SalesInvoice',
@ -112,7 +114,7 @@ module.exports = {
formula: doc => {
if (doc.submitted) return;
return doc.grandTotal;
},
},
readOnly: 1
},
{
@ -178,7 +180,7 @@ module.exports = {
date: new Date().toISOString().slice(0, 10),
paymentType: 'Receive',
for: [
{
{
referenceType: doc.doctype,
referenceName: doc.name,
amount: doc.outstandingAmount
@ -197,7 +199,14 @@ module.exports = {
doc.revert();
}
},
}
{
label: 'Print',
condition: doc => doc.submitted,
action(doc) {
router.push(`/print/${doc.doctype}/${doc.name}`);
}
},
utils.ledgerLink
]
],
printTemplate: InvoiceTemplate
};

View 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>

View 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>

View File

@ -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>

View File

@ -6,7 +6,6 @@
:value="doc.logo"
@change="
value => {
window.console.log(value)
doc.set('logo', value);
doc.update();
}

13
src/print.html Normal file
View 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
View File

@ -0,0 +1 @@
import './styles/index.css';

View File

@ -4,7 +4,7 @@ import Router from 'vue-router';
import ListView from '@/pages/ListView/ListView';
import Dashboard from '@/pages/Dashboard/Dashboard';
import FormView from '@/pages/FormView/FormView';
import PrintView from '@/pages/PrintView';
import PrintView from '@/pages/PrintView/PrintView';
import QuickEditForm from '@/pages/QuickEditForm';
import Report from '@/pages/Report.vue';

View File

@ -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>