2
0
mirror of https://github.com/frappe/books.git synced 2024-09-19 19:19:02 +00:00

incr: user common form for journal entries

This commit is contained in:
18alantom 2023-02-21 11:51:32 +05:30
parent e0141b70d8
commit 332307bdd1
9 changed files with 126 additions and 361 deletions

View File

@ -99,7 +99,7 @@ export class DocHandler {
}
const doc = new Model!(schema, data, this.fyo, isRawValueMap);
doc.name ??= this.#getTemporaryName(schema);
doc.name ??= this.getTemporaryName(schema);
if (cacheDoc) {
this.#addToCache(doc);
}
@ -107,7 +107,7 @@ export class DocHandler {
return doc;
}
#getTemporaryName(schema: Schema): string {
getTemporaryName(schema: Schema): string {
if (schema.naming === 'random') {
return getRandomString();
}

View File

@ -454,7 +454,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
convertToDocValue: boolean = false
): Doc {
if (!this.name && this.schema.naming !== 'manual') {
this.name = getRandomString();
this.name = this.fyo.doc.getTemporaryName(this.schema);
}
docValueMap.name ??= getRandomString();

View File

@ -4,9 +4,9 @@ import {
Action,
DefaultMap,
FiltersMap,
HiddenMap,
ListViewSettings,
} from 'fyo/model/types';
import { DateTime } from 'luxon';
import {
getDocStatus,
getLedgerLinkAction,
@ -39,6 +39,17 @@ export class JournalEntry extends Transactional {
return posting;
}
hidden: HiddenMap = {
referenceNumber: () =>
!(this.referenceNumber || !(this.isSubmitted || this.isCancelled)),
referenceDate: () =>
!(this.referenceDate || !(this.isSubmitted || this.isCancelled)),
userRemark: () =>
!(this.userRemark || !(this.isSubmitted || this.isCancelled)),
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
};
static defaults: DefaultMap = {
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
date: () => new Date(),

View File

@ -4,6 +4,24 @@
"naming": "numberSeries",
"isSubmittable": true,
"fields": [
{
"label": "Entry No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true,
"section": "Default"
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "JV-",
"section": "Default"
},
{
"fieldname": "entryType",
"label": "Entry Type",
@ -55,58 +73,49 @@
"label": "Depreciation Entry"
}
],
"required": true
},
{
"label": "Entry No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
"section": "Default"
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"required": true
"required": true,
"section": "Default"
},
{
"fieldname": "accounts",
"label": "Account Entries",
"fieldtype": "Table",
"target": "JournalEntryAccount",
"required": true
"required": true,
"section": "Accounts"
},
{
"fieldname": "referenceNumber",
"label": "Reference Number",
"fieldtype": "Data"
"fieldtype": "Data",
"section": "References"
},
{
"fieldname": "referenceDate",
"label": "Reference Date",
"fieldtype": "Date"
"fieldtype": "Date",
"section": "References"
},
{
"fieldname": "userRemark",
"label": "User Remark",
"fieldtype": "Text",
"placeholder": "Add a remark"
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "JV-"
"placeholder": "Add a remark",
"section": "References"
},
{
"fieldname": "attachment",
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment"
"fieldtype": "Attachment",
"section": "References"
}
],
"keywordFields": ["name", "entryType"]

View File

@ -1,41 +1,50 @@
<template>
<div :class="containerClasses" class="flex gap-2 items-center">
<label
for="attachment"
class="block whitespace-nowrap overflow-auto no-scrollbar"
:class="[inputClasses, !value ? 'text-gray-600' : 'cursor-default']"
>{{ label }}</label
>
<input
ref="fileInput"
id="attachment"
type="file"
accept="image/*,.pdf"
class="hidden"
:disabled="!!value"
@input="selectFile"
/>
<!-- Buttons -->
<div class="me-2 flex gap-2">
<!-- Upload Button -->
<button v-if="!value" class="bg-gray-300 p-0.5 rounded" @click="upload">
<FeatherIcon name="upload" class="h-4 w-4 text-gray-600" />
</button>
<!-- Download Button -->
<button v-if="value" class="bg-gray-300 p-0.5 rounded" @click="download">
<FeatherIcon name="download" class="h-4 w-4 text-gray-600" />
</button>
<!-- Clear Button -->
<button
v-if="value && !isReadOnly"
class="bg-gray-300 p-0.5 rounded"
@click="clear"
<div>
<div :class="labelClasses" v-if="showLabel && df">
{{ df.label }}
</div>
<div :class="containerClasses" class="flex gap-2 items-center">
<label
for="attachment"
class="block whitespace-nowrap overflow-auto no-scrollbar"
:class="[inputClasses, !value ? 'text-gray-600' : 'cursor-default']"
>{{ label }}</label
>
<FeatherIcon name="x" class="h-4 w-4 text-gray-600" />
</button>
<input
ref="fileInput"
id="attachment"
type="file"
accept="image/*,.pdf"
class="hidden"
:disabled="!!value"
@input="selectFile"
/>
<!-- Buttons -->
<div class="me-2 flex gap-2">
<!-- Upload Button -->
<button v-if="!value" class="bg-gray-300 p-0.5 rounded" @click="upload">
<FeatherIcon name="upload" class="h-4 w-4 text-gray-600" />
</button>
<!-- Download Button -->
<button
v-if="value"
class="bg-gray-300 p-0.5 rounded"
@click="download"
>
<FeatherIcon name="download" class="h-4 w-4 text-gray-600" />
</button>
<!-- Clear Button -->
<button
v-if="value && !isReadOnly"
class="bg-gray-300 p-0.5 rounded"
@click="clear"
>
<FeatherIcon name="x" class="h-4 w-4 text-gray-600" />
</button>
</div>
</div>
</div>
</template>

View File

@ -155,7 +155,7 @@ export default defineComponent({
}
await this.setDoc();
this.groupedFields = this.getGroupedFields();
this.updateGroupedFields();
},
computed: {
hasDoc(): boolean {
@ -227,16 +227,20 @@ export default defineComponent({
},
},
methods: {
getGroupedFields(): UIGroupedFields {
updateGroupedFields(): void {
if (!this.hasDoc) {
return new Map();
return;
}
return getFieldsGroupedByTabAndSection(this.schema, this.doc);
this.groupedFields = getFieldsGroupedByTabAndSection(
this.schema,
this.doc
);
},
async sync() {
try {
await this.doc.sync();
this.updateGroupedFields();
} catch (err) {
if (!(err instanceof Error)) {
return;
@ -248,6 +252,7 @@ export default defineComponent({
async submit() {
try {
await this.doc.submit();
this.updateGroupedFields();
} catch (err) {
if (!(err instanceof Error)) {
return;
@ -297,7 +302,7 @@ export default defineComponent({
this.errors[fieldname] = getErrorMessage(err, this.doc);
}
this.groupedFields = this.getGroupedFields();
this.updateGroupedFields();
},
},
components: {

View File

@ -18,10 +18,11 @@
<div
v-for="field of fields"
:key="field.fieldname"
class="mb-auto"
:class="field.fieldtype === 'Table' ? 'col-span-2 text-base' : ''"
class="mb-auto"
>
<FormControl
:ref="field.fieldname === 'name' ? 'nameField' : 'fields'"
:show-label="true"
:border="true"
:df="field"
@ -29,7 +30,7 @@
@editrow="(doc: Doc) => $emit('editrow', doc)"
@change="(value: DocValue) => $emit('value-change', field, value)"
/>
<div v-if="errors?.[field.fieldname]" class="text-sm text-red-600">
<div v-if="errors?.[field.fieldname]" class="text-sm text-red-600 mt-1">
{{ errors[field.fieldname] }}
</div>
</div>
@ -57,6 +58,24 @@ export default defineComponent({
collapsed: boolean;
};
},
mounted() {
this.focusOnNameField();
},
methods: {
focusOnNameField() {
const naming = this.fyo.schemaMap[this.doc.schemaName]?.naming;
if (naming !== 'manual') {
return;
}
const nameField = (this.$refs.nameField as { focus: Function }[])?.[0];
if (!nameField) {
return;
}
nameField.focus();
},
},
components: { FormControl },
});
</script>

View File

@ -1,269 +0,0 @@
<template>
<FormContainer>
<!-- Page Header (Title, Buttons, etc) -->
<template #header v-if="doc">
<StatusBadge :status="status" />
<DropdownWithActions
v-for="group of groupedActions"
:key="group.label"
:type="group.type"
:actions="group.actions"
>
<p v-if="group.group">
{{ group.group }}
</p>
<feather-icon v-else name="more-horizontal" class="w-4 h-4" />
</DropdownWithActions>
<Button
v-if="doc?.canSave"
type="primary"
class="text-white text-xs"
@click="sync"
>
{{ t`Save` }}
</Button>
<Button
v-else-if="doc.canSubmit"
type="primary"
class="text-white text-xs"
@click="submit"
>
{{ t`Submit` }}
</Button>
</template>
<!-- Journal Entry Form -->
<template #body v-if="doc">
<FormHeader
:form-title="doc.notInserted ? t`New Entry` : doc.name"
:form-sub-title="t`Journal Entry`"
/>
<hr />
<div>
<div class="m-4 grid grid-cols-3 gap-y-4 gap-x-4">
<!-- First Column of Fields -->
<FormControl
:border="true"
:df="getField('numberSeries')"
:value="doc.numberSeries"
@change="(value) => doc.set('numberSeries', value)"
:read-only="!doc.notInserted || doc.submitted"
/>
<FormControl
:border="true"
:df="getField('date')"
:value="doc.date"
:placeholder="'Date'"
@change="(value) => doc.set('date', value)"
:read-only="doc.submitted"
/>
<FormControl
:border="true"
:df="getField('entryType')"
:value="doc.entryType"
placeholder="Entry Type"
@change="(value) => doc.set('entryType', value)"
:read-only="doc.submitted"
/>
<FormControl
:border="true"
:df="getField('referenceNumber')"
:value="doc.referenceNumber"
:placeholder="'Reference Number'"
@change="(value) => doc.set('referenceNumber', value)"
:read-only="doc.submitted"
/>
<FormControl
:border="true"
:df="getField('referenceDate')"
:value="doc.referenceDate"
:placeholder="'Reference Date'"
@change="(value) => doc.set('referenceDate', value)"
:read-only="doc.submitted"
/>
<FormControl
v-if="doc.attachment || !(doc.isSubmitted || doc.isCancelled)"
:border="true"
:df="getField('attachment')"
:value="doc.attachment"
@change="(value) => doc.set('attachment', value)"
:read-only="doc?.submitted"
/>
</div>
<hr />
<!-- Account Entries Table -->
<Table
class="text-base"
:df="getField('accounts')"
:value="doc.accounts"
:showHeader="true"
:max-rows-before-overflow="6"
:read-only="doc.submitted"
/>
</div>
<!-- Footer -->
<div v-if="doc.accounts?.length ?? 0" class="mt-auto">
<hr />
<div class="flex justify-between text-base m-4 gap-12">
<!-- User Remark -->
<FormControl
:border="true"
v-if="!doc.submitted || doc.userRemark"
class="w-1/2 self-end"
:df="getField('userRemark')"
:value="doc.userRemark"
@change="(value) => doc.set('userRemark', value)"
:read-only="doc.submitted"
/>
<!-- Debit and Credit -->
<div class="w-1/2 gap-2 flex flex-col self-end font-semibold ms-auto">
<div class="flex justify-between text-green-600">
<div>{{ t`Total Debit` }}</div>
<div>{{ totalDebit }}</div>
</div>
<hr />
<div class="flex justify-between text-red-600">
<div>{{ t`Total Credit` }}</div>
<div>{{ totalCredit }}</div>
</div>
</div>
</div>
</div>
</template>
</FormContainer>
</template>
<script>
import { computed } from '@vue/reactivity';
import { getDocStatus } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import {
getGroupedActionsForDoc,
routeTo,
showMessageDialog,
} from 'src/utils/ui';
import { handleErrorWithDialog } from '../errorHandling';
export default {
name: 'JournalEntryForm',
props: ['name'],
components: {
Button,
DropdownWithActions,
StatusBadge,
FormControl,
Table,
FormContainer,
FormHeader,
},
provide() {
return {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc),
};
},
data() {
return {
schemaName: ModelNameEnum.JournalEntry,
doc: null,
};
},
activated() {
docsPathRef.value = docsPathMap.JournalEntry;
focusedDocsRef.add(this.doc);
},
deactivated() {
docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
},
async mounted() {
try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) {
if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`);
return;
}
await this.handleError(error);
}
if (fyo.store.isDevelopment) {
window.je = this;
}
},
computed: {
status() {
return getDocStatus(this.doc);
},
totalDebit() {
let value = 0;
if (this.doc.accounts) {
value = this.doc.getSum('accounts', 'debit');
}
return fyo.format(value, 'Currency');
},
totalCredit() {
let value = 0;
if (this.doc.accounts) {
value = this.doc.getSum('accounts', 'credit');
}
return fyo.format(value, 'Currency');
},
groupedActions() {
return getGroupedActionsForDoc(this.doc);
},
},
methods: {
getField(fieldname) {
return fyo.getField(ModelNameEnum.JournalEntry, fieldname);
},
async sync() {
try {
await this.doc.sync();
} catch (err) {
this.handleError(err);
}
},
async submit() {
const ref = this;
await showMessageDialog({
message: this.t`Submit Journal Entry?`,
buttons: [
{
label: this.t`Yes`,
async action() {
try {
await ref.doc.submit();
} catch (err) {
await ref.handleError(err);
}
},
},
{
label: this.t`No`,
action() {},
},
],
});
},
async handleError(e) {
await handleErrorWithDialog(e, this.doc);
},
},
};
</script>

View File

@ -1,22 +1,21 @@
import { ModelNameEnum } from 'models/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import CommonForm from 'src/pages/CommonForm/CommonForm.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import ImportWizard from 'src/pages/ImportWizard.vue';
import GeneralForm from 'src/pages/GeneralForm.vue';
import GetStarted from 'src/pages/GetStarted.vue';
import ImportWizard from 'src/pages/ImportWizard.vue';
import InvoiceForm from 'src/pages/InvoiceForm.vue';
import JournalEntryForm from 'src/pages/JournalEntryForm.vue';
import ListView from 'src/pages/ListView/ListView.vue';
import PrintView from 'src/pages/PrintView/PrintView.vue';
import QuickEditForm from 'src/pages/QuickEditForm.vue';
import CommonForm from 'src/pages/CommonForm/CommonForm.vue';
import Report from 'src/pages/Report.vue';
import Settings from 'src/pages/Settings/Settings.vue';
import {
createRouter,
createWebHistory,
RouteLocationRaw,
RouteRecordRaw,
RouteRecordRaw
} from 'vue-router';
function getGeneralFormItems(): RouteRecordRaw[] {
@ -46,6 +45,7 @@ function getGeneralFormItems(): RouteRecordRaw[] {
function getCommonFormItems(): RouteRecordRaw[] {
return [
ModelNameEnum.JournalEntry,
ModelNameEnum.Payment,
ModelNameEnum.StockMovement,
ModelNameEnum.Item,
@ -82,25 +82,6 @@ const routes: RouteRecordRaw[] = [
},
...getGeneralFormItems(),
...getCommonFormItems(),
{
path: '/edit/JournalEntry/:name',
name: 'JournalEntryForm',
components: {
default: JournalEntryForm,
edit: QuickEditForm,
},
props: {
default: (route) => {
// for sidebar item active state
route.params.schemaName = 'JournalEntry';
return {
schemaName: 'JournalEntry',
name: route.params.name,
};
},
edit: (route) => route.query,
},
},
{
path: '/edit/:schemaName/:name',
name: 'InvoiceForm',