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

Merge pull request #220 from 18alantom/fix-reported-issues-and-improvements

fix: reported issues and improvements
This commit is contained in:
Alan 2021-10-19 14:47:37 +05:30 committed by GitHub
commit c8732d6d8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 238 additions and 228 deletions

View File

@ -97,8 +97,23 @@ module.exports = {
return doc.getFrom('Address', doc.address, 'addressDisplay');
}
}
},
{
fieldname: 'gstin',
label: 'GSTIN No.',
fieldtype: 'Data',
hidden: form => {
return form.gstType === 'Registered Regular' ? 0 : 1;
}
}
],
quickEditFields: ['email', 'phone', 'address', 'defaultAccount', 'currency']
quickEditFields: [
'email',
'phone',
'address',
'defaultAccount',
'currency',
'gstin'
]
};

View File

@ -102,7 +102,7 @@ module.exports = {
label: 'Amount',
fieldtype: 'Currency',
required: 1,
default: doc => doc.getSum('for', 'amount')
formula: doc => doc.getSum('for', 'amount')
},
{
fieldname: 'writeoff',

View File

@ -19,6 +19,9 @@ export default {
computed: {
partyField() {
return this.doc.doctype === 'SalesInvoice' ? 'customer' : 'supplier';
},
isSalesInvoice() {
return this.doc.doctype === 'SalesInvoice';
}
}
};

View File

@ -1,7 +1,7 @@
<template>
<div class="border h-full">
<div>
<div class="px-6 pt-6" v-if="printSettings && accountingSettings">
<div class="px-6 pt-6" v-if="printSettings">
<div class="flex text-sm text-gray-900 border-b pb-4">
<div class="w-1/3">
<div v-if="printSettings.displayLogo">
@ -11,7 +11,7 @@
/>
</div>
<div class="text-xl text-gray-700 font-semibold" v-else>
{{ accountingSettings.companyName }}
{{ frappe.AccountingSettings.companyName }}
</div>
</div>
<div class="w-1/3">
@ -19,7 +19,10 @@
<div class="mt-1">{{ printSettings.phone }}</div>
</div>
<div class="w-1/3">
<div v-if="address">{{ address.addressDisplay }}</div>
<div v-if="companyAddress">{{ companyAddress.addressDisplay }}</div>
<div v-if="printSettings && printSettings.gstin">
GSTIN: {{ printSettings.gstin }}
</div>
</div>
</div>
</div>
@ -33,60 +36,62 @@
{{ frappe.format(doc.date, 'Date') }}
</div>
</div>
<div class="w-1/3">
<div class="w-1/3" v-if="party">
<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 }}
{{ party.name }}
</div>
<div
v-if="partyDoc && partyDoc.gstin"
v-if="party && party.addressDisplay"
class="mt-1 text-xs text-gray-600 text-right"
>
GSTIN: {{ partyDoc.gstin }}
{{ party.addressDisplay }}
</div>
<div
v-if="party && party.gstin"
class="mt-1 text-xs text-gray-600 text-right"
>
GSTIN: {{ party.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"
<div class="text-gray-600 w-full flex border-b">
<div class="py-4 w-5/12">Item</div>
<div class="py-4 text-right w-1/12">Quantity</div>
<div class="py-4 text-right w-3/12">Rate</div>
<div class="py-4 text-right w-3/12">Amount</div>
</div>
<div
class="flex py-1 text-gray-900 w-full border-b"
v-for="row in doc.items"
:key="row.name"
:ratio="ratio"
>
<div class="py-4">
{{ row.idx + 1 }}
<div class="w-5/12 py-4">{{ row.item }}</div>
<div class="w-1/12 text-right py-4">
{{ format(row, 'quantity') }}
</div>
<div
class="py-4"
v-for="df in itemFields"
:key="df.fieldname"
:class="textAlign(df)"
>
{{ frappe.format(row[df.fieldname], df) }}
<div class="w-3/12 text-right py-4">{{ format(row, 'rate') }}</div>
<div class="w-3/12 text-right py-4">
{{ format(row, 'amount') }}
</div>
</Row>
</div>
</div>
</div>
</div>
<div class="px-6 mt-2 flex justify-end text-base">
<div class="w-64">
<div class="w-1/2 bg-pink">
<div
class="uppercase text-sm tracking-widest font-semibold text-gray-800 mt-2"
>
Notes
</div>
<div class="my-4 text-lg whitespace-pre-line">
{{ doc.terms }}
</div>
</div>
<div class="w-1/2">
<div class="flex pl-2 justify-between py-3 border-b">
<div>{{ _('Subtotal') }}</div>
<div>{{ frappe.format(doc.netTotal, 'Currency') }}</div>
@ -111,57 +116,11 @@
</template>
<script>
import frappe from 'frappejs';
import Row from '@/components/Row';
import Base from './Base';
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);
}
extends: Base,
props: ['doc', 'printSettings']
};
</script>

View File

@ -23,12 +23,18 @@
<div class="text-sm text-gray-800" v-if="companyAddress">
{{ companyAddress.addressDisplay }}
</div>
<div
class="text-sm text-gray-800"
v-if="printSettings && printSettings.gstin"
>
GSTIN: {{ printSettings.gstin }}
</div>
</div>
</div>
<div class="mt-8 text-lg">
<div class="flex">
<div class="w-1/3 font-semibold">
{{ doc.doctype === 'SalesInvoice' ? 'Invoice' : 'Bill' }}
{{ isSalesInvoice ? 'Invoice' : 'Bill' }}
</div>
<div class="w-2/3 text-gray-800">
<div class="font-semibold">
@ -41,7 +47,7 @@
</div>
<div class="mt-4 flex">
<div class="w-1/3 font-semibold">
{{ doc.doctype === 'SalesInvoice' ? 'Customer' : 'Supplier' }}
{{ isSalesInvoice ? 'Customer' : 'Supplier' }}
</div>
<div class="w-2/3 text-gray-800" v-if="party">
<div class="font-semibold">
@ -50,6 +56,7 @@
<div>
{{ party.addressDisplay }}
</div>
<div v-if="party && party.gstin">GSTIN: {{ party.gstin }}</div>
</div>
</div>
</div>

View File

@ -42,22 +42,34 @@
<div
class="uppercase text-sm font-semibold tracking-widest text-gray-800"
>
To
{{ isSalesInvoice ? 'To' : 'From' }}
</div>
<div class="mt-4 text-black leading-relaxed text-lg">
{{ party.name }} <br />
{{ party.addressDisplay }}
</div>
<div
class="mt-4 text-black leading-relaxed text-lg"
v-if="party && party.gstin"
>
GSTIN: {{ party.gstin }}
</div>
</div>
<div class="w-1/2" v-if="companyAddress">
<div
class="uppercase text-sm font-semibold tracking-widest text-gray-800"
class="uppercase text-sm font-semibold tracking-widest text-gray-800 ml-8"
>
From
{{ isSalesInvoice ? 'From' : 'To' }}
</div>
<div class="mt-4 text-black leading-relaxed text-lg">
<div class="mt-4 ml-8 text-black leading-relaxed text-lg">
{{ companyAddress.addressDisplay }}
</div>
<div
class="mt-4 ml-8 text-black leading-relaxed text-lg"
v-if="printSettings && printSettings.gstin"
>
GSTIN: {{ printSettings.gstin }}
</div>
</div>
</div>
<div class="px-12 py-10 border-b">

View File

@ -25,14 +25,15 @@ module.exports = {
label: 'Description',
fieldtype: 'Text',
formula: (row, doc) => doc.getFrom('Item', row.item, 'description'),
hidden: 1
hidden: 1,
formulaDependsOn: ['item']
},
{
fieldname: 'quantity',
label: 'Quantity',
fieldtype: 'Float',
required: 1,
formula: () => 1
formula: row => row.quantity || 1
},
{
fieldname: 'rate',
@ -43,7 +44,8 @@ module.exports = {
let baseRate = await doc.getFrom('Item', row.item, 'rate');
return baseRate / doc.exchangeRate;
},
getCurrency: (row, doc) => doc.currency
getCurrency: (row, doc) => doc.currency,
formulaDependsOn: ['item']
},
{
fieldname: 'baseRate',
@ -66,10 +68,8 @@ module.exports = {
label: 'Tax',
fieldtype: 'Link',
target: 'Tax',
formula: (row, doc) => {
if (row.tax) return row.tax;
return doc.getFrom('Item', row.item, 'tax');
}
formula: (row, doc) => doc.getFrom('Item', row.item, 'tax'),
formulaDependsOn: ['item']
},
{
fieldname: 'amount',

View File

@ -54,23 +54,18 @@ export default {
};
},
methods: {
openFileSelector() {
remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
title: frappe._('Select Image'),
properties: ['openFile'],
filters: [
{ name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'webp'] }
]
},
async files => {
if (files && files[0]) {
let dataURL = await this.getDataURL(files[0]);
this.triggerChange(dataURL);
}
}
);
async openFileSelector() {
const options = {
title: frappe._('Select Image'),
properties: ['openFile'],
filters: [{ name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'webp'] }]
};
const { filePaths } = await remote.dialog.showOpenDialog(options);
if (filePaths && filePaths[0]) {
let dataURL = await this.getDataURL(filePaths[0]);
this.triggerChange(dataURL);
}
},
getDataURL(filePath) {
let fs = require('fs');

View File

@ -9,7 +9,6 @@
:class="['resize-none', inputClasses]"
:value="value"
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
@blur="e => triggerChange(e.target.value)"
@focus="e => $emit('focus', e)"
@input="e => $emit('input', e)"

View File

@ -199,6 +199,13 @@ export default {
},
toValue(date) {
// toISOString is buggy and reduces the day by one
// this is because it considers the UTC timestamp
// in order to circumvent that we need to use luxon/moment
// but that refactor could take some time, so fixing the time difference
// as suggested in this answer.
// https://stackoverflow.com/a/16084846/3541205
date.setHours(0, -date.getTimezoneOffset(), 0, 0);
return date.toISOString().slice(0, 10);
},

View File

@ -1,5 +1,5 @@
<template>
<div class="py-4 px-8 flex justify-between window-drag">
<div class="my-4 mx-4 flex justify-between window-drag">
<div class="window-no-drag">
<slot name="title" />
</div>

View File

@ -63,6 +63,10 @@ import router from './router';
console.error(err, vm, info);
};
process.on('unhandledRejection', error => {
console.error(error);
});
/* eslint-disable no-new */
new Vue({
el: '#app',

View File

@ -1,5 +1,5 @@
<template>
<div class="mt-6">
<div class="w-96 mx-auto mt-4">
<template v-if="hasData">
<div class="flex items-center justify-between">
<div class="font-medium">{{ _('Cashflow') }}</div>
@ -21,7 +21,7 @@
v-else
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 889 240"
class="w-full h-full"
class="w-10/12 h-5/6 ml-4 mr-4 my-2"
>
<defs>
<linearGradient x1="50%" y1="100%" x2="50%" y2=".889%" id="a">

View File

@ -6,17 +6,17 @@
<SearchBar class="ml-2" />
</template>
</PageHeader>
<div class="px-8">
<div class="mx-4">
<div class="border-t" />
<Cashflow />
<div class="my-10 border-t" />
<div class="my-4 mt-0 border-t" />
<UnpaidInvoices />
<div class="my-10 border-t" />
<div class="flex -mx-4">
<div class="w-1/2 px-4">
<div class="my-4 border-t" />
<div class="w-96 flex mx-auto">
<div class="w-1/2 mx-4">
<ProfitAndLoss />
</div>
<div class="w-1/2 px-4">
<div class="w-1/2 mx-4">
<Expenses />
</div>
</div>

View File

@ -30,7 +30,7 @@
<div
v-if="hasData"
class="absolute text-base text-center font-semibold"
style="right: 3.8rem; top: 32%;"
style="right: 6rem; top: 32%;"
>
<div>
{{ frappe.format(totalExpense, 'Currency') }}

View File

@ -1,7 +1,7 @@
<template>
<div class="flex -mx-4">
<div class="w-96 flex justify-between mx-auto">
<div
class="w-1/2 px-4 flex flex-col justify-between"
class="w-5/12 mx-4 flex flex-col justify-between"
v-for="invoice in invoices"
:key="invoice.title"
>

View File

@ -22,11 +22,7 @@
</Button>
</template>
</PageHeader>
<div
v-if="doc"
class="flex justify-center flex-1 mb-8 mt-2"
:class="doc.submitted && 'pointer-events-none'"
>
<div v-if="doc" class="flex justify-center flex-1 mb-8 mt-2">
<div
class="border rounded-lg shadow h-full flex flex-col justify-between"
style="width: 600px"
@ -45,6 +41,8 @@
@change="value => doc.set('entryType', value)"
input-class="bg-gray-100 px-3 py-2 text-base"
:show-label="true"
:read-only="doc.submitted"
:class="doc.submitted && 'pointer-events-none'"
/>
<FormControl
class="mt-2"
@ -54,6 +52,8 @@
@change="value => doc.set('date', value)"
input-class="bg-gray-100 px-3 py-2 text-base"
:show-label="true"
:read-only="doc.submitted"
:class="doc.submitted && 'pointer-events-none'"
/>
</div>
<div class="w-1/3">
@ -64,6 +64,8 @@
@change="value => doc.set('referenceNumber', value)"
input-class="bg-gray-100 p-2 text-base"
:show-label="true"
:read-only="doc.submitted"
:class="doc.submitted && 'pointer-events-none'"
/>
<FormControl
class="mt-2"
@ -73,6 +75,8 @@
@change="value => doc.set('referenceDate', value)"
input-class="bg-gray-100 px-3 py-2 text-base"
:show-label="true"
:read-only="doc.submitted"
:class="doc.submitted && 'pointer-events-none'"
/>
</div>
</div>
@ -82,6 +86,7 @@
:df="meta.getField('accounts')"
:value="doc.accounts"
:showHeader="true"
:max-rows-before-overflow="4"
@change="value => doc.set('accounts', value)"
:read-only="doc.submitted"
/>
@ -97,6 +102,8 @@
:df="meta.getField('userRemark')"
:value="doc.userRemark"
@change="value => doc.set('userRemark', value)"
:class="doc.submitted && 'pointer-events-none'"
:read-only="doc.submitted"
/>
</div>
<div class="text-right font-semibold text-green-700 px-3">

View File

@ -90,24 +90,20 @@ export default {
let html = this.$refs.printContainer.innerHTML;
makePDF(html, destination);
},
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);
}
}
);
});
async getSavePath() {
const options = {
title: this._('Select folder'),
defaultPath: `${this.name}.pdf`
};
let { filePath } = await remote.dialog.showSaveDialog(options);
if (filePath) {
if (!filePath.endsWith('.pdf')) {
filePath = filePath + '.pdf';
}
}
return filePath;
}
}
};

View File

@ -9,59 +9,50 @@ import router from '@/router';
import Avatar from '@/components/Avatar';
import config from '@/config';
export function createNewDatabase() {
return new Promise(resolve => {
remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: _('Select folder'),
defaultPath: 'frappe-books.db'
},
filePath => {
if (filePath) {
if (!filePath.endsWith('.db')) {
filePath = filePath + '.db';
}
if (fs.existsSync(filePath)) {
showMessageDialog({
// prettier-ignore
message: _('A file exists with the same name and it will be overwritten. Are you sure you want to continue?'),
buttons: [
{
label: _('Overwrite'),
action() {
fs.unlinkSync(filePath);
resolve(filePath);
}
},
{ label: _('Cancel'), action() {} }
]
});
} else {
resolve(filePath);
}
}
}
);
});
export async function createNewDatabase() {
const options = {
title: _('Select folder'),
defaultPath: 'frappe-books.db'
};
let { filePath } = await remote.dialog.showSaveDialog(options);
if (filePath) {
if (!filePath.endsWith('.db')) {
filePath = filePath + '.db';
}
if (fs.existsSync(filePath)) {
showMessageDialog({
// prettier-ignore
message: _('A file exists with the same name and it will be overwritten. Are you sure you want to continue?'),
buttons: [
{
label: _('Overwrite'),
action() {
fs.unlinkSync(filePath);
return filePath;
}
},
{ label: _('Cancel'), action() {} }
]
});
} else {
return filePath;
}
}
}
export function loadExistingDatabase() {
return new Promise(resolve => {
remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
title: _('Select file'),
properties: ['openFile'],
filters: [{ name: 'SQLite DB File', extensions: ['db'] }]
},
files => {
if (files && files[0]) {
resolve(files[0]);
}
}
);
});
export async function loadExistingDatabase() {
const options = {
title: _('Select file'),
properties: ['openFile'],
filters: [{ name: 'SQLite DB File', extensions: ['db'] }]
};
let { filePaths } = await remote.dialog.showOpenDialog(options);
if (filePaths && filePaths[0]) {
return filePaths[0]
}
}
export async function connectToLocalDatabase(filepath) {
@ -90,22 +81,25 @@ export async function connectToLocalDatabase(filepath) {
config.set('lastSelectedFilePath', filepath);
}
export function showMessageDialog({ message, description, buttons = [] }) {
export async function showMessageDialog({
message,
description,
buttons = []
}) {
let buttonLabels = buttons.map(a => a.label);
remote.dialog.showMessageBox(
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
message,
detail: description,
buttons: buttonLabels
},
response => {
let button = buttons[response];
if (button && button.action) {
button.action();
}
}
);
let button = buttons[response];
if (button && button.action) {
button.action();
}
}
export function deleteDocWithPrompt(doc) {
@ -203,6 +197,16 @@ export function handleErrorWithDialog(e, doc) {
throw e;
}
// NOTE: a hack to find all the css from the current document and inject it to the print version
// remove this if you are able to fix and get the default css loading on the page
function injectCSS(contents) {
const styles = document.getElementsByTagName('style');
for (let style of styles) {
contents.insertCSS(style.innerHTML);
}
}
export function makePDF(html, destination) {
const { BrowserWindow } = remote;
@ -211,7 +215,8 @@ export function makePDF(html, destination) {
height: 842,
show: false,
webPreferences: {
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
enableRemoteModule: true
}
});
@ -234,23 +239,24 @@ export function makePDF(html, destination) {
printWindow.webContents.executeJavaScript(code);
const printOptions = {
marginsType: 1, // no margin
pageSize: 'A4',
printBackground: true,
printBackgrounds: true,
printSelectionOnly: false
};
return new Promise(resolve => {
printWindow.webContents.on('did-finish-load', () => {
printWindow.webContents.printToPDF(
{
marginsType: 1, // no margin
pageSize: 'A4',
printBackground: true
},
(error, data) => {
injectCSS(printWindow.webContents);
printWindow.webContents.printToPDF(printOptions).then(data => {
printWindow.close();
fs.writeFile(destination, data, error => {
if (error) throw error;
printWindow.close();
fs.writeFile(destination, data, error => {
if (error) throw error;
resolve(shell.openItem(destination));
});
}
);
resolve(shell.openItem(destination));
});
});
});
});
}