mirror of
https://github.com/frappe/books.git
synced 2024-09-20 03:29:00 +00:00
incr: complete typing models
- fix model exports - add a README for models base
This commit is contained in:
parent
91bf6e03fa
commit
591e7b3163
@ -33,6 +33,7 @@ import {
|
|||||||
EmptyMessageMap,
|
EmptyMessageMap,
|
||||||
FiltersMap,
|
FiltersMap,
|
||||||
FormulaMap,
|
FormulaMap,
|
||||||
|
FormulaReturn,
|
||||||
HiddenMap,
|
HiddenMap,
|
||||||
ListsMap,
|
ListsMap,
|
||||||
ListViewSettings,
|
ListViewSettings,
|
||||||
@ -507,7 +508,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getValueFromFormula(field: Field, doc: Doc) {
|
async getValueFromFormula(field: Field, doc: Doc) {
|
||||||
let value: Doc[] | DocValue | undefined;
|
let value: FormulaReturn;
|
||||||
|
|
||||||
const formula = doc.formulas[field.fieldtype];
|
const formula = doc.formulas[field.fieldtype];
|
||||||
if (formula === undefined) {
|
if (formula === undefined) {
|
||||||
@ -515,7 +516,6 @@ export default class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
value = await formula();
|
value = await formula();
|
||||||
|
|
||||||
if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) {
|
if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) {
|
||||||
value = value.map((row) => this._initChild(row, field.fieldname));
|
value = value.map((row) => this._initChild(row, field.fieldname));
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DocValue } from 'frappe/core/types';
|
import { DocValue, DocValueMap } from 'frappe/core/types';
|
||||||
import { FieldType } from 'schemas/types';
|
import { FieldType } from 'schemas/types';
|
||||||
import { QueryFilter } from 'utils/db/types';
|
import { QueryFilter } from 'utils/db/types';
|
||||||
import { Router } from 'vue-router';
|
import { Router } from 'vue-router';
|
||||||
@ -16,7 +16,8 @@ import Doc from './doc';
|
|||||||
* - `Validation`: Async function that throw an error if the value is invalid.
|
* - `Validation`: Async function that throw an error if the value is invalid.
|
||||||
* - `Required`: Regular function used to decide if a value is mandatory (there are !notnul in the db).
|
* - `Required`: Regular function used to decide if a value is mandatory (there are !notnul in the db).
|
||||||
*/
|
*/
|
||||||
export type Formula = () => Promise<DocValue | undefined>;
|
export type FormulaReturn = DocValue | DocValueMap[] | undefined | Doc[];
|
||||||
|
export type Formula = () => Promise<FormulaReturn> | FormulaReturn;
|
||||||
export type Default = () => DocValue;
|
export type Default = () => DocValue;
|
||||||
export type Validation = (value: DocValue) => Promise<void>;
|
export type Validation = (value: DocValue) => Promise<void>;
|
||||||
export type Required = () => boolean;
|
export type Required = () => boolean;
|
||||||
|
32
models/README.md
Normal file
32
models/README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Models
|
||||||
|
|
||||||
|
The `models` root folder contains all the model files, i.e. files containing all the models. **Models** here, refers to the classes that handle the data, its validation and updation and a bunch of other stuff.
|
||||||
|
|
||||||
|
Each model directly or indirectly extends the `Doc` class from `frappe/model/doc.ts` so for more info check that file and the associated types in `frappe/model/types.ts`.
|
||||||
|
|
||||||
|
A model class can used even if the class body has no content, for example `PurchaseInvoiceItem`. Else the model used will default to using `Doc`. The class can also be used to provide type information for the field types else they default to the catch all `DocValue` example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Todo extends Doc {
|
||||||
|
title?: string;
|
||||||
|
date?: Date;
|
||||||
|
completed?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While this has obvious advantages, the drawback is if the underlying fieldtype changes this too will have to be changed.
|
||||||
|
|
||||||
|
## Adding Stuff
|
||||||
|
|
||||||
|
When adding stuff to `models/**` make sure that it isn't importing any Vue code or other frontend specific code globally. This is cause model file tests will directly use the the `Frappe` class and will be run using `mocha` on `node`.
|
||||||
|
|
||||||
|
Importing frontend code will break all the tests. This also implies that one should be wary about transitive dependencies.
|
||||||
|
|
||||||
|
_Note: Frontend specific code can be imported but they should be done so, only using dynamic imports i.e. `await import('...')`._
|
||||||
|
|
||||||
|
## Regional Models
|
||||||
|
|
||||||
|
Regional models should as far as possible extend the base model and override what's required.
|
||||||
|
|
||||||
|
They should then be imported dynamicall and returned from `getRegionalModels` in `models/index.ts` on the basis of `countryCode`.
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from 'frappe/model/types';
|
} from 'frappe/model/types';
|
||||||
import { QueryFilter } from 'utils/db/types';
|
import { QueryFilter } from 'utils/db/types';
|
||||||
|
|
||||||
export default class Account extends Doc {
|
export class Account extends Doc {
|
||||||
async beforeInsert() {
|
async beforeInsert() {
|
||||||
if (this.accountType || !this.parentAccount) {
|
if (this.accountType || !this.parentAccount) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,21 +1,36 @@
|
|||||||
import { LedgerPosting } from 'accounting/ledgerPosting';
|
import { LedgerPosting } from 'accounting/ledgerPosting';
|
||||||
import frappe from 'frappe';
|
import frappe from 'frappe';
|
||||||
|
import { DocValue } from 'frappe/core/types';
|
||||||
import Doc from 'frappe/model/doc';
|
import Doc from 'frappe/model/doc';
|
||||||
|
import { DefaultMap, FiltersMap, FormulaMap } from 'frappe/model/types';
|
||||||
import Money from 'pesa/dist/types/src/money';
|
import Money from 'pesa/dist/types/src/money';
|
||||||
import { getExchangeRate } from '../../../accounting/exchangeRate';
|
import { getExchangeRate } from '../../../accounting/exchangeRate';
|
||||||
import { Party } from '../Party/Party';
|
import { Party } from '../Party/Party';
|
||||||
import { Payment } from '../Payment/Payment';
|
import { Payment } from '../Payment/Payment';
|
||||||
import { Tax } from '../Tax/Tax';
|
import { Tax } from '../Tax/Tax';
|
||||||
|
import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||||
|
|
||||||
export abstract class Transaction extends Doc {
|
export abstract class Invoice extends Doc {
|
||||||
_taxes: Record<string, Tax> = {};
|
_taxes: Record<string, Tax> = {};
|
||||||
|
taxes?: TaxSummary[];
|
||||||
|
|
||||||
abstract getPosting(): LedgerPosting;
|
party?: string;
|
||||||
|
account?: string;
|
||||||
|
currency?: string;
|
||||||
|
netTotal?: Money;
|
||||||
|
baseGrandTotal?: Money;
|
||||||
|
exchangeRate?: number;
|
||||||
|
|
||||||
|
abstract getPosting(): Promise<LedgerPosting>;
|
||||||
|
|
||||||
|
get isSales() {
|
||||||
|
return this.schemaName === 'SalesInvoice';
|
||||||
|
}
|
||||||
|
|
||||||
async getPayments() {
|
async getPayments() {
|
||||||
const payments = await frappe.db.getAll('PaymentFor', {
|
const payments = await frappe.db.getAll('PaymentFor', {
|
||||||
fields: ['parent'],
|
fields: ['parent'],
|
||||||
filters: { referenceName: this.name as string },
|
filters: { referenceName: this.name! },
|
||||||
orderBy: 'name',
|
orderBy: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,13 +58,10 @@ export abstract class Transaction extends Doc {
|
|||||||
// update outstanding amounts
|
// update outstanding amounts
|
||||||
await frappe.db.update(this.schemaName, {
|
await frappe.db.update(this.schemaName, {
|
||||||
name: this.name as string,
|
name: this.name as string,
|
||||||
outstandingAmount: this.baseGrandTotal as Money,
|
outstandingAmount: this.baseGrandTotal!,
|
||||||
});
|
});
|
||||||
|
|
||||||
const party = (await frappe.doc.getDoc(
|
const party = (await frappe.doc.getDoc('Party', this.party!)) as Party;
|
||||||
'Party',
|
|
||||||
this.party as string
|
|
||||||
)) as Party;
|
|
||||||
await party.updateOutstandingAmount();
|
await party.updateOutstandingAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +99,7 @@ export abstract class Transaction extends Doc {
|
|||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
return await getExchangeRate({
|
return await getExchangeRate({
|
||||||
fromCurrency: this.currency as string,
|
fromCurrency: this.currency!,
|
||||||
toCurrency: companyCurrency as string,
|
toCurrency: companyCurrency as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -95,7 +107,13 @@ export abstract class Transaction extends Doc {
|
|||||||
async getTaxSummary() {
|
async getTaxSummary() {
|
||||||
const taxes: Record<
|
const taxes: Record<
|
||||||
string,
|
string,
|
||||||
{ account: string; rate: number; amount: Money; baseAmount?: Money }
|
{
|
||||||
|
account: string;
|
||||||
|
rate: number;
|
||||||
|
amount: Money;
|
||||||
|
baseAmount: Money;
|
||||||
|
[key: string]: DocValue;
|
||||||
|
}
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
for (const row of this.items as Doc[]) {
|
for (const row of this.items as Doc[]) {
|
||||||
@ -112,6 +130,7 @@ export abstract class Transaction extends Doc {
|
|||||||
account,
|
account,
|
||||||
rate,
|
rate,
|
||||||
amount: frappe.pesa(0),
|
amount: frappe.pesa(0),
|
||||||
|
baseAmount: frappe.pesa(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
const amount = (row.amount as Money).mul(rate).div(100);
|
const amount = (row.amount as Money).mul(rate).div(100);
|
||||||
@ -122,7 +141,7 @@ export abstract class Transaction extends Doc {
|
|||||||
return Object.keys(taxes)
|
return Object.keys(taxes)
|
||||||
.map((account) => {
|
.map((account) => {
|
||||||
const tax = taxes[account];
|
const tax = taxes[account];
|
||||||
tax.baseAmount = tax.amount.mul(this.exchangeRate as number);
|
tax.baseAmount = tax.amount.mul(this.exchangeRate!);
|
||||||
return tax;
|
return tax;
|
||||||
})
|
})
|
||||||
.filter((tax) => !tax.amount.isZero());
|
.filter((tax) => !tax.amount.isZero());
|
||||||
@ -139,6 +158,40 @@ export abstract class Transaction extends Doc {
|
|||||||
async getGrandTotal() {
|
async getGrandTotal() {
|
||||||
return ((this.taxes ?? []) as Doc[])
|
return ((this.taxes ?? []) as Doc[])
|
||||||
.map((doc) => doc.amount as Money)
|
.map((doc) => doc.amount as Money)
|
||||||
.reduce((a, b) => a.add(b), this.netTotal as Money);
|
.reduce((a, b) => a.add(b), this.netTotal!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formulas: FormulaMap = {
|
||||||
|
account: async () =>
|
||||||
|
this.getFrom('Party', this.party!, 'defaultAccount') as string,
|
||||||
|
currency: async () =>
|
||||||
|
(this.getFrom('Party', this.party!, 'currency') as string) ||
|
||||||
|
(frappe.singles.AccountingSettings!.currency as string),
|
||||||
|
exchangeRate: async () => await this.getExchangeRate(),
|
||||||
|
netTotal: async () => this.getSum('items', 'amount', false),
|
||||||
|
baseNetTotal: async () => this.netTotal!.mul(this.exchangeRate!),
|
||||||
|
taxes: async () => await this.getTaxSummary(),
|
||||||
|
grandTotal: async () => await this.getGrandTotal(),
|
||||||
|
baseGrandTotal: async () =>
|
||||||
|
(this.grandTotal as Money).mul(this.exchangeRate!),
|
||||||
|
outstandingAmount: async () => {
|
||||||
|
if (this.submitted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.baseGrandTotal!;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
defaults: DefaultMap = {
|
||||||
|
date: () => new Date().toISOString().slice(0, 10),
|
||||||
|
};
|
||||||
|
|
||||||
|
static filters: FiltersMap = {
|
||||||
|
account: (doc: Doc) => ({
|
||||||
|
isGroup: false,
|
||||||
|
accountType: doc.isSales ? 'Receivable' : 'Payable',
|
||||||
|
}),
|
||||||
|
numberSeries: (doc: Doc) => ({ referenceType: doc.schemaName }),
|
||||||
|
};
|
||||||
}
|
}
|
106
models/baseModels/InvoiceItem/InvoiceItem.ts
Normal file
106
models/baseModels/InvoiceItem/InvoiceItem.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import frappe from 'frappe';
|
||||||
|
import { DocValue } from 'frappe/core/types';
|
||||||
|
import Doc from 'frappe/model/doc';
|
||||||
|
import {
|
||||||
|
DependsOnMap,
|
||||||
|
FiltersMap,
|
||||||
|
FormulaMap,
|
||||||
|
ValidationMap,
|
||||||
|
} from 'frappe/model/types';
|
||||||
|
import Money from 'pesa/dist/types/src/money';
|
||||||
|
import { Invoice } from '../Invoice/Invoice';
|
||||||
|
|
||||||
|
export abstract class InvoiceItem extends Doc {
|
||||||
|
account?: string;
|
||||||
|
baseAmount?: Money;
|
||||||
|
exchangeRate?: number;
|
||||||
|
parentdoc?: Invoice;
|
||||||
|
|
||||||
|
get isSales() {
|
||||||
|
return this.schemaName === 'SalesInvoiceItem';
|
||||||
|
}
|
||||||
|
|
||||||
|
formulas: FormulaMap = {
|
||||||
|
description: () =>
|
||||||
|
this.parentdoc!.getFrom(
|
||||||
|
'Item',
|
||||||
|
this.item as string,
|
||||||
|
'description'
|
||||||
|
) as string,
|
||||||
|
rate: async () => {
|
||||||
|
const baseRate = ((await this.parentdoc!.getFrom(
|
||||||
|
'Item',
|
||||||
|
this.item as string,
|
||||||
|
'rate'
|
||||||
|
)) || frappe.pesa(0)) as Money;
|
||||||
|
|
||||||
|
return baseRate.div(this.exchangeRate!);
|
||||||
|
},
|
||||||
|
baseRate: () =>
|
||||||
|
(this.rate as Money).mul(this.parentdoc!.exchangeRate as number),
|
||||||
|
account: () => {
|
||||||
|
let accountType = 'expenseAccount';
|
||||||
|
if (this.isSales) {
|
||||||
|
accountType = 'incomeAccount';
|
||||||
|
}
|
||||||
|
return this.parentdoc!.getFrom('Item', this.item as string, accountType);
|
||||||
|
},
|
||||||
|
tax: () => {
|
||||||
|
if (this.tax) {
|
||||||
|
return this.tax as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parentdoc!.getFrom(
|
||||||
|
'Item',
|
||||||
|
this.item as string,
|
||||||
|
'tax'
|
||||||
|
) as string;
|
||||||
|
},
|
||||||
|
amount: () => (this.rate as Money).mul(this.quantity as number),
|
||||||
|
baseAmount: () =>
|
||||||
|
(this.amount as Money).mul(this.parentdoc!.exchangeRate as number),
|
||||||
|
hsnCode: () =>
|
||||||
|
this.parentdoc!.getFrom('Item', this.item as string, 'hsnCode'),
|
||||||
|
};
|
||||||
|
|
||||||
|
dependsOn: DependsOnMap = {
|
||||||
|
hsnCode: ['item'],
|
||||||
|
};
|
||||||
|
|
||||||
|
validations: ValidationMap = {
|
||||||
|
rate: async (value: DocValue) => {
|
||||||
|
if ((value as Money).gte(0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new frappe.errors.ValidationError(
|
||||||
|
frappe.t`Rate (${frappe.format(
|
||||||
|
value,
|
||||||
|
'Currency'
|
||||||
|
)}) cannot be less zero.`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static filters: FiltersMap = {
|
||||||
|
item: (doc: Doc) => {
|
||||||
|
const itemList = doc.parentdoc!.items as Doc[];
|
||||||
|
const items = itemList.map((d) => d.item as string).filter(Boolean);
|
||||||
|
|
||||||
|
let itemNotFor = 'sales';
|
||||||
|
if (doc.isSales) {
|
||||||
|
itemNotFor = 'purchases';
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFilter = { for: ['not in', [itemNotFor]] };
|
||||||
|
if (items.length <= 0) {
|
||||||
|
return baseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: ['not in', items],
|
||||||
|
...baseFilter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -20,6 +20,8 @@ import { Party } from '../Party/Party';
|
|||||||
import { PaymentMethod, PaymentType } from './types';
|
import { PaymentMethod, PaymentType } from './types';
|
||||||
|
|
||||||
export class Payment extends Doc {
|
export class Payment extends Doc {
|
||||||
|
party?: string;
|
||||||
|
|
||||||
async change({ changed }: { changed: string }) {
|
async change({ changed }: { changed: string }) {
|
||||||
switch (changed) {
|
switch (changed) {
|
||||||
case 'for': {
|
case 'for': {
|
||||||
@ -59,7 +61,7 @@ export class Payment extends Doc {
|
|||||||
paymentType = 'Pay';
|
paymentType = 'Pay';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.party = party;
|
this.party = party as string;
|
||||||
this.paymentType = paymentType;
|
this.paymentType = paymentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +154,7 @@ export class Payment extends Doc {
|
|||||||
const writeoff = this.writeoff as Money;
|
const writeoff = this.writeoff as Money;
|
||||||
const entries = new LedgerPosting({
|
const entries = new LedgerPosting({
|
||||||
reference: this,
|
reference: this,
|
||||||
party: this.party as string,
|
party: this.party!,
|
||||||
});
|
});
|
||||||
|
|
||||||
await entries.debit(paymentAccount as string, amount.sub(writeoff));
|
await entries.debit(paymentAccount as string, amount.sub(writeoff));
|
||||||
@ -164,7 +166,7 @@ export class Payment extends Doc {
|
|||||||
|
|
||||||
const writeoffEntry = new LedgerPosting({
|
const writeoffEntry = new LedgerPosting({
|
||||||
reference: this,
|
reference: this,
|
||||||
party: this.party as string,
|
party: this.party!,
|
||||||
});
|
});
|
||||||
const writeOffAccount = frappe.singles.AccountingSettings!
|
const writeOffAccount = frappe.singles.AccountingSettings!
|
||||||
.writeOffAccount as string;
|
.writeOffAccount as string;
|
||||||
@ -227,10 +229,7 @@ export class Payment extends Doc {
|
|||||||
const newOutstanding = outstandingAmount.sub(amount);
|
const newOutstanding = outstandingAmount.sub(amount);
|
||||||
await referenceDoc.set('outstandingAmount', newOutstanding);
|
await referenceDoc.set('outstandingAmount', newOutstanding);
|
||||||
await referenceDoc.update();
|
await referenceDoc.update();
|
||||||
const party = (await frappe.doc.getDoc(
|
const party = (await frappe.doc.getDoc('Party', this.party!)) as Party;
|
||||||
'Party',
|
|
||||||
this.party as string
|
|
||||||
)) as Party;
|
|
||||||
|
|
||||||
await party.updateOutstandingAmount();
|
await party.updateOutstandingAmount();
|
||||||
}
|
}
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
|
|
||||||
import InvoiceTemplate from '../SalesInvoice/InvoiceTemplate.vue';
|
|
||||||
import { getActions } from '../Transaction/Transaction';
|
|
||||||
import PurchaseInvoice from './PurchaseInvoiceDocument';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PurchaseInvoice',
|
|
||||||
doctype: 'DocType',
|
|
||||||
label: t`Bill`,
|
|
||||||
documentClass: PurchaseInvoice,
|
|
||||||
printTemplate: InvoiceTemplate,
|
|
||||||
isSingle: 0,
|
|
||||||
isChild: 0,
|
|
||||||
isSubmittable: 1,
|
|
||||||
keywordFields: ['name', 'supplier'],
|
|
||||||
settings: 'PurchaseInvoiceSettings',
|
|
||||||
showTitle: true,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: t`Bill No`,
|
|
||||||
fieldname: 'name',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1,
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'date',
|
|
||||||
label: t`Date`,
|
|
||||||
fieldtype: 'Date',
|
|
||||||
default: () => new Date().toISOString().slice(0, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'supplier',
|
|
||||||
label: t`Supplier`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Supplier',
|
|
||||||
required: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'account',
|
|
||||||
label: t`Account`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Account',
|
|
||||||
formula: (doc) => doc.getFrom('Party', doc.supplier, 'defaultAccount'),
|
|
||||||
getFilters: () => {
|
|
||||||
return {
|
|
||||||
isGroup: 0,
|
|
||||||
accountType: 'Payable',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'currency',
|
|
||||||
label: t`Supplier Currency`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Currency',
|
|
||||||
hidden: 1,
|
|
||||||
formula: (doc) =>
|
|
||||||
doc.getFrom('Party', doc.supplier, 'currency') ||
|
|
||||||
frappe.AccountingSettings.currency,
|
|
||||||
formulaDependsOn: ['supplier'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'exchangeRate',
|
|
||||||
label: t`Exchange Rate`,
|
|
||||||
fieldtype: 'Float',
|
|
||||||
default: 1,
|
|
||||||
formula: async (doc) => await doc.getExchangeRate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'items',
|
|
||||||
label: t`Items`,
|
|
||||||
fieldtype: 'Table',
|
|
||||||
childtype: 'PurchaseInvoiceItem',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'netTotal',
|
|
||||||
label: t`Net Total`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.getSum('items', 'amount', false),
|
|
||||||
readOnly: 1,
|
|
||||||
getCurrency: (doc) => doc.currency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseNetTotal',
|
|
||||||
label: t`Net Total (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.netTotal.mul(doc.exchangeRate),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'taxes',
|
|
||||||
label: t`Taxes`,
|
|
||||||
fieldtype: 'Table',
|
|
||||||
childtype: 'TaxSummary',
|
|
||||||
formula: (doc) => doc.getTaxSummary(),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'grandTotal',
|
|
||||||
label: t`Grand Total`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.getGrandTotal(),
|
|
||||||
readOnly: 1,
|
|
||||||
getCurrency: (doc) => doc.currency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseGrandTotal',
|
|
||||||
label: t`Grand Total (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.grandTotal.mul(doc.exchangeRate),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'outstandingAmount',
|
|
||||||
label: t`Outstanding Amount`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => {
|
|
||||||
if (doc.submitted) return;
|
|
||||||
return doc.baseGrandTotal;
|
|
||||||
},
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'terms',
|
|
||||||
label: t`Terms`,
|
|
||||||
fieldtype: 'Text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'cancelled',
|
|
||||||
label: t`Cancelled`,
|
|
||||||
fieldtype: 'Check',
|
|
||||||
default: 0,
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'numberSeries',
|
|
||||||
label: t`Number Series`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'NumberSeries',
|
|
||||||
required: 1,
|
|
||||||
getFilters: () => {
|
|
||||||
return { referenceType: 'PurchaseInvoice' };
|
|
||||||
},
|
|
||||||
default: DEFAULT_NUMBER_SERIES['PurchaseInvoice'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
actions: getActions('PurchaseInvoice'),
|
|
||||||
};
|
|
@ -0,0 +1,48 @@
|
|||||||
|
import { LedgerPosting } from 'accounting/ledgerPosting';
|
||||||
|
import { Action, ListViewSettings } from 'frappe/model/types';
|
||||||
|
import {
|
||||||
|
getTransactionActions,
|
||||||
|
getTransactionStatusColumn,
|
||||||
|
} from '../../helpers';
|
||||||
|
import { Invoice } from '../Invoice/Invoice';
|
||||||
|
import { PurchaseInvoiceItem } from '../PurchaseInvoiceItem/PurchaseInvoiceItem';
|
||||||
|
|
||||||
|
export class PurchaseInvoice extends Invoice {
|
||||||
|
items?: PurchaseInvoiceItem[];
|
||||||
|
|
||||||
|
async getPosting() {
|
||||||
|
const entries: LedgerPosting = new LedgerPosting({
|
||||||
|
reference: this,
|
||||||
|
party: this.party,
|
||||||
|
});
|
||||||
|
|
||||||
|
await entries.credit(this.account!, this.baseGrandTotal!);
|
||||||
|
|
||||||
|
for (const item of this.items!) {
|
||||||
|
await entries.debit(item.account!, item.baseAmount!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.taxes) {
|
||||||
|
for (const tax of this.taxes) {
|
||||||
|
await entries.debit(tax.account!, tax.baseAmount!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.makeRoundOffEntry();
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
static actions: Action[] = getTransactionActions('PurchaseInvoice');
|
||||||
|
|
||||||
|
static listSettings: ListViewSettings = {
|
||||||
|
formRoute: (name) => `/edit/PurchaseInvoice/${name}`,
|
||||||
|
columns: [
|
||||||
|
'party',
|
||||||
|
'name',
|
||||||
|
getTransactionStatusColumn(),
|
||||||
|
'date',
|
||||||
|
'grandTotal',
|
||||||
|
'outstandingAmount',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
import TransactionDocument from '../Transaction/TransactionDocument';
|
|
||||||
|
|
||||||
export default class PurchaseInvoice extends TransactionDocument {};
|
|
@ -1,16 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
import { getStatusColumn } from '../Transaction/Transaction';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
doctype: 'PurchaseInvoice',
|
|
||||||
title: t`Bills`,
|
|
||||||
formRoute: (name) => `/edit/PurchaseInvoice/${name}`,
|
|
||||||
columns: [
|
|
||||||
'supplier',
|
|
||||||
'name',
|
|
||||||
getStatusColumn('PurchaseInvoice'),
|
|
||||||
'date',
|
|
||||||
'grandTotal',
|
|
||||||
'outstandingAmount',
|
|
||||||
],
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
import TransactionServer from '../Transaction/TransactionServer';
|
|
||||||
import PurchaseInvoice from './PurchaseInvoiceDocument';
|
|
||||||
import LedgerPosting from '../../../accounting/ledgerPosting';
|
|
||||||
|
|
||||||
class PurchaseInvoiceServer extends PurchaseInvoice {
|
|
||||||
async getPosting() {
|
|
||||||
let entries = new LedgerPosting({ reference: this, party: this.supplier });
|
|
||||||
await entries.credit(this.account, this.baseGrandTotal);
|
|
||||||
|
|
||||||
for (let item of this.items) {
|
|
||||||
await entries.debit(item.account, item.baseAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.taxes) {
|
|
||||||
for (let tax of this.taxes) {
|
|
||||||
await entries.debit(tax.account, tax.baseAmount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries.makeRoundOffEntry();
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply common methods from TransactionServer
|
|
||||||
Object.assign(PurchaseInvoiceServer.prototype, TransactionServer);
|
|
||||||
|
|
||||||
export default PurchaseInvoiceServer;
|
|
@ -1,109 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
export default {
|
|
||||||
name: 'PurchaseInvoiceItem',
|
|
||||||
doctype: 'DocType',
|
|
||||||
isChild: 1,
|
|
||||||
keywordFields: [],
|
|
||||||
tableFields: ['item', 'tax', 'quantity', 'rate', 'amount'],
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
fieldname: 'item',
|
|
||||||
label: t`Item`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Item',
|
|
||||||
required: 1,
|
|
||||||
getFilters(_, doc) {
|
|
||||||
let items = doc.parentdoc.items.map((d) => d.item).filter(Boolean);
|
|
||||||
|
|
||||||
const baseFilter = { for: ['not in', ['sales']] };
|
|
||||||
if (items.length <= 0) {
|
|
||||||
return baseFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: ['not in', items],
|
|
||||||
...baseFilter,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'description',
|
|
||||||
label: t`Description`,
|
|
||||||
fieldtype: 'Text',
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'description'),
|
|
||||||
hidden: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'quantity',
|
|
||||||
label: t`Quantity`,
|
|
||||||
fieldtype: 'Float',
|
|
||||||
required: 1,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'rate',
|
|
||||||
label: t`Rate`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
required: 1,
|
|
||||||
formula: async (row, doc) => {
|
|
||||||
const baseRate =
|
|
||||||
(await doc.getFrom('Item', row.item, 'rate')) || frappe.pesa(0);
|
|
||||||
return baseRate.div(doc.exchangeRate);
|
|
||||||
},
|
|
||||||
getCurrency: (row, doc) => doc.currency,
|
|
||||||
validate(value) {
|
|
||||||
if (value.gte(0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new frappe.errors.ValidationError(
|
|
||||||
frappe.t`Rate (${frappe.format(
|
|
||||||
value,
|
|
||||||
'Currency'
|
|
||||||
)}) cannot be less zero.`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseRate',
|
|
||||||
label: t`Rate (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (row, doc) => row.rate.mul(doc.exchangeRate),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'account',
|
|
||||||
label: t`Account`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Account',
|
|
||||||
required: 1,
|
|
||||||
readOnly: 1,
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'expenseAccount'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'tax',
|
|
||||||
label: t`Tax`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Tax',
|
|
||||||
formula: (row, doc) => {
|
|
||||||
if (row.tax) return row.tax;
|
|
||||||
return doc.getFrom('Item', row.item, 'tax');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'amount',
|
|
||||||
label: t`Amount`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
readOnly: 1,
|
|
||||||
formula: (row) => row.rate.mul(row.quantity),
|
|
||||||
getCurrency: (row, doc) => doc.currency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseAmount',
|
|
||||||
label: t`Amount (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
readOnly: 1,
|
|
||||||
formula: (row, doc) => row.amount.mul(doc.exchangeRate),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
@ -0,0 +1,3 @@
|
|||||||
|
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
|
||||||
|
|
||||||
|
export class PurchaseInvoiceItem extends InvoiceItem {}
|
@ -1,25 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import PurchaseInvoiceItemOriginal from './PurchaseInvoiceItem';
|
|
||||||
|
|
||||||
export default function getAugmentedPurchaseInvoiceItem({ country }) {
|
|
||||||
const PurchaseInvoiceItem = cloneDeep(PurchaseInvoiceItemOriginal);
|
|
||||||
if (!country) {
|
|
||||||
return PurchaseInvoiceItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (country === 'India') {
|
|
||||||
PurchaseInvoiceItem.fields = [
|
|
||||||
...PurchaseInvoiceItem.fields,
|
|
||||||
{
|
|
||||||
fieldname: 'hsnCode',
|
|
||||||
label: t`HSN/SAC`,
|
|
||||||
fieldtype: 'Int',
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'hsnCode'),
|
|
||||||
formulaDependsOn: ['item'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return PurchaseInvoiceItem;
|
|
||||||
}
|
|
@ -1,152 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
|
|
||||||
import { getActions } from '../Transaction/Transaction';
|
|
||||||
import InvoiceTemplate from './InvoiceTemplate.vue';
|
|
||||||
import SalesInvoice from './SalesInvoiceDocument';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SalesInvoice',
|
|
||||||
label: t`Invoice`,
|
|
||||||
doctype: 'DocType',
|
|
||||||
documentClass: SalesInvoice,
|
|
||||||
printTemplate: InvoiceTemplate,
|
|
||||||
isSingle: 0,
|
|
||||||
isChild: 0,
|
|
||||||
isSubmittable: 1,
|
|
||||||
keywordFields: ['name', 'customer'],
|
|
||||||
settings: 'SalesInvoiceSettings',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: t`Invoice No`,
|
|
||||||
fieldname: 'name',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1,
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'date',
|
|
||||||
label: t`Date`,
|
|
||||||
fieldtype: 'Date',
|
|
||||||
default: () => new Date().toISOString().slice(0, 10),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'customer',
|
|
||||||
label: t`Customer`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Customer',
|
|
||||||
required: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'account',
|
|
||||||
label: t`Account`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Account',
|
|
||||||
disableCreation: true,
|
|
||||||
formula: (doc) => doc.getFrom('Party', doc.customer, 'defaultAccount'),
|
|
||||||
getFilters: () => {
|
|
||||||
return {
|
|
||||||
isGroup: 0,
|
|
||||||
accountType: 'Receivable',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'currency',
|
|
||||||
label: t`Customer Currency`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Currency',
|
|
||||||
formula: (doc) =>
|
|
||||||
doc.getFrom('Party', doc.customer, 'currency') ||
|
|
||||||
frappe.AccountingSettings.currency,
|
|
||||||
formulaDependsOn: ['customer'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'exchangeRate',
|
|
||||||
label: t`Exchange Rate`,
|
|
||||||
fieldtype: 'Float',
|
|
||||||
default: 1,
|
|
||||||
formula: (doc) => doc.getExchangeRate(),
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'items',
|
|
||||||
label: t`Items`,
|
|
||||||
fieldtype: 'Table',
|
|
||||||
childtype: 'SalesInvoiceItem',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'netTotal',
|
|
||||||
label: t`Net Total`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.getSum('items', 'amount', false),
|
|
||||||
readOnly: 1,
|
|
||||||
getCurrency: (doc) => doc.currency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseNetTotal',
|
|
||||||
label: t`Net Total (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.netTotal.mul(doc.exchangeRate),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'taxes',
|
|
||||||
label: t`Taxes`,
|
|
||||||
fieldtype: 'Table',
|
|
||||||
childtype: 'TaxSummary',
|
|
||||||
formula: (doc) => doc.getTaxSummary(),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'grandTotal',
|
|
||||||
label: t`Grand Total`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.getGrandTotal(),
|
|
||||||
readOnly: 1,
|
|
||||||
getCurrency: (doc) => doc.currency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseGrandTotal',
|
|
||||||
label: t`Grand Total (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => doc.grandTotal.mul(doc.exchangeRate),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'outstandingAmount',
|
|
||||||
label: t`Outstanding Amount`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (doc) => {
|
|
||||||
if (doc.submitted) return;
|
|
||||||
return doc.baseGrandTotal;
|
|
||||||
},
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'terms',
|
|
||||||
label: t`Notes`,
|
|
||||||
fieldtype: 'Text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'cancelled',
|
|
||||||
label: t`Cancelled`,
|
|
||||||
fieldtype: 'Check',
|
|
||||||
default: 0,
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'numberSeries',
|
|
||||||
label: t`Number Series`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'NumberSeries',
|
|
||||||
required: 1,
|
|
||||||
getFilters: () => {
|
|
||||||
return { referenceType: 'SalesInvoice' };
|
|
||||||
},
|
|
||||||
default: DEFAULT_NUMBER_SERIES['SalesInvoice'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
actions: getActions('SalesInvoice'),
|
|
||||||
};
|
|
46
models/baseModels/SalesInvoice/SalesInvoice.ts
Normal file
46
models/baseModels/SalesInvoice/SalesInvoice.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { LedgerPosting } from 'accounting/ledgerPosting';
|
||||||
|
import { Action, ListViewSettings } from 'frappe/model/types';
|
||||||
|
import {
|
||||||
|
getTransactionActions,
|
||||||
|
getTransactionStatusColumn,
|
||||||
|
} from '../../helpers';
|
||||||
|
import { Invoice } from '../Invoice/Invoice';
|
||||||
|
import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem';
|
||||||
|
|
||||||
|
export class SalesInvoice extends Invoice {
|
||||||
|
items?: SalesInvoiceItem[];
|
||||||
|
|
||||||
|
async getPosting() {
|
||||||
|
const entries: LedgerPosting = new LedgerPosting({
|
||||||
|
reference: this,
|
||||||
|
party: this.party,
|
||||||
|
});
|
||||||
|
await entries.debit(this.account!, this.baseGrandTotal!);
|
||||||
|
|
||||||
|
for (const item of this.items!) {
|
||||||
|
await entries.credit(item.account!, item.baseAmount!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.taxes) {
|
||||||
|
for (const tax of this.taxes!) {
|
||||||
|
await entries.credit(tax.account!, tax.baseAmount!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.makeRoundOffEntry();
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
static actions: Action[] = getTransactionActions('SalesInvoice');
|
||||||
|
|
||||||
|
static listSettings: ListViewSettings = {
|
||||||
|
formRoute: (name) => `/edit/SalesInvoice/${name}`,
|
||||||
|
columns: [
|
||||||
|
'party',
|
||||||
|
'name',
|
||||||
|
getTransactionStatusColumn(),
|
||||||
|
'date',
|
||||||
|
'grandTotal',
|
||||||
|
'outstandingAmount',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
import TransactionDocument from '../Transaction/TransactionDocument';
|
|
||||||
|
|
||||||
export default class SalesInvoice extends TransactionDocument {};
|
|
@ -1,16 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
import { getStatusColumn } from '../Transaction/Transaction';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
doctype: 'SalesInvoice',
|
|
||||||
title: t`Invoices`,
|
|
||||||
formRoute: (name) => `/edit/SalesInvoice/${name}`,
|
|
||||||
columns: [
|
|
||||||
'customer',
|
|
||||||
'name',
|
|
||||||
getStatusColumn('SalesInvoice'),
|
|
||||||
'date',
|
|
||||||
'grandTotal',
|
|
||||||
'outstandingAmount',
|
|
||||||
],
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
import TransactionServer from '../Transaction/TransactionServer';
|
|
||||||
import SalesInvoice from './SalesInvoiceDocument';
|
|
||||||
import LedgerPosting from '../../../accounting/ledgerPosting';
|
|
||||||
|
|
||||||
class SalesInvoiceServer extends SalesInvoice {
|
|
||||||
async getPosting() {
|
|
||||||
let entries = new LedgerPosting({ reference: this, party: this.customer });
|
|
||||||
await entries.debit(this.account, this.baseGrandTotal);
|
|
||||||
|
|
||||||
for (let item of this.items) {
|
|
||||||
await entries.credit(item.account, item.baseAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.taxes) {
|
|
||||||
for (let tax of this.taxes) {
|
|
||||||
await entries.credit(tax.account, tax.baseAmount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries.makeRoundOffEntry();
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply common methods from TransactionServer
|
|
||||||
Object.assign(SalesInvoiceServer.prototype, TransactionServer);
|
|
||||||
|
|
||||||
export default SalesInvoiceServer;
|
|
@ -1,25 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import SalesInvoiceItemOriginal from './SalesInvoiceItem';
|
|
||||||
|
|
||||||
export default function getAugmentedSalesInvoiceItem({ country }) {
|
|
||||||
const SalesInvoiceItem = cloneDeep(SalesInvoiceItemOriginal);
|
|
||||||
if (!country) {
|
|
||||||
return SalesInvoiceItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (country === 'India') {
|
|
||||||
SalesInvoiceItem.fields = [
|
|
||||||
...SalesInvoiceItem.fields,
|
|
||||||
{
|
|
||||||
fieldname: 'hsnCode',
|
|
||||||
label: t`HSN/SAC`,
|
|
||||||
fieldtype: 'Int',
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'hsnCode'),
|
|
||||||
formulaDependsOn: ['item'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return SalesInvoiceItem;
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
import { t } from 'frappe';
|
|
||||||
export default {
|
|
||||||
name: 'SalesInvoiceItem',
|
|
||||||
doctype: 'DocType',
|
|
||||||
isChild: 1,
|
|
||||||
regional: 1,
|
|
||||||
keywordFields: [],
|
|
||||||
tableFields: ['item', 'tax', 'quantity', 'rate', 'amount'],
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
fieldname: 'item',
|
|
||||||
label: t`Item`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Item',
|
|
||||||
required: 1,
|
|
||||||
getFilters(_, doc) {
|
|
||||||
let items = doc.parentdoc.items.map((d) => d.item).filter(Boolean);
|
|
||||||
|
|
||||||
const baseFilter = { for: ['not in', ['purchases']] };
|
|
||||||
if (items.length <= 0) {
|
|
||||||
return baseFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: ['not in', items],
|
|
||||||
...baseFilter,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'description',
|
|
||||||
label: t`Description`,
|
|
||||||
fieldtype: 'Text',
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'description'),
|
|
||||||
hidden: 1,
|
|
||||||
formulaDependsOn: ['item'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'quantity',
|
|
||||||
label: t`Quantity`,
|
|
||||||
fieldtype: 'Float',
|
|
||||||
required: 1,
|
|
||||||
default: 1,
|
|
||||||
validate(value, doc) {
|
|
||||||
if (value >= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new frappe.errors.ValidationError(
|
|
||||||
frappe.t`Quantity (${value}) cannot be less than zero.`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'rate',
|
|
||||||
label: t`Rate`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
required: 1,
|
|
||||||
formula: async (row, doc) => {
|
|
||||||
const baseRate =
|
|
||||||
(await doc.getFrom('Item', row.item, 'rate')) || frappe.pesa(0);
|
|
||||||
return baseRate.div(doc.exchangeRate);
|
|
||||||
},
|
|
||||||
getCurrency: (row, doc) => doc.currency,
|
|
||||||
formulaDependsOn: ['item'],
|
|
||||||
validate(value, doc) {
|
|
||||||
if (value.gte(0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new frappe.errors.ValidationError(
|
|
||||||
frappe.t`Rate (${frappe.format(
|
|
||||||
value,
|
|
||||||
'Currency'
|
|
||||||
)}) cannot be less zero.`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseRate',
|
|
||||||
label: t`Rate (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
formula: (row, doc) => row.rate.mul(doc.exchangeRate),
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'account',
|
|
||||||
label: t`Account`,
|
|
||||||
hidden: 1,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Account',
|
|
||||||
required: 1,
|
|
||||||
readOnly: 1,
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'incomeAccount'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'tax',
|
|
||||||
label: t`Tax`,
|
|
||||||
fieldtype: 'Link',
|
|
||||||
target: 'Tax',
|
|
||||||
formula: (row, doc) => doc.getFrom('Item', row.item, 'tax'),
|
|
||||||
formulaDependsOn: ['item'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'amount',
|
|
||||||
label: t`Amount`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
readOnly: 1,
|
|
||||||
formula: (row) => row.rate.mul(row.quantity),
|
|
||||||
getCurrency: (row, doc) => doc.currency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'baseAmount',
|
|
||||||
label: t`Amount (Company Currency)`,
|
|
||||||
fieldtype: 'Currency',
|
|
||||||
readOnly: 1,
|
|
||||||
formula: (row, doc) => row.amount.mul(doc.exchangeRate),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
3
models/baseModels/SalesInvoiceItem/SalesInvoiceItem.ts
Normal file
3
models/baseModels/SalesInvoiceItem/SalesInvoiceItem.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
|
||||||
|
|
||||||
|
export class SalesInvoiceItem extends InvoiceItem {}
|
@ -3,6 +3,11 @@ import { FormulaMap } from 'frappe/model/types';
|
|||||||
import Money from 'pesa/dist/types/src/money';
|
import Money from 'pesa/dist/types/src/money';
|
||||||
|
|
||||||
export class TaxSummary extends Doc {
|
export class TaxSummary extends Doc {
|
||||||
|
account?: string;
|
||||||
|
rate?: number;
|
||||||
|
amount?: Money;
|
||||||
|
baseAmount?: Money;
|
||||||
|
|
||||||
formulas: FormulaMap = {
|
formulas: FormulaMap = {
|
||||||
baseAmount: async () => {
|
baseAmount: async () => {
|
||||||
const amount = this.amount as Money;
|
const amount = this.amount as Money;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { openQuickEdit } from '@/utils';
|
import { openQuickEdit } from '@/utils';
|
||||||
import frappe from 'frappe';
|
import frappe from 'frappe';
|
||||||
import Doc from 'frappe/model/doc';
|
import Doc from 'frappe/model/doc';
|
||||||
import { Action } from 'frappe/model/types';
|
import { Action, ColumnConfig } from 'frappe/model/types';
|
||||||
import Money from 'pesa/dist/types/src/money';
|
import Money from 'pesa/dist/types/src/money';
|
||||||
import { Router } from 'vue-router';
|
import { Router } from 'vue-router';
|
||||||
import { InvoiceStatus } from './types';
|
import { InvoiceStatus } from './types';
|
||||||
@ -26,12 +26,12 @@ export function getLedgerLinkAction(): Action {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransactionActions(schemaName: string) {
|
export function getTransactionActions(schemaName: string): Action[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: frappe.t`Make Payment`,
|
label: frappe.t`Make Payment`,
|
||||||
condition: (doc: Doc) =>
|
condition: (doc: Doc) =>
|
||||||
doc.submitted && (doc.outstandingAmount as Money).gt(0),
|
(doc.submitted as boolean) && (doc.outstandingAmount as Money).gt(0),
|
||||||
action: async function makePayment(doc: Doc) {
|
action: async function makePayment(doc: Doc) {
|
||||||
const payment = await frappe.doc.getEmptyDoc('Payment');
|
const payment = await frappe.doc.getEmptyDoc('Payment');
|
||||||
payment.once('afterInsert', async () => {
|
payment.once('afterInsert', async () => {
|
||||||
@ -64,8 +64,8 @@ export function getTransactionActions(schemaName: string) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: frappe.t`Print`,
|
label: frappe.t`Print`,
|
||||||
condition: (doc: Doc) => doc.submitted,
|
condition: (doc: Doc) => doc.submitted as boolean,
|
||||||
action(doc: Doc, router: Router) {
|
action: async (doc: Doc, router: Router) => {
|
||||||
router.push({ path: `/print/${doc.doctype}/${doc.name}` });
|
router.push({ path: `/print/${doc.doctype}/${doc.name}` });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -73,7 +73,7 @@ export function getTransactionActions(schemaName: string) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransactionStatusColumn() {
|
export function getTransactionStatusColumn(): ColumnConfig {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
Unpaid: frappe.t`Unpaid`,
|
Unpaid: frappe.t`Unpaid`,
|
||||||
Paid: frappe.t`Paid`,
|
Paid: frappe.t`Paid`,
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import Account from './doctype/Account/Account.js';
|
|
||||||
import AccountingLedgerEntry from './doctype/AccountingLedgerEntry/AccountingLedgerEntry.js';
|
|
||||||
import AccountingSettings from './doctype/AccountingSettings/AccountingSettings.js';
|
|
||||||
import Address from './doctype/Address/Address.js';
|
|
||||||
import Color from './doctype/Color/Color.js';
|
|
||||||
import CompanySettings from './doctype/CompanySettings/CompanySettings.js';
|
|
||||||
import Contact from './doctype/Contact/Contact.js';
|
|
||||||
import Currency from './doctype/Currency/Currency.js';
|
|
||||||
import GetStarted from './doctype/GetStarted/GetStarted.js';
|
|
||||||
import Item from './doctype/Item/Item.js';
|
|
||||||
import JournalEntry from './doctype/JournalEntry/JournalEntry.js';
|
|
||||||
import JournalEntryAccount from './doctype/JournalEntryAccount/JournalEntryAccount.js';
|
|
||||||
import JournalEntrySettings from './doctype/JournalEntrySettings/JournalEntrySettings.js';
|
|
||||||
import Customer from './doctype/Party/Customer.js';
|
|
||||||
import Party from './doctype/Party/Party.js';
|
|
||||||
import Supplier from './doctype/Party/Supplier.js';
|
|
||||||
import Payment from './doctype/Payment/Payment.js';
|
|
||||||
import PaymentFor from './doctype/PaymentFor/PaymentFor.js';
|
|
||||||
import PaymentSettings from './doctype/PaymentSettings/PaymentSettings.js';
|
|
||||||
import PrintSettings from './doctype/PrintSettings/PrintSettings.js';
|
|
||||||
import PurchaseInvoice from './doctype/PurchaseInvoice/PurchaseInvoice.js';
|
|
||||||
import PurchaseInvoiceItem from './doctype/PurchaseInvoiceItem/PurchaseInvoiceItem.js';
|
|
||||||
import PurchaseInvoiceSettings from './doctype/PurchaseInvoiceSettings/PurchaseInvoiceSettings.js';
|
|
||||||
import SalesInvoice from './doctype/SalesInvoice/SalesInvoice.js';
|
|
||||||
import SalesInvoiceItem from './doctype/SalesInvoiceItem/SalesInvoiceItem.js';
|
|
||||||
import SalesInvoiceSettings from './doctype/SalesInvoiceSettings/SalesInvoiceSettings.js';
|
|
||||||
import SetupWizard from './doctype/SetupWizard/SetupWizard.js';
|
|
||||||
import Tax from './doctype/Tax/Tax.js';
|
|
||||||
import TaxDetail from './doctype/TaxDetail/TaxDetail.js';
|
|
||||||
import TaxSummary from './doctype/TaxSummary/TaxSummary.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
SetupWizard,
|
|
||||||
Currency,
|
|
||||||
Color,
|
|
||||||
Account,
|
|
||||||
AccountingSettings,
|
|
||||||
CompanySettings,
|
|
||||||
AccountingLedgerEntry,
|
|
||||||
Party,
|
|
||||||
Customer,
|
|
||||||
Supplier,
|
|
||||||
Payment,
|
|
||||||
PaymentFor,
|
|
||||||
PaymentSettings,
|
|
||||||
Item,
|
|
||||||
SalesInvoice,
|
|
||||||
SalesInvoiceItem,
|
|
||||||
SalesInvoiceSettings,
|
|
||||||
PurchaseInvoice,
|
|
||||||
PurchaseInvoiceItem,
|
|
||||||
PurchaseInvoiceSettings,
|
|
||||||
Tax,
|
|
||||||
TaxDetail,
|
|
||||||
TaxSummary,
|
|
||||||
Address,
|
|
||||||
Contact,
|
|
||||||
JournalEntry,
|
|
||||||
JournalEntryAccount,
|
|
||||||
JournalEntrySettings,
|
|
||||||
PrintSettings,
|
|
||||||
GetStarted,
|
|
||||||
};
|
|
47
models/index.ts
Normal file
47
models/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Account } from './baseModels/Account/Account';
|
||||||
|
import { AccountingLedgerEntry } from './baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
|
||||||
|
import { AccountingSettings } from './baseModels/AccountingSettings/AccountingSettings';
|
||||||
|
import { Address } from './baseModels/Address/Address';
|
||||||
|
import { Item } from './baseModels/Item/Item';
|
||||||
|
import { JournalEntry } from './baseModels/JournalEntry/JournalEntry';
|
||||||
|
import { JournalEntryAccount } from './baseModels/JournalEntryAccount/JournalEntryAccount';
|
||||||
|
import { Party } from './baseModels/Party/Party';
|
||||||
|
import { Payment } from './baseModels/Payment/Payment';
|
||||||
|
import { PaymentFor } from './baseModels/PaymentFor/PaymentFor';
|
||||||
|
import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice';
|
||||||
|
import { PurchaseInvoiceItem } from './baseModels/PurchaseInvoiceItem/PurchaseInvoiceItem';
|
||||||
|
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
|
||||||
|
import { SalesInvoiceItem } from './baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||||
|
import { SetupWizard } from './baseModels/SetupWizard/SetupWizard';
|
||||||
|
import { Tax } from './baseModels/Tax/Tax';
|
||||||
|
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Account,
|
||||||
|
AccountingLedgerEntry,
|
||||||
|
AccountingSettings,
|
||||||
|
Address,
|
||||||
|
Item,
|
||||||
|
JournalEntry,
|
||||||
|
JournalEntryAccount,
|
||||||
|
Party,
|
||||||
|
Payment,
|
||||||
|
PaymentFor,
|
||||||
|
PurchaseInvoice,
|
||||||
|
PurchaseInvoiceItem,
|
||||||
|
SalesInvoice,
|
||||||
|
SalesInvoiceItem,
|
||||||
|
SetupWizard,
|
||||||
|
Tax,
|
||||||
|
TaxSummary,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getRegionalModels(countryCode: string) {
|
||||||
|
if (countryCode !== 'in') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Address } = await import('./regionalModels/in/Address');
|
||||||
|
const { Party } = await import('./regionalModels/in/Party');
|
||||||
|
return { Address, Party };
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import frappe from 'frappe';
|
|
||||||
|
|
||||||
async function setAugmentedModel(model, regionalInfo) {
|
|
||||||
const getAugmentedModel = (
|
|
||||||
await import('./doctype/' + model + '/RegionalChanges')
|
|
||||||
).default;
|
|
||||||
const augmentedModel = getAugmentedModel(regionalInfo);
|
|
||||||
frappe.models[model] = augmentedModel;
|
|
||||||
frappe.models[model].augmented = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function regionalModelUpdates(regionalInfo) {
|
|
||||||
for (let model in frappe.models) {
|
|
||||||
const { regional, basedOn, augmented } = frappe.models[model];
|
|
||||||
if (!regional || basedOn || augmented) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await setAugmentedModel(model, regionalInfo);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +1 @@
|
|||||||
export enum DoctypeName {
|
|
||||||
SetupWizard = 'SetupWizard',
|
|
||||||
Currency = 'Currency',
|
|
||||||
Color = 'Color',
|
|
||||||
Account = 'Account',
|
|
||||||
AccountingSettings = 'AccountingSettings',
|
|
||||||
CompanySettings = 'CompanySettings',
|
|
||||||
AccountingLedgerEntry = 'AccountingLedgerEntry',
|
|
||||||
Party = 'Party',
|
|
||||||
Customer = 'Customer',
|
|
||||||
Supplier = 'Supplier',
|
|
||||||
Payment = 'Payment',
|
|
||||||
PaymentFor = 'PaymentFor',
|
|
||||||
PaymentSettings = 'PaymentSettings',
|
|
||||||
Item = 'Item',
|
|
||||||
SalesInvoice = 'SalesInvoice',
|
|
||||||
SalesInvoiceItem = 'SalesInvoiceItem',
|
|
||||||
SalesInvoiceSettings = 'SalesInvoiceSettings',
|
|
||||||
PurchaseInvoice = 'PurchaseInvoice',
|
|
||||||
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
|
|
||||||
PurchaseInvoiceSettings = 'PurchaseInvoiceSettings',
|
|
||||||
Tax = 'Tax',
|
|
||||||
TaxDetail = 'TaxDetail',
|
|
||||||
TaxSummary = 'TaxSummary',
|
|
||||||
Address = 'Address',
|
|
||||||
Contact = 'Contact',
|
|
||||||
JournalEntry = 'JournalEntry',
|
|
||||||
JournalEntryAccount = 'JournalEntryAccount',
|
|
||||||
JournalEntrySettings = 'JournalEntrySettings',
|
|
||||||
Quotation = 'Quotation',
|
|
||||||
QuotationItem = 'QuotationItem',
|
|
||||||
QuotationSettings = 'QuotationSettings',
|
|
||||||
SalesOrder = 'SalesOrder',
|
|
||||||
SalesOrderItem = 'SalesOrderItem',
|
|
||||||
SalesOrderSettings = 'SalesOrderSettings',
|
|
||||||
Fulfillment = 'Fulfillment',
|
|
||||||
FulfillmentItem = 'FulfillmentItem',
|
|
||||||
FulfillmentSettings = 'FulfillmentSettings',
|
|
||||||
PurchaseOrder = 'PurchaseOrder',
|
|
||||||
PurchaseOrderItem = 'PurchaseOrderItem',
|
|
||||||
PurchaseOrderSettings = 'PurchaseOrderSettings',
|
|
||||||
PurchaseReceipt = 'PurchaseReceipt',
|
|
||||||
PurchaseReceiptItem = 'PurchaseReceiptItem',
|
|
||||||
PurchaseReceiptSettings = 'PurchaseReceiptSettings',
|
|
||||||
Event = 'Event',
|
|
||||||
EventSchedule = 'EventSchedule',
|
|
||||||
EventSettings = 'EventSettings',
|
|
||||||
Email = 'Email',
|
|
||||||
EmailAccount = 'EmailAccount',
|
|
||||||
PrintSettings = 'PrintSettings',
|
|
||||||
GetStarted = 'GetStarted',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InvoiceStatus = 'Draft' | 'Unpaid' | 'Cancelled' | 'Paid';
|
export type InvoiceStatus = 'Draft' | 'Unpaid' | 'Cancelled' | 'Paid';
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
"name": "PurchaseInvoiceItem",
|
"name": "PurchaseInvoiceItem",
|
||||||
"label": "Purchase Invoice Item",
|
"label": "Purchase Invoice Item",
|
||||||
"isChild": true,
|
"isChild": true,
|
||||||
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
|
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "item",
|
"fieldname": "item",
|
||||||
@ -64,6 +63,13 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"computed": true,
|
"computed": true,
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "hsnCode",
|
||||||
|
"label": "HSN/SAC",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"placeholder": "HSN/SAC Code"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"tableFields": ["item", "tax", "quantity", "rate", "amount"]
|
||||||
|
}
|
||||||
|
@ -64,7 +64,13 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"computed": true,
|
"computed": true,
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "hsnCode",
|
||||||
|
"label": "HSN/SAC",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"placeholder": "HSN/SAC Code"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tableFields": ["item", "tax", "quantity", "rate", "amount"]
|
"tableFields": ["item", "tax", "quantity", "rate", "amount"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user