mirror of
https://github.com/frappe/books.git
synced 2024-11-13 00:46:28 +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 = {
|
||||
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'
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
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 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
|
||||
};
|
||||
|
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"
|
||||
@change="
|
||||
value => {
|
||||
window.console.log(value)
|
||||
doc.set('logo', value);
|
||||
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 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';
|
||||
|
@ -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