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:
parent
bd5ee080c2
commit
6e2c5cdf96
@ -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
|
||||
]
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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() {
|
||||
|
28
src/components/DropdownWithActions.vue
Normal file
28
src/components/DropdownWithActions.vue
Normal 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>
|
@ -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);
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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();
|
||||
},
|
||||
|
32
src/utils.js
32
src/utils.js
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user