mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
Merge pull request #418 from 18alantom/add-discounts
feat: add discounts
This commit is contained in:
commit
6154b3aa55
@ -2,7 +2,7 @@ import {
|
||||
CannotCommitError,
|
||||
getDbError,
|
||||
NotFoundError,
|
||||
ValueError,
|
||||
ValueError
|
||||
} from 'fyo/utils/errors';
|
||||
import { knex, Knex } from 'knex';
|
||||
import {
|
||||
@ -11,12 +11,12 @@ import {
|
||||
RawValue,
|
||||
Schema,
|
||||
SchemaMap,
|
||||
TargetField,
|
||||
TargetField
|
||||
} from '../../schemas/types';
|
||||
import {
|
||||
getIsNullOrUndef,
|
||||
getRandomString,
|
||||
getValueMapFromList,
|
||||
getValueMapFromList
|
||||
} from '../../utils';
|
||||
import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types';
|
||||
import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers';
|
||||
@ -24,7 +24,7 @@ import {
|
||||
ColumnDiff,
|
||||
FieldValueMap,
|
||||
GetQueryBuilderOptions,
|
||||
SingleValue,
|
||||
SingleValue
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
@ -201,7 +201,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
|
||||
if (fields === undefined) {
|
||||
fields = schema.fields.map((f) => f.fieldname);
|
||||
fields = schema.fields.filter((f) => !f.computed).map((f) => f.fieldname);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -356,7 +356,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
async #removeColumns(schemaName: string, targetColumns: string[]) {
|
||||
const fields = this.schemaMap[schemaName]?.fields
|
||||
.filter((f) => f.fieldtype !== FieldTypeEnum.Table)
|
||||
.filter((f) => f.fieldtype !== FieldTypeEnum.Table && !f.computed)
|
||||
.map((f) => f.fieldname);
|
||||
const tableRows = await this.getAll(schemaName, { fields });
|
||||
this.prestigeTheTable(schemaName, tableRows);
|
||||
@ -504,7 +504,9 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
async #getColumnDiff(schemaName: string): Promise<ColumnDiff> {
|
||||
const tableColumns = await this.#getTableColumns(schemaName);
|
||||
const validFields = this.schemaMap[schemaName]!.fields;
|
||||
const validFields = this.schemaMap[schemaName]!.fields.filter(
|
||||
(f) => !f.computed
|
||||
);
|
||||
const diff: ColumnDiff = { added: [], removed: [] };
|
||||
|
||||
for (const field of validFields) {
|
||||
@ -610,7 +612,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);
|
||||
}
|
||||
|
||||
@ -752,9 +756,9 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
fieldValueMap.name = getRandomString();
|
||||
}
|
||||
|
||||
// Non Table Fields
|
||||
// Column fields
|
||||
const fields = this.schemaMap[schemaName]!.fields.filter(
|
||||
(f) => f.fieldtype !== FieldTypeEnum.Table
|
||||
(f) => f.fieldtype !== FieldTypeEnum.Table && !f.computed
|
||||
);
|
||||
|
||||
const validMap: FieldValueMap = {};
|
||||
@ -769,8 +773,9 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
singleSchemaName: string,
|
||||
fieldValueMap: FieldValueMap
|
||||
) {
|
||||
const fields = this.schemaMap[singleSchemaName]!.fields;
|
||||
|
||||
const fields = this.schemaMap[singleSchemaName]!.fields.filter(
|
||||
(f) => !f.computed
|
||||
);
|
||||
for (const field of fields) {
|
||||
const value = fieldValueMap[field.fieldname] as RawValue | undefined;
|
||||
if (value === undefined) {
|
||||
@ -860,8 +865,8 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
const updateMap = { ...fieldValueMap };
|
||||
delete updateMap.name;
|
||||
const schema = this.schemaMap[schemaName] as Schema;
|
||||
for (const { fieldname, fieldtype } of schema.fields) {
|
||||
if (fieldtype !== FieldTypeEnum.Table) {
|
||||
for (const { fieldname, fieldtype, computed } of schema.fields) {
|
||||
if (fieldtype !== FieldTypeEnum.Table && !computed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
@ -444,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}`);
|
||||
@ -493,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;
|
||||
@ -557,6 +590,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
const formulaFields = Object.keys(this.formulas).map(
|
||||
(fn) => this.fieldMap[fn]
|
||||
);
|
||||
|
||||
changed ||= await this._applyFormulaForFields(
|
||||
formulaFields,
|
||||
doc,
|
||||
@ -577,7 +611,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 +625,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 +633,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
|
||||
let value: FormulaReturn;
|
||||
try {
|
||||
value = await formula();
|
||||
value = await formula(fieldname);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
@ -623,9 +657,9 @@ 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);
|
||||
await this._syncValues(data);
|
||||
|
||||
this.fyo.telemetry.log(Verb.Created, this.schemaName);
|
||||
return this;
|
||||
@ -636,9 +670,9 @@ 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);
|
||||
await this._syncValues(data);
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -750,7 +784,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)) {
|
||||
|
@ -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;
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, ListsMap, ValidationMap } from 'fyo/model/types';
|
||||
import {
|
||||
ChangeArg,
|
||||
FiltersMap,
|
||||
ListsMap,
|
||||
ReadOnlyMap,
|
||||
ValidationMap
|
||||
} from 'fyo/model/types';
|
||||
import { validateEmail } from 'fyo/model/validationFunction';
|
||||
import { createDiscountAccount } from 'src/setup/setupInstance';
|
||||
import { getCountryInfo } from 'utils/misc';
|
||||
|
||||
export class AccountingSettings extends Doc {
|
||||
enableDiscounting?: boolean;
|
||||
static filters: FiltersMap = {
|
||||
writeOffAccount: () => ({
|
||||
isGroup: false,
|
||||
@ -13,6 +21,10 @@ export class AccountingSettings extends Doc {
|
||||
isGroup: false,
|
||||
rootType: 'Expense',
|
||||
}),
|
||||
discountAccount: () => ({
|
||||
isGroup: false,
|
||||
rootType: 'Income',
|
||||
}),
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -22,4 +34,20 @@ export class AccountingSettings extends Doc {
|
||||
static lists: ListsMap = {
|
||||
country: () => Object.keys(getCountryInfo()),
|
||||
};
|
||||
|
||||
readOnly: ReadOnlyMap = {
|
||||
enableDiscounting: () => {
|
||||
return !!this.enableDiscounting;
|
||||
},
|
||||
};
|
||||
|
||||
async change(ch: ChangeArg) {
|
||||
const discountingEnabled =
|
||||
ch.changed === 'enableDiscounting' && this.enableDiscounting;
|
||||
const discountAccountNotSet = !this.discountAccount;
|
||||
|
||||
if (discountingEnabled && discountAccountNotSet) {
|
||||
await createDiscountAccount(this.fyo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
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 { ValidationError } from 'fyo/utils/errors';
|
||||
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 +17,7 @@ export abstract class Invoice extends Transactional {
|
||||
_taxes: Record<string, Tax> = {};
|
||||
taxes?: TaxSummary[];
|
||||
|
||||
items?: InvoiceItem[];
|
||||
party?: string;
|
||||
account?: string;
|
||||
currency?: string;
|
||||
@ -23,6 +26,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 +38,20 @@ export abstract class Invoice extends Transactional {
|
||||
return this.schemaName === 'SalesInvoice';
|
||||
}
|
||||
|
||||
get enableDiscounting() {
|
||||
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
|
||||
}
|
||||
|
||||
async validate() {
|
||||
await super.validate();
|
||||
if (
|
||||
this.enableDiscounting &&
|
||||
!this.fyo.singles?.AccountingSettings?.discountAccount
|
||||
) {
|
||||
throw new ValidationError(this.fyo.t`Discount Account is not set.`);
|
||||
}
|
||||
}
|
||||
|
||||
async afterSubmit() {
|
||||
await super.afterSubmit();
|
||||
|
||||
@ -123,25 +144,29 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
> = {};
|
||||
|
||||
for (const row of this.items as Doc[]) {
|
||||
if (!row.tax) {
|
||||
type TaxDetail = { account: string; rate: number };
|
||||
|
||||
for (const item of this.items ?? []) {
|
||||
if (!item.tax) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tax = await this.getTax(row.tax as string);
|
||||
for (const d of tax.details as Doc[]) {
|
||||
const account = d.account as string;
|
||||
const rate = d.rate as number;
|
||||
|
||||
taxes[account] = taxes[account] || {
|
||||
const tax = await this.getTax(item.tax!);
|
||||
for (const { account, rate } of tax.details as TaxDetail[]) {
|
||||
taxes[account] ??= {
|
||||
account,
|
||||
rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
baseAmount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
const amount = (row.amount as Money).mul(rate).div(100);
|
||||
taxes[account].amount = taxes[account].amount.add(amount);
|
||||
let amount = item.amount!;
|
||||
if (this.enableDiscounting && !this.discountAfterTax) {
|
||||
amount = item.itemDiscountedTotal!;
|
||||
}
|
||||
|
||||
const taxAmount = amount.mul(rate / 100);
|
||||
taxes[account].amount = taxes[account].amount.add(taxAmount);
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,10 +187,76 @@ export abstract class Invoice extends Transactional {
|
||||
return this._taxes[tax];
|
||||
}
|
||||
|
||||
getTotalDiscount() {
|
||||
if (!this.enableDiscounting) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
const itemDiscountAmount = this.getItemDiscountAmount();
|
||||
const invoiceDiscountAmount = this.getInvoiceDiscountAmount();
|
||||
return itemDiscountAmount.add(invoiceDiscountAmount);
|
||||
}
|
||||
|
||||
async getGrandTotal() {
|
||||
const totalDiscount = this.getTotalDiscount();
|
||||
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);
|
||||
}
|
||||
|
||||
getInvoiceDiscountAmount() {
|
||||
if (!this.enableDiscounting) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
if (this.setDiscountAmount) {
|
||||
return this.discountAmount ?? this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
let totalItemAmounts = this.fyo.pesa(0);
|
||||
for (const item of this.items ?? []) {
|
||||
if (this.discountAfterTax) {
|
||||
totalItemAmounts = totalItemAmounts.add(item.itemTaxedTotal!);
|
||||
} else {
|
||||
totalItemAmounts = totalItemAmounts.add(item.itemDiscountedTotal!);
|
||||
}
|
||||
}
|
||||
|
||||
return totalItemAmounts.percent(this.discountPercent ?? 0);
|
||||
}
|
||||
|
||||
getItemDiscountAmount() {
|
||||
if (!this.enableDiscounting) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
if (!this?.items?.length) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
let discountAmount = this.fyo.pesa(0);
|
||||
for (const item of this.items) {
|
||||
if (item.setItemDiscountAmount) {
|
||||
discountAmount = discountAmount.add(
|
||||
item.itemDiscountAmount ?? this.fyo.pesa(0)
|
||||
);
|
||||
} else if (!this.discountAfterTax) {
|
||||
discountAmount = discountAmount.add(
|
||||
(item.amount ?? this.fyo.pesa(0)).mul(
|
||||
(item.itemDiscountPercent ?? 0) / 100
|
||||
)
|
||||
);
|
||||
} else if (this.discountAfterTax) {
|
||||
discountAmount = discountAmount.add(
|
||||
(item.itemTaxedTotal ?? this.fyo.pesa(0)).mul(
|
||||
(item.itemDiscountPercent ?? 0) / 100
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return discountAmount;
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
@ -215,6 +306,25 @@ export abstract class Invoice extends Transactional {
|
||||
},
|
||||
};
|
||||
|
||||
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: () => true || !this.enableDiscounting,
|
||||
discountAmount: () =>
|
||||
true || !(this.enableDiscounting && !!this.setDiscountAmount),
|
||||
discountPercent: () =>
|
||||
true || !(this.enableDiscounting && !this.setDiscountAmount),
|
||||
discountAfterTax: () => !this.enableDiscounting,
|
||||
};
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
date: () => new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, FormulaMap, ValidationMap } from 'fyo/model/types';
|
||||
import {
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
HiddenMap,
|
||||
ValidationMap
|
||||
} from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
@ -12,11 +17,40 @@ export abstract class InvoiceItem extends Doc {
|
||||
baseAmount?: Money;
|
||||
exchangeRate?: number;
|
||||
parentdoc?: Invoice;
|
||||
rate?: Money;
|
||||
quantity?: number;
|
||||
tax?: string;
|
||||
|
||||
setItemDiscountAmount?: boolean;
|
||||
itemDiscountAmount?: Money;
|
||||
itemDiscountPercent?: number;
|
||||
itemDiscountedTotal?: Money;
|
||||
itemTaxedTotal?: Money;
|
||||
|
||||
get isSales() {
|
||||
return this.schemaName === 'SalesInvoiceItem';
|
||||
}
|
||||
|
||||
get discountAfterTax() {
|
||||
return !!this?.parentdoc?.discountAfterTax;
|
||||
}
|
||||
|
||||
get enableDiscounting() {
|
||||
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
|
||||
}
|
||||
|
||||
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 () =>
|
||||
@ -28,16 +62,51 @@ export abstract class InvoiceItem extends Doc {
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
rate: {
|
||||
formula: async () => {
|
||||
formula: async (fieldname) => {
|
||||
const rate = (await this.fyo.getValue(
|
||||
'Item',
|
||||
this.item as string,
|
||||
'rate'
|
||||
)) as undefined | Money;
|
||||
|
||||
return rate ?? this.fyo.pesa(0);
|
||||
if (
|
||||
fieldname !== 'itemTaxedTotal' &&
|
||||
fieldname !== 'itemDiscountedTotal'
|
||||
) {
|
||||
return rate ?? this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
const quantity = this.quantity ?? 0;
|
||||
const itemDiscountPercent = this.itemDiscountPercent ?? 0;
|
||||
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
|
||||
const totalTaxRate = await this.getTotalTaxRate();
|
||||
const itemTaxedTotal = this.itemTaxedTotal ?? this.fyo.pesa(0);
|
||||
const itemDiscountedTotal =
|
||||
this.itemDiscountedTotal ?? this.fyo.pesa(0);
|
||||
const isItemTaxedTotal = fieldname === 'itemTaxedTotal';
|
||||
const discountAfterTax = this.discountAfterTax;
|
||||
const setItemDiscountAmount = !!this.setItemDiscountAmount;
|
||||
|
||||
const rateFromTotals = getRate(
|
||||
quantity,
|
||||
itemDiscountPercent,
|
||||
itemDiscountAmount,
|
||||
totalTaxRate,
|
||||
itemTaxedTotal,
|
||||
itemDiscountedTotal,
|
||||
isItemTaxedTotal,
|
||||
discountAfterTax,
|
||||
setItemDiscountAmount
|
||||
);
|
||||
|
||||
return rateFromTotals ?? rate ?? this.fyo.pesa(0);
|
||||
},
|
||||
dependsOn: ['item'],
|
||||
dependsOn: [
|
||||
'item',
|
||||
'itemTaxedTotal',
|
||||
'itemDiscountedTotal',
|
||||
'setItemDiscountAmount',
|
||||
],
|
||||
},
|
||||
baseRate: {
|
||||
formula: () =>
|
||||
@ -98,6 +167,84 @@ export abstract class InvoiceItem extends Doc {
|
||||
await this.fyo.getValue('Item', this.item as string, 'hsnCode'),
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
itemDiscountedTotal: {
|
||||
formula: async () => {
|
||||
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.setItemDiscountAmount && this.itemDiscountAmount?.isZero()) {
|
||||
return rate.mul(quantity);
|
||||
}
|
||||
|
||||
if (!this.setItemDiscountAmount && this.itemDiscountPercent === 0) {
|
||||
return rate.mul(quantity);
|
||||
}
|
||||
|
||||
if (!this.discountAfterTax) {
|
||||
return getDiscountedTotalBeforeTaxation(
|
||||
rate,
|
||||
quantity,
|
||||
itemDiscountAmount,
|
||||
itemDiscountPercent,
|
||||
!!this.setItemDiscountAmount
|
||||
);
|
||||
}
|
||||
|
||||
return getDiscountedTotalAfterTaxation(
|
||||
totalTaxRate,
|
||||
rate,
|
||||
quantity,
|
||||
itemDiscountAmount,
|
||||
itemDiscountPercent,
|
||||
!!this.setItemDiscountAmount
|
||||
);
|
||||
},
|
||||
dependsOn: [
|
||||
'itemDiscountAmount',
|
||||
'itemDiscountPercent',
|
||||
'itemTaxedTotal',
|
||||
'setItemDiscountAmount',
|
||||
'tax',
|
||||
'rate',
|
||||
'quantity',
|
||||
'item',
|
||||
],
|
||||
},
|
||||
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,
|
||||
!!this.setItemDiscountAmount
|
||||
);
|
||||
}
|
||||
|
||||
return getTaxedTotalBeforeDiscounting(totalTaxRate, rate, quantity);
|
||||
},
|
||||
dependsOn: [
|
||||
'itemDiscountAmount',
|
||||
'itemDiscountPercent',
|
||||
'itemDiscountedTotal',
|
||||
'setItemDiscountAmount',
|
||||
'tax',
|
||||
'rate',
|
||||
'quantity',
|
||||
'item',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -113,6 +260,55 @@ export abstract class InvoiceItem extends Doc {
|
||||
)}) cannot be less zero.`
|
||||
);
|
||||
},
|
||||
itemDiscountAmount: async (value: DocValue) => {
|
||||
if ((value as Money).lte(this.amount!)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Discount Amount (${this.fyo.format(
|
||||
value,
|
||||
'Currency'
|
||||
)}) cannot be greated than Amount (${this.fyo.format(
|
||||
this.amount!,
|
||||
'Currency'
|
||||
)}).`
|
||||
);
|
||||
},
|
||||
itemDiscountPercent: async (value: DocValue) => {
|
||||
if ((value as number) < 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Discount Percent (${
|
||||
value as number
|
||||
}) cannot be greater than 100.`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
hidden: HiddenMap = {
|
||||
itemDiscountedTotal: () => {
|
||||
if (!this.enableDiscounting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!!this.setItemDiscountAmount && this.itemDiscountAmount?.isZero()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.setItemDiscountAmount && this.itemDiscountPercent === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
setItemDiscountAmount: () => !this.enableDiscounting,
|
||||
itemDiscountAmount: () =>
|
||||
!(this.enableDiscounting && !!this.setItemDiscountAmount),
|
||||
itemDiscountPercent: () =>
|
||||
!(this.enableDiscounting && !this.setItemDiscountAmount),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
@ -136,9 +332,156 @@ export abstract class InvoiceItem extends Doc {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
static createFilters: FiltersMap = {
|
||||
item: (doc: Doc) => {
|
||||
return { for: doc.isSales ? 'Sales' : 'Purchases' };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getDiscountedTotalBeforeTaxation(
|
||||
rate: Money,
|
||||
quantity: number,
|
||||
itemDiscountAmount: Money,
|
||||
itemDiscountPercent: number,
|
||||
setDiscountAmount: boolean
|
||||
) {
|
||||
/**
|
||||
* 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 (setDiscountAmount) {
|
||||
return amount.sub(itemDiscountAmount);
|
||||
}
|
||||
|
||||
return amount.mul(1 - itemDiscountPercent / 100);
|
||||
}
|
||||
|
||||
function getTaxedTotalAfterDiscounting(
|
||||
totalTaxRate: number,
|
||||
rate: Money,
|
||||
quantity: number,
|
||||
itemDiscountAmount: Money,
|
||||
itemDiscountPercent: number,
|
||||
setItemDiscountAmount: boolean
|
||||
) {
|
||||
/**
|
||||
* If Discount is applied before taxation
|
||||
* Formula: Discounted Total * (1 + TotalTaxRate / 100)
|
||||
*/
|
||||
|
||||
const discountedTotal = getDiscountedTotalBeforeTaxation(
|
||||
rate,
|
||||
quantity,
|
||||
itemDiscountAmount,
|
||||
itemDiscountPercent,
|
||||
setItemDiscountAmount
|
||||
);
|
||||
|
||||
return discountedTotal.mul(1 + totalTaxRate / 100);
|
||||
}
|
||||
|
||||
function getDiscountedTotalAfterTaxation(
|
||||
totalTaxRate: number,
|
||||
rate: Money,
|
||||
quantity: number,
|
||||
itemDiscountAmount: Money,
|
||||
itemDiscountPercent: number,
|
||||
setItemDiscountAmount: boolean
|
||||
) {
|
||||
/**
|
||||
* 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 (setItemDiscountAmount) {
|
||||
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);
|
||||
}
|
||||
|
||||
function getRate(
|
||||
quantity: number,
|
||||
itemDiscountPercent: number,
|
||||
itemDiscountAmount: Money,
|
||||
totalTaxRate: number,
|
||||
itemTaxedTotal: Money,
|
||||
itemDiscountedTotal: Money,
|
||||
isItemTaxedTotal: boolean,
|
||||
discountAfterTax: boolean,
|
||||
setItemDiscountAmount: boolean
|
||||
) {
|
||||
const isItemDiscountedTotal = !isItemTaxedTotal;
|
||||
const discountBeforeTax = !discountAfterTax;
|
||||
|
||||
/**
|
||||
* Rate calculated from itemDiscountedTotal
|
||||
*/
|
||||
if (isItemDiscountedTotal && discountBeforeTax && setItemDiscountAmount) {
|
||||
return itemDiscountedTotal.add(itemDiscountAmount).div(quantity);
|
||||
}
|
||||
|
||||
if (isItemDiscountedTotal && discountBeforeTax && !setItemDiscountAmount) {
|
||||
return itemDiscountedTotal.div(quantity * (1 - itemDiscountPercent / 100));
|
||||
}
|
||||
|
||||
if (isItemDiscountedTotal && discountAfterTax && setItemDiscountAmount) {
|
||||
return itemDiscountedTotal
|
||||
.add(itemDiscountAmount)
|
||||
.div(quantity * (1 + totalTaxRate / 100));
|
||||
}
|
||||
|
||||
if (isItemDiscountedTotal && discountAfterTax && !setItemDiscountAmount) {
|
||||
return itemDiscountedTotal.div(
|
||||
(quantity * (100 - itemDiscountPercent) * (100 + totalTaxRate)) / 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate calculated from itemTaxedTotal
|
||||
*/
|
||||
if (isItemTaxedTotal && discountAfterTax) {
|
||||
return itemTaxedTotal.div(quantity * (1 + totalTaxRate / 100));
|
||||
}
|
||||
|
||||
if (isItemTaxedTotal && discountBeforeTax && setItemDiscountAmount) {
|
||||
return itemTaxedTotal
|
||||
.div(1 + totalTaxRate / 100)
|
||||
.add(itemDiscountAmount)
|
||||
.div(quantity);
|
||||
}
|
||||
|
||||
if (isItemTaxedTotal && discountBeforeTax && !setItemDiscountAmount) {
|
||||
return itemTaxedTotal.div(
|
||||
quantity * (1 - itemDiscountPercent / 100) * (1 + totalTaxRate / 100)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -10,14 +10,14 @@ import {
|
||||
HiddenMap,
|
||||
ListViewSettings,
|
||||
RequiredMap,
|
||||
ValidationMap,
|
||||
ValidationMap
|
||||
} from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import {
|
||||
getDocStatus,
|
||||
getLedgerLinkAction,
|
||||
getStatusMap,
|
||||
statusColor,
|
||||
statusColor
|
||||
} from 'models/helpers';
|
||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
@ -366,6 +366,10 @@ export class Payment extends Transactional {
|
||||
formula: async () => this.getSum('for', 'amount', false),
|
||||
dependsOn: ['for'],
|
||||
},
|
||||
amountPaid: {
|
||||
formula: async () => this.amount!.sub(this.writeoff!),
|
||||
dependsOn: ['amount', 'writeoff', 'for'],
|
||||
},
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -404,6 +408,7 @@ export class Payment extends Transactional {
|
||||
hidden: HiddenMap = {
|
||||
referenceId: () => this.paymentMethod === 'Cash',
|
||||
clearanceDate: () => this.paymentMethod === 'Cash',
|
||||
amountPaid: () => this.writeoff?.isZero() ?? true,
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -11,7 +11,6 @@ export class PurchaseInvoice extends Invoice {
|
||||
|
||||
async getPosting() {
|
||||
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
|
||||
|
||||
await posting.credit(this.account!, this.baseGrandTotal!);
|
||||
|
||||
for (const item of this.items!) {
|
||||
@ -24,6 +23,13 @@ export class PurchaseInvoice extends Invoice {
|
||||
}
|
||||
}
|
||||
|
||||
const discountAmount = await this.getTotalDiscount();
|
||||
const discountAccount = this.fyo.singles.AccountingSettings
|
||||
?.discountAccount as string | undefined;
|
||||
if (discountAccount && discountAmount.isPositive()) {
|
||||
await posting.credit(discountAccount, discountAmount);
|
||||
}
|
||||
|
||||
await posting.makeRoundOffEntry();
|
||||
return posting;
|
||||
}
|
||||
|
@ -23,6 +23,13 @@ export class SalesInvoice extends Invoice {
|
||||
}
|
||||
}
|
||||
|
||||
const discountAmount = await this.getTotalDiscount();
|
||||
const discountAccount = this.fyo.singles.AccountingSettings
|
||||
?.discountAccount as string | undefined;
|
||||
if (discountAccount && discountAmount.isPositive()) {
|
||||
await posting.debit(discountAccount, discountAmount);
|
||||
}
|
||||
|
||||
await posting.makeRoundOffEntry();
|
||||
return posting;
|
||||
}
|
||||
|
@ -16,15 +16,13 @@
|
||||
"label": "Write Off Account",
|
||||
"fieldname": "writeOffAccount",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"default": "Write Off"
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"label": "Round Off Account",
|
||||
"fieldname": "roundOffAccount",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"default": "Rounded Off"
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@ -36,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "fullname",
|
||||
"label": "Name",
|
||||
"label": "Full Name",
|
||||
"fieldtype": "Data",
|
||||
"required": true
|
||||
},
|
||||
@ -65,6 +63,18 @@
|
||||
"fieldtype": "Date",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "enableDiscounting",
|
||||
"label": "Enable Discounting",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"label": "Discount Account",
|
||||
"fieldname": "discountAccount",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "setupComplete",
|
||||
"label": "Setup Complete",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"fieldtype": "Link",
|
||||
"target": "UOM",
|
||||
"create": true,
|
||||
"default": "Unit",
|
||||
"placeholder": "Unit Type"
|
||||
},
|
||||
{
|
||||
|
@ -120,6 +120,12 @@
|
||||
"label": "Write Off",
|
||||
"fieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "amountPaid",
|
||||
"label": "Amount Paid",
|
||||
"fieldtype": "Currency",
|
||||
"computed": true
|
||||
},
|
||||
{
|
||||
"fieldname": "for",
|
||||
"label": "Payment Reference",
|
||||
@ -141,6 +147,7 @@
|
||||
"clearanceDate",
|
||||
"amount",
|
||||
"writeoff",
|
||||
"amountPaid",
|
||||
"for"
|
||||
],
|
||||
"keywordFields": ["name", "party", "paymentType"]
|
||||
|
@ -53,7 +53,8 @@
|
||||
"label": "Items",
|
||||
"fieldtype": "Table",
|
||||
"target": "PurchaseInvoiceItem",
|
||||
"required": true
|
||||
"required": true,
|
||||
"edit": true
|
||||
},
|
||||
{
|
||||
"fieldname": "netTotal",
|
||||
@ -92,6 +93,31 @@
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "setDiscountAmount",
|
||||
"label": "Set Discount Amount",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "discountAmount",
|
||||
"label": "Discount Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "discountPercent",
|
||||
"label": "Discount Percent",
|
||||
"fieldtype": "Float",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "discountAfterTax",
|
||||
"label": "Discount After Tax",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "terms",
|
||||
"label": "Terms",
|
||||
|
@ -64,6 +64,38 @@
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "setItemDiscountAmount",
|
||||
"label": "Set Discount Amount",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "itemDiscountAmount",
|
||||
"label": "Discount Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "itemDiscountPercent",
|
||||
"label": "Discount Percent",
|
||||
"fieldtype": "Float",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "itemDiscountedTotal",
|
||||
"label": "Discounted Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false,
|
||||
"computed": true
|
||||
},
|
||||
{
|
||||
"fieldname": "itemTaxedTotal",
|
||||
"label": "Taxed Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false,
|
||||
"computed": true
|
||||
},
|
||||
{
|
||||
"fieldname": "hsnCode",
|
||||
"label": "HSN/SAC",
|
||||
@ -73,5 +105,20 @@
|
||||
}
|
||||
],
|
||||
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
|
||||
"keywordFields": ["item", "tax"]
|
||||
"keywordFields": ["item", "tax"],
|
||||
"quickEditFields": [
|
||||
"item",
|
||||
"account",
|
||||
"description",
|
||||
"hsnCode",
|
||||
"tax",
|
||||
"quantity",
|
||||
"rate",
|
||||
"amount",
|
||||
"setItemDiscountAmount",
|
||||
"itemDiscountAmount",
|
||||
"itemDiscountPercent",
|
||||
"itemDiscountedTotal",
|
||||
"itemTaxedTotal"
|
||||
]
|
||||
}
|
||||
|
@ -52,7 +52,8 @@
|
||||
"label": "Items",
|
||||
"fieldtype": "Table",
|
||||
"target": "SalesInvoiceItem",
|
||||
"required": true
|
||||
"required": true,
|
||||
"edit": true
|
||||
},
|
||||
{
|
||||
"fieldname": "netTotal",
|
||||
@ -91,6 +92,31 @@
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "setDiscountAmount",
|
||||
"label": "Set Discount Amount",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "discountAmount",
|
||||
"label": "Discount Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "discountPercent",
|
||||
"label": "Discount Percent",
|
||||
"fieldtype": "Float",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "discountAfterTax",
|
||||
"label": "Discount After Tax",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "terms",
|
||||
"label": "Notes",
|
||||
|
@ -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",
|
||||
@ -65,14 +61,60 @@
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "setItemDiscountAmount",
|
||||
"label": "Set Discount Amount",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "itemDiscountAmount",
|
||||
"label": "Discount Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "itemDiscountPercent",
|
||||
"label": "Discount Percent",
|
||||
"fieldtype": "Float",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"fieldname": "itemDiscountedTotal",
|
||||
"label": "Discounted Amount",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": false,
|
||||
"computed": true
|
||||
},
|
||||
{
|
||||
"fieldname": "itemTaxedTotal",
|
||||
"label": "Taxed Amount",
|
||||
"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"]
|
||||
"keywordFields": ["item", "tax"],
|
||||
"quickEditFields": [
|
||||
"item",
|
||||
"account",
|
||||
"description",
|
||||
"hsnCode",
|
||||
"tax",
|
||||
"quantity",
|
||||
"rate",
|
||||
"amount",
|
||||
"setItemDiscountAmount",
|
||||
"itemDiscountAmount",
|
||||
"itemDiscountPercent",
|
||||
"itemDiscountedTotal",
|
||||
"itemTaxedTotal"
|
||||
]
|
||||
}
|
||||
|
@ -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 };
|
||||
@ -50,6 +51,7 @@ export interface TargetField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link;
|
||||
target: string; // Name of the table or group of tables to fetch values
|
||||
create?: boolean; // Whether to show Create in the dropdown
|
||||
edit?: boolean; // Whether the Table has quick editable columns
|
||||
}
|
||||
|
||||
export interface DynamicLinkField extends BaseField {
|
||||
|
@ -6,7 +6,6 @@
|
||||
flex
|
||||
justify-center
|
||||
items-center
|
||||
h-8
|
||||
text-sm
|
||||
"
|
||||
:class="_class"
|
||||
@ -35,15 +34,20 @@ export default {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
_class() {
|
||||
return {
|
||||
'opacity-50 cursor-not-allowed pointer-events-none': this.disabled,
|
||||
'text-white': this.type === 'primary',
|
||||
'bg-blue-500': this.type === 'primary',
|
||||
'bg-blue-500': this.type === 'primary' && this.background,
|
||||
'text-gray-700': this.type !== 'primary',
|
||||
'bg-gray-200': this.type !== 'primary',
|
||||
'bg-gray-200': this.type !== 'primary' && this.background,
|
||||
'h-8': this.background,
|
||||
'px-3': this.padding && this.icon,
|
||||
'px-6': this.padding && !this.icon,
|
||||
};
|
||||
|
@ -12,14 +12,8 @@
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-between
|
||||
focus-within:bg-gray-200
|
||||
pr-2
|
||||
rounded
|
||||
"
|
||||
class="flex items-center justify-between pr-2 rounded"
|
||||
:class="isReadOnly ? '' : 'focus-within:bg-gray-200'"
|
||||
>
|
||||
<input
|
||||
ref="input"
|
||||
|
@ -62,8 +62,10 @@ 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',
|
||||
this.isReadOnly ? 'text-gray-800' : 'text-gray-900',
|
||||
'focus:outline-none rounded w-full placeholder-gray-500',
|
||||
this.isReadOnly
|
||||
? 'text-gray-800 focus:bg-transparent'
|
||||
: 'text-gray-900 focus:bg-gray-200',
|
||||
];
|
||||
|
||||
return this.getInputClassesFromProp(classes);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<div class="mr-3 text-gray-900 text-sm" v-if="showLabel && !labelRight">
|
||||
<div class="mr-3 text-gray-600 text-sm" v-if="showLabel && !labelRight">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<div style="width: 14px; height: 14px; overflow: hidden; cursor: pointer">
|
||||
@ -61,7 +61,7 @@
|
||||
@focus="(e) => $emit('focus', e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 text-gray-900 text-sm" v-if="showLabel && labelRight">
|
||||
<div class="ml-3 text-gray-600 text-sm" v-if="showLabel && labelRight">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</label>
|
||||
|
@ -8,7 +8,7 @@
|
||||
ref="input"
|
||||
:class="inputClasses"
|
||||
:type="inputType"
|
||||
:value="value.round()"
|
||||
:value="value?.round()"
|
||||
:placeholder="inputPlaceholder"
|
||||
:readonly="isReadOnly"
|
||||
@blur="onBlur"
|
||||
@ -69,7 +69,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
formattedValue() {
|
||||
return fyo.format(this.value, this.df, this.doc);
|
||||
return fyo.format(this.value ?? fyo.pesa(0), this.df, this.doc);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -38,6 +38,7 @@
|
||||
v-bind="{ row, tableFields, size, ratio, isNumeric }"
|
||||
:read-only="isReadOnly"
|
||||
@remove="removeRow(row)"
|
||||
:can-edit-row="canEditRow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -85,6 +86,7 @@ import TableRow from './TableRow.vue';
|
||||
|
||||
export default {
|
||||
name: 'Table',
|
||||
emits: ['editrow'],
|
||||
extends: Base,
|
||||
props: {
|
||||
value: { type: Array, default: () => [] },
|
||||
@ -169,8 +171,17 @@ export default {
|
||||
}
|
||||
return 2;
|
||||
},
|
||||
canEditRow() {
|
||||
return this.df.edit;
|
||||
},
|
||||
ratio() {
|
||||
return [0.3].concat(this.tableFields.map(() => 1));
|
||||
const ratio = [0.3].concat(this.tableFields.map(() => 1));
|
||||
|
||||
if (this.canEditRow) {
|
||||
return ratio.concat(0.3);
|
||||
}
|
||||
|
||||
return ratio;
|
||||
},
|
||||
tableFields() {
|
||||
const fields = fyo.schemaMap[this.df.target].tableFields ?? [];
|
||||
|
@ -19,6 +19,7 @@
|
||||
<feather-icon
|
||||
name="x"
|
||||
class="w-4 h-4 -ml-1 cursor-pointer"
|
||||
:button="true"
|
||||
@click="$emit('remove')"
|
||||
/>
|
||||
</span>
|
||||
@ -39,6 +40,15 @@
|
||||
@change="(value) => onChange(df, value)"
|
||||
@new-doc="(doc) => row.set(df.fieldname, doc.name)"
|
||||
/>
|
||||
<Button
|
||||
:icon="true"
|
||||
:padding="false"
|
||||
:background="false"
|
||||
@click="openRowQuickEdit"
|
||||
v-if="canEditRow"
|
||||
>
|
||||
<feather-icon name="edit" class="w-4 h-4 text-gray-600" />
|
||||
</Button>
|
||||
|
||||
<!-- Error Display -->
|
||||
<div
|
||||
@ -53,6 +63,7 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import Row from 'src/components/Row.vue';
|
||||
import { getErrorMessage } from 'src/utils';
|
||||
import Button from '../Button.vue';
|
||||
import FormControl from './FormControl.vue';
|
||||
|
||||
export default {
|
||||
@ -64,11 +75,16 @@ export default {
|
||||
ratio: Array,
|
||||
isNumeric: Function,
|
||||
readOnly: Boolean,
|
||||
canEditRow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['remove'],
|
||||
components: {
|
||||
Row,
|
||||
FormControl,
|
||||
Button,
|
||||
},
|
||||
data: () => ({ hovering: false, errors: {} }),
|
||||
beforeCreate() {
|
||||
@ -95,6 +111,13 @@ export default {
|
||||
getErrorString() {
|
||||
return Object.values(this.errors).filter(Boolean).join(' ');
|
||||
},
|
||||
openRowQuickEdit() {
|
||||
if (!this.row) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$parent.$emit('editrow', this.row);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -9,6 +9,8 @@
|
||||
:class="['resize-none', inputClasses]"
|
||||
:value="value"
|
||||
:placeholder="inputPlaceholder"
|
||||
style="vertical-align: top"
|
||||
:readonly="isReadOnly"
|
||||
@blur="(e) => triggerChange(e.target.value)"
|
||||
@focus="(e) => $emit('focus', e)"
|
||||
@input="(e) => $emit('input', e)"
|
||||
|
@ -1,26 +1,31 @@
|
||||
<template>
|
||||
<div class="flex flex-col bg-gray-25">
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<PageHeader :title="title" :border="false">
|
||||
<slot name="header" />
|
||||
</PageHeader>
|
||||
<div class="flex bg-gray-25">
|
||||
<div class="flex flex-1 flex-col">
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<PageHeader :title="title" :border="false">
|
||||
<slot name="header" />
|
||||
</PageHeader>
|
||||
|
||||
<!-- Invoice Form -->
|
||||
<div
|
||||
class="
|
||||
border
|
||||
rounded-lg
|
||||
shadow-lg
|
||||
flex flex-col
|
||||
self-center
|
||||
w-form
|
||||
h-full
|
||||
mb-4
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<slot name="body" />
|
||||
<!-- Invoice Form -->
|
||||
<div
|
||||
class="
|
||||
border
|
||||
rounded-lg
|
||||
shadow-lg
|
||||
flex flex-col
|
||||
self-center
|
||||
w-form
|
||||
h-full
|
||||
mb-4
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<slot name="body" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Quick Edit -->
|
||||
<slot name="quickedit" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -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>
|
||||
|
@ -25,6 +25,9 @@ export default {
|
||||
showHSN() {
|
||||
return this.doc.items.map((i) => i.hsnCode).every(Boolean);
|
||||
},
|
||||
totalDiscount() {
|
||||
return this.doc.getTotalDiscount();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -104,6 +104,13 @@
|
||||
<div>{{ t`Subtotal` }}</div>
|
||||
<div>{{ fyo.format(doc.netTotal, 'Currency') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-3 border-b"
|
||||
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-3"
|
||||
v-for="tax in doc.taxes"
|
||||
@ -112,6 +119,13 @@
|
||||
<div>{{ tax.account }}</div>
|
||||
<div>{{ fyo.format(tax.amount, 'Currency') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-3 border-t"
|
||||
v-if="totalDiscount?.float > 0 && doc.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
|
@ -85,18 +85,25 @@
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<div class="flex -mx-3">
|
||||
<div class="flex justify-end flex-1 p-3 bg-gray-100">
|
||||
<div class="flex justify-end flex-1 py-3 bg-gray-100 gap-8 pr-6">
|
||||
<div class="text-right">
|
||||
<div class="text-gray-800">{{ t`Subtotal` }}</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ fyo.format(doc.netTotal, 'Currency') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ml-8 text-right"
|
||||
v-for="tax in doc.taxes"
|
||||
:key="tax.name"
|
||||
class="text-right"
|
||||
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax"
|
||||
>
|
||||
<div class="text-gray-800">{{ t`Discount` }}</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right" v-for="tax in doc.taxes" :key="tax.name">
|
||||
<div class="text-gray-800">
|
||||
{{ tax.account }}
|
||||
</div>
|
||||
@ -104,9 +111,19 @@
|
||||
{{ fyo.format(tax.amount, 'Currency') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-right"
|
||||
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax"
|
||||
>
|
||||
<div class="text-gray-800">{{ t`Discount` }}</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-3 text-right text-white"
|
||||
class="py-3 px-4 text-right text-white"
|
||||
:style="{ backgroundColor: printSettings.color }"
|
||||
>
|
||||
<div>
|
||||
|
@ -128,6 +128,13 @@
|
||||
<div>{{ t`Subtotal` }}</div>
|
||||
<div>{{ fyo.format(doc.netTotal, 'Currency') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1"
|
||||
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1"
|
||||
v-for="tax in doc.taxes"
|
||||
@ -136,6 +143,13 @@
|
||||
<div>{{ tax.account }}</div>
|
||||
<div>{{ fyo.format(tax.amount, 'Currency') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1"
|
||||
v-if="totalDiscount?.float > 0 && doc.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1 font-semibold"
|
||||
:style="{ color: printSettings.color }"
|
||||
|
@ -32,7 +32,7 @@
|
||||
text-2xl
|
||||
focus:outline-none
|
||||
w-full
|
||||
placeholder-gray-700
|
||||
placeholder-gray-500
|
||||
text-gray-900
|
||||
rounded-md
|
||||
p-3
|
||||
|
@ -62,10 +62,8 @@
|
||||
height: getFieldHeight(df),
|
||||
}"
|
||||
>
|
||||
<div class="py-2 pl-4 flex text-gray-600">
|
||||
<div class="py-1">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<div class="pl-4 flex text-gray-600">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -190,7 +188,7 @@ export default {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.submitted) {
|
||||
if (this.submitted || this.doc.parentdoc?.isSubmitted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -10,6 +10,13 @@
|
||||
>
|
||||
{{ t`Print` }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="true"
|
||||
v-if="!doc?.isSubmitted && doc.enableDiscounting"
|
||||
@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,48 @@
|
||||
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.enableDiscounting"
|
||||
:show-label="true"
|
||||
:label-right="false"
|
||||
class="
|
||||
text-base
|
||||
bg-gray-100
|
||||
rounded
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-ful
|
||||
"
|
||||
input-class="px-3 py-2 text-base bg-transparent text-right"
|
||||
:df="getField('setDiscountAmount')"
|
||||
:value="doc.setDiscountAmount"
|
||||
@change="(value) => doc.set('setDiscountAmount', value)"
|
||||
:read-only="doc?.submitted"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="doc.enableDiscounting && !doc.setDiscountAmount"
|
||||
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"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="doc.enableDiscounting && 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 />
|
||||
|
||||
@ -101,6 +144,7 @@
|
||||
:showHeader="true"
|
||||
:max-rows-before-overflow="4"
|
||||
@change="(value) => doc.set('items', value)"
|
||||
@editrow="toggleQuickEditDoc"
|
||||
:read-only="doc?.submitted"
|
||||
/>
|
||||
</div>
|
||||
@ -110,16 +154,22 @@
|
||||
<div v-if="doc.items?.length ?? 0" class="mt-auto">
|
||||
<hr />
|
||||
<div class="flex justify-between text-base m-4 gap-12">
|
||||
<!-- Form Terms-->
|
||||
<FormControl
|
||||
class="w-1/2 self-end"
|
||||
v-if="!doc?.submitted || doc.terms"
|
||||
:df="getField('terms')"
|
||||
:value="doc.terms"
|
||||
input-class="bg-gray-100"
|
||||
@change="(value) => doc.set('terms', value)"
|
||||
:read-only="doc?.submitted"
|
||||
/>
|
||||
<div class="w-1/2 flex flex-col justify-between">
|
||||
<!-- Discount Note -->
|
||||
<p v-if="discountNote?.length" class="text-gray-600 text-sm">
|
||||
{{ discountNote }}
|
||||
</p>
|
||||
<!-- Form Terms-->
|
||||
<FormControl
|
||||
v-if="!doc?.submitted || doc.terms"
|
||||
:df="getField('terms')"
|
||||
:value="doc.terms"
|
||||
input-class="bg-gray-100"
|
||||
class="mt-auto"
|
||||
@change="(value) => doc.set('terms', value)"
|
||||
:read-only="doc?.submitted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="w-1/2 gap-2 flex flex-col self-end ml-auto">
|
||||
@ -130,24 +180,71 @@
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<!-- Discount Applied Before Taxes -->
|
||||
<div
|
||||
v-if="totalDiscount.float > 0 && !doc.discountAfterTax"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between"
|
||||
v-if="itemDiscountAmount.float > 0"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>
|
||||
{{ `- ${fyo.format(itemDiscountAmount, 'Currency')}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between" v-if="discountAmount.float > 0">
|
||||
<div>{{ t`Invoice Discount` }}</div>
|
||||
<div>{{ `- ${fyo.format(discountAmount, 'Currency')}` }}</div>
|
||||
</div>
|
||||
<hr v-if="doc.taxes?.length" />
|
||||
</div>
|
||||
|
||||
<!-- Taxes -->
|
||||
<div
|
||||
class="flex justify-between"
|
||||
v-for="tax in doc.taxes"
|
||||
:key="tax.name"
|
||||
v-if="doc.taxes?.length"
|
||||
class="flex flex-col gap-2 max-h-12 overflow-y-auto"
|
||||
>
|
||||
<div>{{ tax.account }}</div>
|
||||
<div>
|
||||
{{
|
||||
fyo.format(tax.amount, {
|
||||
fieldtype: 'Currency',
|
||||
currency: doc.currency,
|
||||
})
|
||||
}}
|
||||
<div
|
||||
class="flex justify-between"
|
||||
v-for="tax in doc.taxes"
|
||||
:key="tax.name"
|
||||
>
|
||||
<div>{{ tax.account }}</div>
|
||||
<div>
|
||||
{{
|
||||
fyo.format(tax.amount, {
|
||||
fieldtype: 'Currency',
|
||||
currency: doc.currency,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr v-if="doc.taxes?.length" />
|
||||
|
||||
<!-- Discount Applied After Taxes -->
|
||||
<div
|
||||
v-if="totalDiscount.float > 0 && doc.discountAfterTax"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between"
|
||||
v-if="itemDiscountAmount.float > 0"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>
|
||||
{{ `- ${fyo.format(itemDiscountAmount, 'Currency')}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between" v-if="discountAmount.float > 0">
|
||||
<div>{{ t`Invoice Discount` }}</div>
|
||||
<div>{{ `- ${fyo.format(discountAmount, 'Currency')}` }}</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<!-- Grand Total -->
|
||||
<div
|
||||
class="
|
||||
@ -163,9 +260,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Outstanding Amount -->
|
||||
<hr v-if="doc.outstandingAmount > 0" />
|
||||
<hr v-if="doc.outstandingAmount?.float > 0" />
|
||||
<div
|
||||
v-if="doc.outstandingAmount > 0"
|
||||
v-if="doc.outstandingAmount?.float > 0"
|
||||
class="flex justify-between text-red-600 font-semibold text-base"
|
||||
>
|
||||
<div>{{ t`Outstanding Amount` }}</div>
|
||||
@ -175,6 +272,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #quickedit v-if="quickEditDoc">
|
||||
<QuickEditForm
|
||||
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"
|
||||
:load-on-close="false"
|
||||
@close="toggleQuickEditDoc(null)"
|
||||
/>
|
||||
</template>
|
||||
</FormContainer>
|
||||
</template>
|
||||
<script>
|
||||
@ -190,13 +303,14 @@ import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
docsPath,
|
||||
getActionsForDocument,
|
||||
openSettings,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
docsPath,
|
||||
getActionsForDocument,
|
||||
routeTo,
|
||||
showMessageDialog
|
||||
} from 'src/utils/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { handleErrorWithDialog } from '../errorHandling';
|
||||
import QuickEditForm from './QuickEditForm.vue';
|
||||
|
||||
export default {
|
||||
name: 'InvoiceForm',
|
||||
@ -208,6 +322,7 @@ export default {
|
||||
DropdownWithActions,
|
||||
Table,
|
||||
FormContainer,
|
||||
QuickEditForm,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@ -220,6 +335,8 @@ export default {
|
||||
return {
|
||||
chstatus: false,
|
||||
doc: null,
|
||||
quickEditDoc: null,
|
||||
quickEditFields: [],
|
||||
color: null,
|
||||
printSettings: null,
|
||||
companyName: null,
|
||||
@ -236,6 +353,34 @@ export default {
|
||||
this.chstatus;
|
||||
return getDocStatus(this.doc);
|
||||
},
|
||||
discountNote() {
|
||||
const zeroInvoiceDiscount = this.doc?.discountAmount?.isZero();
|
||||
const zeroItemDiscount = this.itemDiscountAmount?.isZero();
|
||||
|
||||
if (zeroInvoiceDiscount && zeroItemDiscount) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!this.doc?.taxes?.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let text = this.t`Discount applied before taxation`;
|
||||
if (this.doc.discountAfterTax) {
|
||||
text = this.t`Discount applied after taxation`;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
totalDiscount() {
|
||||
return this.discountAmount.add(this.itemDiscountAmount);
|
||||
},
|
||||
discountAmount() {
|
||||
return this.doc?.getInvoiceDiscountAmount();
|
||||
},
|
||||
itemDiscountAmount() {
|
||||
return this.doc.getItemDiscountAmount();
|
||||
},
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = docsPathMap[this.schemaName];
|
||||
@ -267,6 +412,27 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
routeTo,
|
||||
toggleInvoiceSettings() {
|
||||
if (!this.schemaName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = ['discountAfterTax'].map((fn) =>
|
||||
fyo.getField(this.schemaName, fn)
|
||||
);
|
||||
|
||||
this.toggleQuickEditDoc(this.doc, fields);
|
||||
},
|
||||
async toggleQuickEditDoc(doc, fields = []) {
|
||||
if (this.quickEditDoc && doc) {
|
||||
this.quickEditDoc = null;
|
||||
this.quickEditFields = [];
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
this.quickEditDoc = doc;
|
||||
this.quickEditFields = fields;
|
||||
},
|
||||
actions() {
|
||||
return getActionsForDocument(this.doc);
|
||||
},
|
||||
@ -309,9 +475,6 @@ export default {
|
||||
async handleError(e) {
|
||||
await handleErrorWithDialog(e, this.doc);
|
||||
},
|
||||
openInvoiceSettings() {
|
||||
openSettings('Invoice');
|
||||
},
|
||||
formattedValue(fieldname, doc) {
|
||||
if (!doc) {
|
||||
doc = this.doc;
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -4,7 +4,10 @@
|
||||
:class="white ? 'bg-white' : 'bg-gray-25'"
|
||||
>
|
||||
<!-- Quick edit Tool bar -->
|
||||
<div class="flex items-center justify-between px-4 border-b h-row-largest">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 h-row-largest"
|
||||
:class="{ 'border-b': showName }"
|
||||
>
|
||||
<!-- Close Button and Status Text -->
|
||||
<div class="flex items-center">
|
||||
<Button :icon="true" @click="routeToPrevious">
|
||||
@ -16,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Actions, Badge and Status Change Buttons -->
|
||||
<div class="flex items-stretch gap-2">
|
||||
<div class="flex items-stretch gap-2" v-if="showSave">
|
||||
<StatusBadge :status="status" />
|
||||
<DropdownWithActions :actions="actions" />
|
||||
<Button
|
||||
@ -49,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"
|
||||
v-if="doc && showName"
|
||||
>
|
||||
<FormControl
|
||||
v-if="imageField"
|
||||
@ -89,6 +92,7 @@
|
||||
<script>
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { t } from 'fyo';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { getDocStatus } from 'models/helpers';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||
@ -106,6 +110,12 @@ export default {
|
||||
schemaName: String,
|
||||
defaults: String,
|
||||
white: { type: Boolean, default: false },
|
||||
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: () => [] },
|
||||
},
|
||||
@ -116,6 +126,7 @@ export default {
|
||||
TwoColumnForm,
|
||||
DropdownWithActions,
|
||||
},
|
||||
emits: ['close'],
|
||||
provide() {
|
||||
return {
|
||||
schemaName: this.schemaName,
|
||||
@ -145,6 +156,9 @@ export default {
|
||||
await this.fetchFieldsAndDoc();
|
||||
},
|
||||
computed: {
|
||||
isChild() {
|
||||
return !!this?.doc?.schema?.isChild;
|
||||
},
|
||||
schema() {
|
||||
return fyo.schemaMap[this.schemaName] ?? null;
|
||||
},
|
||||
@ -152,6 +166,10 @@ export default {
|
||||
return getDocStatus(this.doc);
|
||||
},
|
||||
fields() {
|
||||
if (this.sourceFields?.length) {
|
||||
return this.sourceFields;
|
||||
}
|
||||
|
||||
if (!this.schema) {
|
||||
return [];
|
||||
}
|
||||
@ -215,8 +233,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (readOnly) {
|
||||
const isManual = this.schema.naming === 'manual';
|
||||
const isNumberSeries = fyo.getField(this.schemaName, 'numberSeries');
|
||||
if (readOnly && (!this?.doc[fieldname] || isNumberSeries)) {
|
||||
this.doc.set(fieldname, t`New ${this.schema.label}`);
|
||||
}
|
||||
|
||||
if (this?.doc[fieldname] && !isManual) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -227,6 +250,10 @@ export default {
|
||||
}, 300);
|
||||
},
|
||||
async fetchDoc() {
|
||||
if (this.sourceDoc) {
|
||||
return (this.doc = this.sourceDoc);
|
||||
}
|
||||
|
||||
if (!this.schemaName) {
|
||||
this.$router.back();
|
||||
}
|
||||
@ -280,10 +307,15 @@ export default {
|
||||
}
|
||||
},
|
||||
routeToPrevious() {
|
||||
if (this.doc.dirty && !this.doc.notInserted) {
|
||||
if (this.loadOnClose && this.doc.dirty && !this.doc.notInserted) {
|
||||
this.doc.load();
|
||||
}
|
||||
this.$router.back();
|
||||
|
||||
if (this.routeBack) {
|
||||
this.$router.back();
|
||||
} else {
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
setTitleSize() {
|
||||
if (!this.$refs.titleControl) {
|
||||
|
@ -95,7 +95,8 @@ export default {
|
||||
if (
|
||||
fieldnames.includes('displayPrecision') ||
|
||||
fieldnames.includes('hideGetStarted') ||
|
||||
fieldnames.includes('displayPrecision')
|
||||
fieldnames.includes('displayPrecision') ||
|
||||
fieldnames.includes('enableDiscounting')
|
||||
) {
|
||||
this.showReloadToast();
|
||||
}
|
||||
|
@ -37,17 +37,33 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return [
|
||||
const fields = [
|
||||
'fullname',
|
||||
'companyName',
|
||||
'country',
|
||||
'bankName',
|
||||
'currency',
|
||||
'writeOffAccount',
|
||||
'roundOffAccount',
|
||||
'fiscalYearStart',
|
||||
'fiscalYearEnd',
|
||||
'gstin',
|
||||
].map((fieldname) => fyo.getField('AccountingSettings', fieldname));
|
||||
'writeOffAccount',
|
||||
'roundOffAccount',
|
||||
];
|
||||
|
||||
if (!this.doc.enableDiscounting) {
|
||||
fields.push('enableDiscounting');
|
||||
}
|
||||
|
||||
if (this.doc.enableDiscounting) {
|
||||
fields.push('discountAccount');
|
||||
}
|
||||
|
||||
if (fyo.singles.SystemSettings.countryCode === 'in') {
|
||||
fields.push('gstin');
|
||||
}
|
||||
|
||||
return fields.map((fieldname) =>
|
||||
fyo.getField('AccountingSettings', fieldname)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -5,8 +5,9 @@ import { createNumberSeries } from 'fyo/model/naming';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_LOCALE,
|
||||
DEFAULT_SERIES_START,
|
||||
DEFAULT_SERIES_START
|
||||
} from 'fyo/utils/consts';
|
||||
import { AccountRootTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { initializeInstance } from 'src/initFyo';
|
||||
@ -159,14 +160,65 @@ async function createAccountRecords(
|
||||
const createCOA = new CreateCOA(chartOfAccounts, fyo);
|
||||
await createCOA.run();
|
||||
const parentAccount = await getBankAccountParentName(country, fyo);
|
||||
const docObject = {
|
||||
const bankAccountDoc = {
|
||||
name: bankName,
|
||||
rootType: 'Asset',
|
||||
rootType: AccountRootTypeEnum.Asset,
|
||||
parentAccount,
|
||||
accountType: 'Bank',
|
||||
isGroup: false,
|
||||
};
|
||||
await checkAndCreateDoc('Account', docObject, fyo);
|
||||
|
||||
await checkAndCreateDoc('Account', bankAccountDoc, fyo);
|
||||
await createDiscountAccount(fyo);
|
||||
await setDefaultAccounts(fyo);
|
||||
}
|
||||
|
||||
export async function createDiscountAccount(fyo: Fyo) {
|
||||
const incomeAccountName = fyo.t`Indirect Income`;
|
||||
const accountExists = await fyo.db.exists(
|
||||
ModelNameEnum.Account,
|
||||
incomeAccountName
|
||||
);
|
||||
|
||||
if (!accountExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const discountAccountName = fyo.t`Discounts`;
|
||||
const discountAccountDoc = {
|
||||
name: discountAccountName,
|
||||
rootType: AccountRootTypeEnum.Income,
|
||||
parentAccount: incomeAccountName,
|
||||
accountType: 'Income Account',
|
||||
isGroup: false,
|
||||
};
|
||||
|
||||
await checkAndCreateDoc(ModelNameEnum.Account, discountAccountDoc, fyo);
|
||||
await fyo.singles.AccountingSettings!.setAndSync(
|
||||
'discountAccount',
|
||||
discountAccountName
|
||||
);
|
||||
}
|
||||
|
||||
async function setDefaultAccounts(fyo: Fyo) {
|
||||
const accountMap: Record<string, string> = {
|
||||
writeOffAccount: fyo.t`Write Off`,
|
||||
roundOffAccount: fyo.t`Rounded Off`,
|
||||
};
|
||||
|
||||
for (const key in accountMap) {
|
||||
const accountName = accountMap[key];
|
||||
const accountExists = await fyo.db.exists(
|
||||
ModelNameEnum.Account,
|
||||
accountName
|
||||
);
|
||||
|
||||
if (!accountExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await fyo.singles.AccountingSettings!.setAndSync(key, accountName);
|
||||
}
|
||||
}
|
||||
|
||||
async function completeSetup(companyName: string, fyo: Fyo) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user