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

incr: add a few calculations

- add enableDiscounting
- add toggle between amount and percent
- minor ui fixes
This commit is contained in:
18alantom 2022-07-13 23:18:20 +05:30
parent be44333e64
commit 7f928ca712
16 changed files with 182 additions and 21 deletions

View File

@ -453,7 +453,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
if (data && data.name) {
this._syncValues(data);
await this._syncValues(data);
await this.loadLinks();
} else {
throw new NotFoundError(`Not Found: ${this.schemaName} ${this.name}`);
@ -502,15 +502,39 @@ export class Doc extends Observable<DocValue | Doc[]> {
return link;
}
_syncValues(data: DocValueMap) {
async _syncValues(data: DocValueMap) {
this._clearValues();
this._setValuesWithoutChecks(data);
await this._setComputedValuesFromFormulas();
this._dirty = false;
this.trigger('change', {
doc: this,
});
}
async _setComputedValuesFromFormulas() {
for (const field of this.schema.fields) {
await this._setComputedValuesForChildren(field);
if (!field.computed) {
continue;
}
const value = await this._getValueFromFormula(field, this);
this[field.fieldname] = value ?? null;
}
}
async _setComputedValuesForChildren(field: Field) {
if (field.fieldtype !== 'Table') {
return;
}
const childDocs: Doc[] = (this[field.fieldname] as Doc[]) ?? [];
for (const doc of childDocs) {
await doc._setComputedValuesFromFormulas();
}
}
_clearValues() {
for (const { fieldname } of this.schema.fields) {
this[fieldname] = null;
@ -635,7 +659,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
const validDict = this.getValidDict(false, true);
const data = await this.fyo.db.insert(this.schemaName, validDict);
this._syncValues(data);
await this._syncValues(data);
this.fyo.telemetry.log(Verb.Created, this.schemaName);
return this;
@ -648,7 +672,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
const data = this.getValidDict(false, true);
await this.fyo.db.update(this.schemaName, data);
this._syncValues(data);
await this._syncValues(data);
return this;
}

View File

@ -50,6 +50,7 @@ export function getMissingMandatoryMessage(doc: Doc) {
const value = doc.get(f.fieldname);
const isNullOrUndef = getIsNullOrUndef(value);
console.log(f.fieldname, value);
if (f.fieldtype === FieldTypeEnum.Table) {
return isNullOrUndef || (value as Doc[])?.length === 0;
}

View File

@ -1,5 +1,10 @@
import { Doc } from 'fyo/model/doc';
import { FiltersMap, ListsMap, ValidationMap } from 'fyo/model/types';
import {
FiltersMap,
ListsMap,
ReadOnlyMap,
ValidationMap,
} from 'fyo/model/types';
import { validateEmail } from 'fyo/model/validationFunction';
import { getCountryInfo } from 'utils/misc';
@ -22,4 +27,10 @@ export class AccountingSettings extends Doc {
static lists: ListsMap = {
country: () => Object.keys(getCountryInfo()),
};
readOnly: ReadOnlyMap = {
enableDiscounting: () => {
return !!this.enableDiscounting;
},
};
}

View File

@ -1,11 +1,12 @@
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
import { DefaultMap, FiltersMap, FormulaMap, HiddenMap } from 'fyo/model/types';
import { getExchangeRate } from 'models/helpers';
import { Transactional } from 'models/Transactional/Transactional';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { getIsNullOrUndef } from 'utils';
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
import { Party } from '../Party/Party';
import { Payment } from '../Payment/Payment';
import { Tax } from '../Tax/Tax';
@ -15,6 +16,7 @@ export abstract class Invoice extends Transactional {
_taxes: Record<string, Tax> = {};
taxes?: TaxSummary[];
items?: InvoiceItem[];
party?: string;
account?: string;
currency?: string;
@ -23,6 +25,10 @@ export abstract class Invoice extends Transactional {
baseGrandTotal?: Money;
outstandingAmount?: Money;
exchangeRate?: number;
setDiscountAmount?: boolean;
discountAmount?: Money;
discountPercent?: number;
discountAfterTax?: boolean;
submitted?: boolean;
cancelled?: boolean;
@ -31,6 +37,10 @@ export abstract class Invoice extends Transactional {
return this.schemaName === 'SalesInvoice';
}
get enableDiscounting() {
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
}
async afterSubmit() {
await super.afterSubmit();
@ -163,9 +173,25 @@ export abstract class Invoice extends Transactional {
}
async getGrandTotal() {
const itemDiscountAmount = this.getItemDiscountAmount();
const discountAmount = this.discountAmount ?? this.fyo.pesa(0);
const totalDiscount = itemDiscountAmount.add(discountAmount);
return ((this.taxes ?? []) as Doc[])
.map((doc) => doc.amount as Money)
.reduce((a, b) => a.add(b), this.netTotal!);
.reduce((a, b) => a.add(b), this.netTotal!)
.sub(totalDiscount);
}
getItemDiscountAmount() {
if (!this?.items?.length) {
return this.fyo.pesa(0);
}
return this.items.reduce(
(acc, i) => acc.add(i.itemDiscountAmount ?? this.fyo.pesa(0)),
this.fyo.pesa(0)
);
}
formulas: FormulaMap = {
@ -200,6 +226,24 @@ export abstract class Invoice extends Transactional {
formula: async () => this.netTotal!.mul(this.exchangeRate!),
},
taxes: { formula: async () => await this.getTaxSummary() },
discountAmount: {
formula: async (fieldname) => {
if (fieldname !== 'discountPercent') {
return this.discountAmount!;
}
return this.getDiscountAmountFromPercent();
},
},
discountPercent: {
formula: async (fieldname) => {
if (fieldname === 'discountAmount') {
return this.getDiscountPercentFromAmount();
}
return this.discountPercent!;
},
},
grandTotal: { formula: async () => await this.getGrandTotal() },
baseGrandTotal: {
formula: async () => (this.grandTotal as Money).mul(this.exchangeRate!),
@ -215,6 +259,35 @@ export abstract class Invoice extends Transactional {
},
};
getDiscountPercentFromAmount() {
const discountAmount = this.discountAmount ?? this.fyo.pesa(0);
const itemDiscountedAmounts = this.getItemDiscountedAmounts();
return discountAmount.mul(100).div(itemDiscountedAmounts).float;
}
getDiscountAmountFromPercent() {
const discountPercent = this.discountPercent ?? 0;
const itemDiscountedAmounts = this.getItemDiscountedAmounts();
return itemDiscountedAmounts.percent(discountPercent);
}
getItemDiscountedAmounts() {
let itemDiscountedAmounts = this.fyo.pesa(0);
for (const item of this.items ?? []) {
itemDiscountedAmounts = itemDiscountedAmounts.add(
item.itemDiscountedTotal ?? item.amount!
);
}
return itemDiscountedAmounts;
}
hidden: HiddenMap = {
setDiscountAmount: () => !this.enableDiscounting,
discountAmount: () => !(this.enableDiscounting && !!this.setDiscountAmount),
discountPercent: () => !(this.enableDiscounting && !this.setDiscountAmount),
discountAfterTax: () => !this.enableDiscounting,
};
static defaults: DefaultMap = {
date: () => new Date().toISOString().slice(0, 10),
};

View File

@ -16,14 +16,16 @@ export abstract class InvoiceItem extends Doc {
amount?: Money;
baseAmount?: Money;
exchangeRate?: number;
itemDiscountPercent?: number;
itemDiscountAmount?: Money;
parentdoc?: Invoice;
rate?: Money;
quantity?: number;
tax?: string;
itemTaxedTotal?: Money;
setItemDiscountAmount?: boolean;
itemDiscountAmount?: Money;
itemDiscountPercent?: number;
itemDiscountedTotal?: Money;
itemTaxedTotal?: Money;
get isSales() {
return this.schemaName === 'SalesInvoiceItem';
@ -33,6 +35,10 @@ export abstract class InvoiceItem extends Doc {
return !!this?.parentdoc?.discountAfterTax;
}
get enableDiscounting() {
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
}
async getTotalTaxRate(): Promise<number> {
if (!this.tax) {
return 0;
@ -313,6 +319,11 @@ export abstract class InvoiceItem extends Doc {
(this.itemDiscountAmount?.isZero() || this.itemDiscountPercent === 0)
);
},
setItemDiscountAmount: () => !this.enableDiscounting,
itemDiscountAmount: () =>
!(this.enableDiscounting && !!this.setItemDiscountAmount),
itemDiscountPercent: () =>
!(this.enableDiscounting && !this.setItemDiscountAmount),
};
static filters: FiltersMap = {

View File

@ -36,7 +36,7 @@
},
{
"fieldname": "fullname",
"label": "Name",
"label": "Full Name",
"fieldtype": "Data",
"required": true
},
@ -65,6 +65,12 @@
"fieldtype": "Date",
"required": true
},
{
"fieldname": "enableDiscounting",
"label": "Enable Discounting",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "setupComplete",
"label": "Setup Complete",

View File

@ -93,6 +93,12 @@
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "discountAmount",
"label": "Discount Amount",

View File

@ -64,6 +64,12 @@
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setItemDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "itemDiscountAmount",
"label": "Discount Amount",
@ -109,6 +115,7 @@
"quantity",
"rate",
"amount",
"setItemDiscountAmount",
"itemDiscountAmount",
"itemDiscountPercent",
"itemDiscountedTotal",

View File

@ -92,6 +92,12 @@
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "discountAmount",
"label": "Discount Amount",

View File

@ -61,6 +61,12 @@
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setItemDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "itemDiscountAmount",
"label": "Discount Amount",
@ -105,6 +111,7 @@
"quantity",
"rate",
"amount",
"setItemDiscountAmount",
"itemDiscountAmount",
"itemDiscountPercent",
"itemDiscountedTotal",

View File

@ -62,7 +62,7 @@ export default {
'px-3 py-2': this.size !== 'small',
'px-2 py-1': this.size === 'small',
},
'focus:outline-none focus:bg-gray-200 rounded w-full placeholder-gray-400',
'focus:outline-none focus:bg-gray-200 rounded w-full placeholder-gray-500',
this.isReadOnly ? 'text-gray-800' : 'text-gray-900',
];

View File

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

View File

@ -32,7 +32,7 @@
text-2xl
focus:outline-none
w-full
placeholder-gray-700
placeholder-gray-500
text-gray-900
rounded-md
p-3

View File

@ -95,7 +95,7 @@
:read-only="doc?.submitted"
/>
<FormControl
v-if="doc.discountPercent > 0"
v-if="doc.discountPercent > 0 && !doc.setDiscountAmount"
class="text-base bg-gray-100 rounded"
input-class="px-3 py-2 text-base bg-transparent text-right"
:df="getField('discountPercent')"
@ -103,6 +103,15 @@
@change="(value) => doc.set('discountPercent', value)"
:read-only="doc?.submitted"
/>
<FormControl
v-if="doc.discountAmount.float > 0 && doc.setDiscountAmount"
class="text-base bg-gray-100 rounded"
input-class="px-3 py-2 text-base bg-transparent text-right"
:df="getField('discountAmount')"
:value="doc.discountAmount"
@change="(value) => doc.set('discountAmount', value)"
:read-only="doc?.submitted"
/>
</div>
<hr />
@ -357,12 +366,7 @@ export default {
return this.doc?.discountAmount ?? fyo.pesa(0);
},
itemDiscountAmount() {
const itemDiscountAmount = (this.doc?.items ?? []).reduce(
(acc, i) => acc.add(i.itemDiscountAmount),
fyo.pesa(0)
);
return itemDiscountAmount;
return this.doc.getItemDiscountAmount();
},
},
activated() {
@ -402,6 +406,7 @@ export default {
const fields = [
'discountAfterTax',
'setDiscountAmount',
'discountAmount',
'discountPercent',
].map((fn) => fyo.getField(this.schemaName, fn));

View File

@ -95,7 +95,8 @@ export default {
if (
fieldnames.includes('displayPrecision') ||
fieldnames.includes('hideGetStarted') ||
fieldnames.includes('displayPrecision')
fieldnames.includes('displayPrecision') ||
fieldnames.includes('enableDiscounting')
) {
this.showReloadToast();
}

View File

@ -38,6 +38,7 @@ export default {
computed: {
fields() {
return [
'fullname',
'companyName',
'country',
'bankName',
@ -46,6 +47,7 @@ export default {
'roundOffAccount',
'fiscalYearStart',
'fiscalYearEnd',
'enableDiscounting',
'gstin',
].map((fieldname) => fyo.getField('AccountingSettings', fieldname));
},