2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 15:20:56 +00:00

incr: add computed, update row amount display vals

This commit is contained in:
18alantom 2022-07-12 15:53:41 +05:30
parent 3dfdf22f9f
commit eb66317dce
14 changed files with 347 additions and 42 deletions

View File

@ -610,7 +610,9 @@ export default class DatabaseCore extends DatabaseBase {
async #createTable(schemaName: string, tableName?: string) {
tableName ??= schemaName;
const fields = this.schemaMap[schemaName]!.fields;
const fields = this.schemaMap[schemaName]!.fields.filter(
(f) => !f.computed
);
return await this.#runCreateTableQuery(tableName, fields);
}

View File

@ -384,18 +384,27 @@ export class Doc extends Observable<DocValue | Doc[]> {
await validator(value);
}
getValidDict(filterMeta: boolean = false): DocValueMap {
getValidDict(
filterMeta: boolean = false,
filterComputed: boolean = false
): DocValueMap {
let fields = this.schema.fields;
if (filterMeta) {
fields = this.schema.fields.filter((f) => !f.meta);
}
if (filterComputed) {
fields = this.schema.fields.filter((f) => !f.computed);
}
const data: DocValueMap = {};
for (const field of fields) {
let value = this[field.fieldname] as DocValue | DocValueMap[];
if (Array.isArray(value)) {
value = value.map((doc) => (doc as Doc).getValidDict(filterMeta));
value = value.map((doc) =>
(doc as Doc).getValidDict(filterMeta, filterComputed)
);
}
if (isPesa(value)) {
@ -577,7 +586,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
continue;
}
const newVal = await this._getValueFromFormula(field, doc);
const newVal = await this._getValueFromFormula(field, doc, fieldname);
const previousVal = doc.get(field.fieldname);
const isSame = areDocValuesEqual(newVal as DocValue, previousVal);
if (newVal === undefined || isSame) {
@ -591,7 +600,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
return changed;
}
async _getValueFromFormula(field: Field, doc: Doc) {
async _getValueFromFormula(field: Field, doc: Doc, fieldname?: string) {
const { formula } = doc.formulas[field.fieldname] ?? {};
if (formula === undefined) {
return;
@ -599,7 +608,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
let value: FormulaReturn;
try {
value = await formula();
value = await formula(fieldname);
} catch {
return;
}
@ -623,7 +632,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
this._setBaseMetaValues();
await this._preSync();
const validDict = this.getValidDict();
const validDict = this.getValidDict(false, true);
const data = await this.fyo.db.insert(this.schemaName, validDict);
this._syncValues(data);
@ -636,7 +645,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
this._updateModifiedMetaValues();
await this._preSync();
const data = this.getValidDict();
const data = this.getValidDict(false, true);
await this.fyo.db.update(this.schemaName, data);
this._syncValues(data);
@ -750,7 +759,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
duplicate(): Doc {
const updateMap = this.getValidDict(true);
const updateMap = this.getValidDict(true, true);
for (const field in updateMap) {
const value = updateMap[field];
if (!Array.isArray(value)) {

View File

@ -18,7 +18,7 @@ import { Doc } from './doc';
* - `Required`: Regular function used to decide if a value is mandatory (there are !notnul in the db).
*/
export type FormulaReturn = DocValue | DocValueMap[] | undefined | Doc[];
export type Formula = () => Promise<FormulaReturn> | FormulaReturn;
export type Formula = (fieldname?: string) => Promise<FormulaReturn> | FormulaReturn;
export type FormulaConfig = { dependsOn?: string[]; formula: Formula };
export type Default = () => DocValue;
export type Validation = (value: DocValue) => Promise<void> | void;

View File

@ -11,12 +11,33 @@ export abstract class InvoiceItem extends Doc {
amount?: Money;
baseAmount?: Money;
exchangeRate?: number;
itemDiscountPercent?: number;
itemDiscountAmount?: Money;
parentdoc?: Invoice;
rate?: Money;
quantity?: number;
tax?: string;
get isSales() {
return this.schemaName === 'SalesInvoiceItem';
}
get discountAfterTax() {
return !!this?.parentdoc?.discountAfterTax;
}
async getTotalTaxRate(): Promise<number> {
if (!this.tax) {
return 0;
}
const details =
((await this.fyo.getValue('Tax', this.tax, 'details')) as Doc[]) ?? [];
return details.reduce((acc, doc) => {
return (doc.rate as number) + acc;
}, 0);
}
formulas: FormulaMap = {
description: {
formula: async () =>
@ -27,6 +48,102 @@ export abstract class InvoiceItem extends Doc {
)) as string,
dependsOn: ['item'],
},
itemDiscountAmount: {
formula: async (fieldname) => {
if (fieldname === 'itemDiscountPercent') {
return this.amount!.percent(this.itemDiscountPercent ?? 0);
}
return this.fyo.pesa(0);
},
dependsOn: ['itemDiscountPercent'],
},
itemDiscountPercent: {
formula: async (fieldname) => {
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
if (!this.discountAfterTax) {
return itemDiscountAmount.div(this.amount ?? 0).mul(100).float;
}
const totalTaxRate = await this.getTotalTaxRate();
const rate = this.rate ?? this.fyo.pesa(0);
const quantity = this.quantity ?? 1;
const taxedTotal = getTaxedTotalBeforeDiscounting(
totalTaxRate,
rate,
quantity
);
return itemDiscountAmount.div(taxedTotal).mul(100).float;
},
dependsOn: ['itemDiscountAmount'],
},
itemDiscountedTotal: {
formula: async (fieldname) => {
const totalTaxRate = await this.getTotalTaxRate();
const rate = this.rate ?? this.fyo.pesa(0);
const quantity = this.quantity ?? 1;
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
const itemDiscountPercent = this.itemDiscountPercent ?? 0;
if (!this.discountAfterTax) {
return getDiscountedTotalBeforeTaxation(
rate,
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
);
}
return getDiscountedTotalAfterTaxation(
totalTaxRate,
rate,
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
);
},
dependsOn: [
'item',
'rate',
'tax',
'quantity',
'itemDiscountAmount',
'itemDiscountPercent',
],
},
itemTaxedTotal: {
formula: async (fieldname) => {
const totalTaxRate = await this.getTotalTaxRate();
const rate = this.rate ?? this.fyo.pesa(0);
const quantity = this.quantity ?? 1;
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
const itemDiscountPercent = this.itemDiscountPercent ?? 0;
if (!this.discountAfterTax) {
return getTaxedTotalAfterDiscounting(
totalTaxRate,
rate,
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
);
}
return getTaxedTotalBeforeDiscounting(totalTaxRate, rate, quantity);
},
dependsOn: [
'item',
'rate',
'tax',
'quantity',
'itemDiscountAmount',
'itemDiscountPercent',
],
},
rate: {
formula: async () => {
const rate = (await this.fyo.getValue(
@ -142,3 +259,88 @@ export abstract class InvoiceItem extends Doc {
},
};
}
function getDiscountedTotalBeforeTaxation(
rate: Money,
quantity: number,
itemDiscountAmount: Money,
itemDiscountPercent: number,
fieldname?: string
) {
/**
* If Discount is applied before taxation
* Use different formulas depending on how discount is set
* - if amount : Quantity * Rate - DiscountAmount
* - if percent: Quantity * Rate (1 - DiscountPercent / 100)
*/
const amount = rate.mul(quantity);
if (fieldname === 'itemDiscountAmount') {
return amount.sub(itemDiscountAmount);
}
return amount.mul(1 - itemDiscountPercent / 100);
}
function getTaxedTotalAfterDiscounting(
totalTaxRate: number,
rate: Money,
quantity: number,
itemDiscountAmount: Money,
itemDiscountPercent: number,
fieldname?: string
) {
/**
* If Discount is applied before taxation
* Formula: Discounted Total * (1 + TotalTaxRate / 100)
*/
const discountedTotal = getDiscountedTotalBeforeTaxation(
rate,
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
);
return discountedTotal.mul(1 + totalTaxRate / 100);
}
function getDiscountedTotalAfterTaxation(
totalTaxRate: number,
rate: Money,
quantity: number,
itemDiscountAmount: Money,
itemDiscountPercent: number,
fieldname?: string
) {
/**
* If Discount is applied after taxation
* Use different formulas depending on how discount is set
* - if amount : Taxed Total - Discount Amount
* - if percent: Taxed Total * (1 - Discount Percent / 100)
*/
const taxedTotal = getTaxedTotalBeforeDiscounting(
totalTaxRate,
rate,
quantity
);
if (fieldname === 'itemDiscountAmount') {
return taxedTotal.sub(itemDiscountAmount);
}
return taxedTotal.mul(1 - itemDiscountPercent / 100);
}
function getTaxedTotalBeforeDiscounting(
totalTaxRate: number,
rate: Money,
quantity: number
) {
/**
* If Discount is applied after taxation
* Formula: Rate * Quantity * (1 + Total Tax Rate / 100)
*/
return rate.mul(quantity).mul(1 + totalTaxRate / 100);
}

View File

@ -66,16 +66,30 @@
},
{
"fieldname": "itemDiscountAmount",
"label": "Item Discount Amount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "itemDiscountPercent",
"label": "Item Discount Percent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Total",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Total",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
@ -88,12 +102,16 @@
"keywordFields": ["item", "tax"],
"quickEditFields": [
"item",
"account",
"description",
"hsnCode",
"tax",
"quantity",
"rate",
"amount",
"itemDiscountAmount",
"itemDiscountPercent"
"itemDiscountPercent",
"itemDiscountedTotal",
"itemTaxedTotal"
]
}

View File

@ -14,8 +14,7 @@
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text",
"hidden": true
"fieldtype": "Text"
},
{
"fieldname": "quantity",
@ -39,12 +38,9 @@
{
"fieldname": "account",
"label": "Account",
"hidden": true,
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true,
"readOnly": true
"required": true
},
{
"fieldname": "tax",
@ -67,34 +63,51 @@
},
{
"fieldname": "itemDiscountAmount",
"label": "Item Discount Amount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "itemDiscountPercent",
"label": "Item Discount Percent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Total",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Total",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code",
"hidden": true
"placeholder": "HSN/SAC Code"
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
"keywordFields": ["item", "tax"],
"quickEditFields": [
"item",
"account",
"description",
"hsnCode",
"tax",
"quantity",
"rate",
"amount",
"itemDiscountAmount",
"itemDiscountPercent"
"itemDiscountPercent",
"itemDiscountedTotal",
"itemTaxedTotal"
]
}

View File

@ -33,6 +33,7 @@ export interface BaseField {
groupBy?: string; // UI Facing used in dropdowns fields
meta?: boolean; // Field is a meta field, i.e. only for the db, not UI
inline?: boolean; // UI Facing config, whether to display doc inline.
computed?: boolean; // Computed values are not stored in the database.
}
export type SelectOption = { value: string; label: string };

View File

@ -46,6 +46,7 @@
:background="false"
@click="openRowQuickEdit"
v-if="canEditRow"
:disabled="isEditing"
>
<feather-icon name="edit" class="w-4 h-4 text-gray-600" />
</Button>
@ -97,6 +98,9 @@ export default {
doc: this.row,
};
},
inject: {
isEditing: { default: false },
},
methods: {
onChange(df, value) {
if (value == null) {

View File

@ -16,6 +16,7 @@
</h1>
<div class="flex items-stretch window-no-drag gap-2 ml-auto">
<slot />
<div class="border-r" v-if="showBorder" />
<BackLink v-if="backLink" class="window-no-drag" />
<SearchBar v-if="!hideSearch" />
</div>
@ -33,5 +34,10 @@ export default {
border: { type: Boolean, default: true },
},
components: { SearchBar, BackLink },
computed: {
showBorder() {
return !!this.$slots.default;
},
},
};
</script>

View File

@ -16,7 +16,7 @@
<keep-alive>
<component
:is="Component"
class="w-80 flex-1"
class="w-quick-edit flex-1"
:key="$route.query.schemaName + $route.query.name"
/>
</keep-alive>

View File

@ -10,6 +10,13 @@
>
{{ t`Print` }}
</Button>
<Button
:icon="true"
v-if="!doc?.isSubmitted && !quickEditDoc"
@click="toggleInvoiceSettings"
>
<feather-icon name="settings" class="w-4 h-4" />
</Button>
<DropdownWithActions :actions="actions()" />
<Button
v-if="doc?.notInserted || doc?.dirty"
@ -60,7 +67,6 @@
input-class="text-lg font-semibold bg-transparent"
:df="getField('party')"
:value="doc.party"
:placeholder="getField('party').label"
@change="(value) => doc.set('party', value)"
@new-doc="(party) => doc.set('party', party.name)"
:read-only="doc?.submitted"
@ -69,7 +75,6 @@
input-class="bg-gray-100 px-3 py-2 text-base text-right"
:df="getField('date')"
:value="doc.date"
:placeholder="'Date'"
@change="(value) => doc.set('date', value)"
:read-only="doc?.submitted"
/>
@ -86,10 +91,18 @@
input-class="px-3 py-2 text-base bg-transparent"
:df="getField('account')"
:value="doc.account"
:placeholder="'Account'"
@change="(value) => doc.set('account', value)"
:read-only="doc?.submitted"
/>
<FormControl
v-if="doc.discountPercent > 0"
class="text-base bg-gray-100 rounded"
input-class="px-3 py-2 text-base bg-transparent text-right"
:df="getField('discountPercent')"
:value="doc.discountPercent"
@change="(value) => doc.set('discountPercent', value)"
:read-only="doc?.submitted"
/>
</div>
<hr />
@ -101,7 +114,7 @@
:showHeader="true"
:max-rows-before-overflow="4"
@change="(value) => doc.set('items', value)"
@editrow="(r) => (row = r)"
@editrow="toggleQuickEditDoc"
:read-only="doc?.submitted"
/>
</div>
@ -177,14 +190,19 @@
</div>
</template>
<template #quickedit v-if="row">
<template #quickedit v-if="quickEditDoc">
<QuickEditForm
class="w-80"
:name="row.name"
:source-doc="row"
:schema-name="row.schemaName"
class="w-quick-edit"
:name="quickEditDoc.name"
:show-name="false"
:show-save="false"
:source-doc="quickEditDoc"
:source-fields="quickEditFields"
:schema-name="quickEditDoc.schemaName"
:white="true"
:route-back="false"
@close="row = null"
:load-on-close="false"
@close="toggleQuickEditDoc(null)"
/>
</template>
</FormContainer>
@ -227,13 +245,15 @@ export default {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc),
isEditing: computed(() => !!this.quickEditDoc),
};
},
data() {
return {
chstatus: false,
doc: null,
row: null,
quickEditDoc: null,
quickEditFields: [],
color: null,
printSettings: null,
companyName: null,
@ -281,6 +301,23 @@ export default {
},
methods: {
routeTo,
toggleInvoiceSettings() {
if (this.quickEditDoc || !this.schemaName) {
return;
}
const fields = [
'discountAfterTax',
'discountAmount',
'discountPercent',
].map((fn) => fyo.getField(this.schemaName, fn));
this.toggleQuickEditDoc(this.doc, fields);
},
toggleQuickEditDoc(doc, fields = []) {
this.quickEditDoc = doc;
this.quickEditFields = fields;
},
actions() {
return getActionsForDocument(this.doc);
},

View File

@ -37,7 +37,7 @@
</div>
<!-- Printview Customizer -->
<div class="border-l w-80" v-if="showCustomiser">
<div class="border-l w-quick-edit" v-if="showCustomiser">
<div
class="px-4 flex items-center justify-between h-row-largest border-b"
>

View File

@ -6,7 +6,7 @@
<!-- Quick edit Tool bar -->
<div
class="flex items-center justify-between px-4 h-row-largest"
:class="{ 'border-b': !isChild }"
:class="{ 'border-b': showName }"
>
<!-- Close Button and Status Text -->
<div class="flex items-center">
@ -19,7 +19,7 @@
</div>
<!-- Actions, Badge and Status Change Buttons -->
<div class="flex items-stretch gap-2" v-if="!isChild">
<div class="flex items-stretch gap-2" v-if="showSave">
<StatusBadge :status="status" />
<DropdownWithActions :actions="actions" />
<Button
@ -52,7 +52,7 @@
<div
class="px-4 flex-center flex flex-col items-center gap-1.5"
style="height: calc(var(--h-row-mid) * 2 + 1px)"
v-if="doc && !isChild"
v-if="doc && showName"
>
<FormControl
v-if="imageField"
@ -110,8 +110,12 @@ export default {
schemaName: String,
defaults: String,
white: { type: Boolean, default: false },
sourceDoc: { type: Doc, default: null },
routeBack: { type: Boolean, default: true },
showName: { type: Boolean, default: true },
showSave: { type: Boolean, default: true },
sourceDoc: { type: Doc, default: null },
loadOnClose: { type: Boolean, default: true },
sourceFields: { type: Array, default: () => [] },
hideFields: { type: Array, default: () => [] },
showFields: { type: Array, default: () => [] },
},
@ -162,6 +166,10 @@ export default {
return getDocStatus(this.doc);
},
fields() {
if (this.sourceFields?.length) {
return this.sourceFields;
}
if (!this.schema) {
return [];
}
@ -294,7 +302,7 @@ export default {
}
},
routeToPrevious() {
if (this.doc.dirty && !this.doc.notInserted) {
if (this.loadOnClose && this.doc.dirty && !this.doc.notInserted) {
this.doc.load();
}

View File

@ -58,6 +58,7 @@ html {
--w-sidebar: 12rem;
--w-desk: calc(100vw - var(--w-sidebar));
--w-desk-fixed: calc(var(--w-app) - var(--w-sidebar));
--w-quick-edit: 22rem;
--w-scrollbar: 0.5rem;
/* Row Heights */
@ -73,6 +74,10 @@ html {
width: 600px;
}
.w-quick-edit {
width: var(--w-quick-edit)
}
.h-form {
height: 800px;
}