2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 14:48:25 +00:00

fix: JournalEntry fixes

- Form actions
- Set debit / credit in 2nd row automatically
- Validate before update and insert
- Add JournalEntry as filter option in General Ledger
- Commonify actions for document in InvoiceForm, JournalEntryForm and QuickEditForm
- DropdownWithActions component
This commit is contained in:
Faris Ansari 2019-12-20 12:14:31 +05:30
parent bd5ee080c2
commit 6e2c5cdf96
9 changed files with 147 additions and 205 deletions

View File

@ -1,15 +1,11 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
const { ledgerLink } = require('../../../accounting/utils');
const { DateTime } = require('luxon');
module.exports = {
label: 'Journal Entry',
name: 'JournalEntry',
doctype: 'DocType',
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: ['name'],
showTitle: true,
settings: 'JournalEntrySettings',
fields: [
{
@ -35,7 +31,8 @@ module.exports = {
{
fieldname: 'date',
label: 'Date',
fieldtype: 'Date'
fieldtype: 'Date',
default: DateTime.local().toISODate()
},
{
fieldname: 'accounts',
@ -61,22 +58,14 @@ module.exports = {
placeholder: 'User Remark'
}
],
layout: [
// section 1
actions: [
{
columns: [{ fields: ['entryType'] }, { fields: ['date'] }]
label: 'Revert',
condition: doc => doc.submitted,
action(doc) {
doc.revert();
}
},
// section 2
{
columns: [{ fields: ['accounts'] }]
},
// section 3
{
columns: [{ fields: ['referenceNumber'] }, { fields: ['referenceDate'] }]
},
// section 4
{
columns: [{ fields: ['userRemark'] }]
}
ledgerLink
]
};

View File

@ -1,4 +1,3 @@
const frappe = require('frappejs');
const BaseDocument = require('frappejs/model/document');
const LedgerPosting = require('../../../accounting/ledgerPosting');
@ -17,7 +16,15 @@ module.exports = class JournalEntryServer extends BaseDocument {
return entries;
}
async afterSubmit() {
beforeUpdate() {
this.getPosting().validateEntries();
}
beforeInsert() {
this.getPosting().validateEntries();
}
async beforeSubmit() {
await this.getPosting().post();
}

View File

@ -1,10 +1,6 @@
module.exports = {
name: 'JournalEntryAccount',
doctype: 'DocType',
isSingle: 0,
isChild: 1,
keywordFields: [],
layout: 'ratio',
fields: [
{
fieldname: 'account',
@ -12,24 +8,34 @@ module.exports = {
fieldtype: 'Link',
target: 'Account',
required: 1,
getFilters: (query, control) => {
if (query)
return {
keywords: ['like', query],
isGroup: 0
};
}
getFilters: () => ({ isGroup: 0 })
},
{
fieldname: 'debit',
label: 'Debit',
fieldtype: 'Currency'
fieldtype: 'Currency',
formula: autoDebitCredit('debit')
},
{
fieldname: 'credit',
label: 'Credit',
fieldtype: 'Currency'
fieldtype: 'Currency',
formula: autoDebitCredit('credit')
}
],
tableFields: ['account', 'debit', 'credit']
};
function autoDebitCredit(type = 'debit') {
let otherType = type === 'debit' ? 'credit' : 'debit';
return (row, doc) => {
if (row[type] == 0) return null;
if (row[otherType]) return null;
let totalType = doc.getSum('accounts', type);
let totalOtherType = doc.getSum('accounts', otherType);
if (totalType < totalOtherType) {
return totalOtherType - totalType;
}
};
}

View File

@ -10,8 +10,9 @@ const viewConfig = {
options: [
{ label: '', value: '' },
{ label: 'Sales Invoice', value: 'SalesInvoice' },
{ label: 'Purchase Invoice', value: 'PurchaseInvoice' },
{ label: 'Payment', value: 'Payment' },
{ label: 'Purchase Invoice', value: 'PurchaseInvoice' }
{ label: 'Journal Entry', value: 'JournalEntry' }
],
size: 'small',
label: 'Reference Type',
@ -70,33 +71,7 @@ const viewConfig = {
{
label: 'Export',
type: 'primary',
action: async report => {
// async function getReportDetails() {
// let [rows, columns] = await report.getReportData(
// report.currentFilters
// );
// let columnData = columns.map(column => {
// return {
// id: column.id,
// content: column.content,
// checked: true
// };
// });
// return {
// title: title,
// rows: rows,
// columnData: columnData
// };
// }
// report.$modal.show({
// modalProps: {
// title: `Export ${title}`,
// noFooter: true
// },
// component: require('../../src/components/ExportWizard').default,
// props: await getReportDetails()
// });
}
action: () => {}
}
],
getColumns() {

View File

@ -0,0 +1,28 @@
<template>
<Dropdown
v-if="actions && actions.length"
class="text-xs"
:items="actions"
right
>
<template v-slot="{ toggleDropdown }">
<Button class="text-gray-900" :icon="true" @click="toggleDropdown()">
<feather-icon name="more-horizontal" class="w-4 h-4" />
</Button>
</template>
</Dropdown>
</template>
<script>
import Dropdown from '@/components/Dropdown';
import Button from '@/components/Button';
export default {
name: 'DropdownWithActions',
props: ['actions'],
components: {
Dropdown,
Button
}
};
</script>

View File

@ -14,22 +14,7 @@
>
<feather-icon name="printer" class="w-4 h-4" />
</Button>
<Dropdown
v-if="actions && actions.length"
class="text-xs"
:items="actions"
right
>
<template v-slot="{ toggleDropdown }">
<Button
class="text-gray-900 text-xs ml-2"
:icon="true"
@click="toggleDropdown()"
>
<feather-icon name="more-horizontal" class="w-4 h-4" />
</Button>
</template>
</Dropdown>
<DropdownWithActions class="ml-2" :actions="actions" />
<Button
v-if="showSave"
type="primary"
@ -171,10 +156,10 @@ import frappe from 'frappejs';
import PageHeader from '@/components/PageHeader';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import Dropdown from '@/components/Dropdown';
import DropdownWithActions from '@/components/DropdownWithActions';
import BackLink from '@/components/BackLink';
import { openSettings } from '@/pages/Settings/utils';
import { deleteDocWithPrompt, handleErrorWithDialog } from '@/utils';
import { handleErrorWithDialog, getActionsForDocument } from '@/utils';
export default {
name: 'InvoiceForm',
@ -183,7 +168,7 @@ export default {
PageHeader,
Button,
FormControl,
Dropdown,
DropdownWithActions,
BackLink
},
provide() {
@ -203,17 +188,6 @@ export default {
meta() {
return frappe.getMeta(this.doctype);
},
itemsMeta() {
return frappe.getMeta(`${this.doctype}Item`);
},
itemTableFields() {
return this.itemsMeta.tableFields.map(fieldname =>
this.itemsMeta.getField(fieldname)
);
},
itemTableColumnRatio() {
return [0.3].concat(this.itemTableFields.map(() => 1));
},
partyField() {
let fieldname = {
SalesInvoice: 'customer',
@ -228,25 +202,7 @@ export default {
return this.doc && (this.doc._notInserted || this.doc._dirty);
},
actions() {
if (!this.doc) return null;
let deleteAction = {
component: {
template: `<span class="text-red-700">{{ _('Delete') }}</span>`
},
condition: doc => !doc.isNew() && !doc.submitted,
action: this.deleteAction
};
let actions = [...(this.meta.actions || []), deleteAction]
.filter(d => (d.condition ? d.condition(this.doc) : true))
.map(d => {
return {
label: d.label,
component: d.component,
action: d.action.bind(this, this.doc, this.$router)
};
});
return actions;
return getActionsForDocument(this.doc);
}
},
async mounted() {
@ -258,7 +214,7 @@ export default {
this.routeToList();
return;
}
throw error;
this.handleError(error);
}
this.printSettings = await frappe.getSingle('PrintSettings');
this.companyName = (
@ -271,23 +227,13 @@ export default {
}
},
methods: {
async addNewItem() {
this.doc.append('items');
},
async onSaveClick() {
await this.doc.set(
'items',
this.doc.items.filter(row => row.item)
);
// await this.doc.set(
// 'items',
// this.doc.items.filter(row => row.item)
// );
return this.doc.insertOrUpdate().catch(this.handleError);
},
deleteAction() {
return deleteDocWithPrompt(this.doc).then(res => {
if (res) {
this.routeToList();
}
});
},
onSubmitClick() {
return this.doc.submit().catch(this.handleError);
},

View File

@ -2,26 +2,29 @@
<div class="flex flex-col">
<PageHeader>
<BackLink slot="title" @click="$router.back()" />
<template slot="actions">
<template slot="actions" v-if="doc">
<DropdownWithActions class="ml-2" :actions="actions" />
<Button
v-if="doc._notInserted || doc._dirty"
type="primary"
class="text-white text-xs ml-2"
@click="onSaveClick"
>{{ _('Save') }}</Button
>
{{ _('Save') }}
</Button>
<Button
v-if="!doc._dirty && !doc._notInserted && !doc.submitted"
type="primary"
class="text-white text-xs ml-2"
@click="onSubmitClick"
>{{ _('Submit') }}</Button
>
{{ _('Submit') }}
</Button>
</template>
</PageHeader>
<div
v-if="doc"
class="flex justify-center flex-1 mb-8 mt-6"
v-if="meta"
:class="doc.submitted && 'pointer-events-none'"
>
<div
@ -109,13 +112,13 @@
</div>
</template>
<script>
import frappe from 'frappejs';
import PageHeader from '@/components/PageHeader';
import Button from '@/components/Button';
import DropdownWithActions from '@/components/DropdownWithActions';
import FormControl from '@/components/Controls/FormControl';
import Row from '@/components/Row';
import Dropdown from '@/components/Dropdown';
import { openQuickEdit } from '@/utils';
import BackLink from '@/components/BackLink';
import { handleErrorWithDialog, getActionsForDocument } from '@/utils';
export default {
name: 'JournalEntryForm',
@ -123,9 +126,8 @@ export default {
components: {
PageHeader,
Button,
DropdownWithActions,
FormControl,
Row,
Dropdown,
BackLink
},
provide() {
@ -137,14 +139,13 @@ export default {
data() {
return {
doctype: 'JournalEntry',
meta: null,
itemsMeta: null,
doc: {},
partyDoc: null,
printSettings: null
doc: null
};
},
computed: {
meta() {
return frappe.getMeta(this.doctype);
},
totalDebit() {
let value = 0;
if (this.doc.accounts) {
@ -158,27 +159,32 @@ export default {
value = this.doc.getSum('accounts', 'credit');
}
return frappe.format(value, 'Currency');
},
actions() {
return getActionsForDocument(this.doc);
}
},
async mounted() {
this.meta = frappe.getMeta(this.doctype);
this.doc = await frappe.getDoc(this.doctype, this.name);
try {
this.doc = await frappe.getDoc(this.doctype, this.name);
window.je = this.doc;
} catch (error) {
if (error instanceof frappe.errors.NotFoundError) {
this.$router.push(`/list/${this.doctype}`);
return;
}
this.handleError(error);
}
},
methods: {
async addNewItem() {
this.doc.append('items');
},
async onSaveClick() {
return this.doc.insertOrUpdate();
return this.doc.insertOrUpdate().catch(this.handleError);
},
async onSubmitClick() {
await this.doc.submit();
await this.doc.submit().catch(this.handleError);
},
async fetchPartyDoc() {
this.partyDoc = await frappe.getDoc(
'Party',
this.doc[this.partyField.fieldname]
);
handleError(e) {
handleErrorWithDialog(e, this.doc);
}
}
};

View File

@ -10,22 +10,7 @@
}}</span>
</div>
<div class="flex items-stretch">
<Dropdown
v-if="actions && actions.length"
:items="actions"
right
class="text-base"
>
<template v-slot="{ toggleDropdown }">
<Button
class="text-gray-900"
:icon="true"
@click="toggleDropdown()"
>
<feather-icon name="more-horizontal" class="w-4 h-4" />
</Button>
</template>
</Dropdown>
<DropdownWithActions :actions="actions" />
<Button
:icon="true"
@click="insertDoc"
@ -94,8 +79,8 @@ import { _ } from 'frappejs';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import TwoColumnForm from '@/components/TwoColumnForm';
import Dropdown from '@/components/Dropdown';
import { deleteDocWithPrompt, openQuickEdit } from '@/utils';
import DropdownWithActions from '@/components/DropdownWithActions';
import { openQuickEdit, getActionsForDocument } from '@/utils';
export default {
name: 'QuickEditForm',
@ -104,7 +89,7 @@ export default {
Button,
FormControl,
TwoColumnForm,
Dropdown
DropdownWithActions
},
provide() {
let vm = this;
@ -137,29 +122,7 @@ export default {
.filter(df => !(this.hideFields || []).includes(df.fieldname));
},
actions() {
if (!this.doc) return null;
let deleteAction = {
component: {
template: `<span class="text-red-700">{{ _('Delete') }}</span>`
},
condition: doc => !doc.isNew() && !doc.submitted,
action: () => {
this.deleteDoc().then(() => {
this.routeToList();
});
}
};
let actions = [...(this.meta.actions || []), deleteAction]
.filter(d => (d.condition ? d.condition(this.doc) : true))
.map(d => {
return {
label: d.label,
component: d.component,
action: d.action.bind(this, this.doc, this.$router)
};
});
return actions;
return getActionsForDocument(this.doc);
}
},
methods: {
@ -219,12 +182,6 @@ export default {
submitDoc() {
this.$refs.form.submit();
},
deleteDoc() {
return deleteDocWithPrompt(this.doc);
},
routeToList() {
this.$router.push(`/list/${this.doctype}`);
},
routeToPrevious() {
this.$router.back();
},

View File

@ -1,6 +1,5 @@
import frappe from 'frappejs';
import fs from 'fs';
import path from 'path';
import { _ } from 'frappejs/utils';
import { remote, shell } from 'electron';
import router from '@/router';
@ -63,7 +62,7 @@ export function showMessageDialog({ message, description, buttons = [] }) {
}
export function deleteDocWithPrompt(doc) {
return new Promise((resolve, reject) => {
return new Promise(resolve => {
showMessageDialog({
message: _('Are you sure you want to delete {0} "{1}"?', [
doc.doctype,
@ -214,3 +213,32 @@ export function makePDF(html, destination) {
});
});
}
export function getActionsForDocument(doc) {
if (!doc) return [];
let deleteAction = {
component: {
template: `<span class="text-red-700">{{ _('Delete') }}</span>`
},
condition: doc => !doc.isNew() && !doc.submitted,
action: () =>
deleteDocWithPrompt(doc).then(res => {
if (res) {
router.push(`/list/${doc.doctype}`);
}
})
};
let actions = [...(doc.meta.actions || []), deleteAction]
.filter(d => (d.condition ? d.condition(doc) : true))
.map(d => {
return {
label: d.label,
component: d.component,
action: d.action.bind(this, doc, router)
};
});
return actions;
}