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

incr: complete typing models

- fix model exports
- add a README for models base
This commit is contained in:
18alantom 2022-04-14 14:52:45 +05:30
parent 91bf6e03fa
commit 591e7b3163
37 changed files with 389 additions and 845 deletions

View File

@ -33,6 +33,7 @@ import {
EmptyMessageMap,
FiltersMap,
FormulaMap,
FormulaReturn,
HiddenMap,
ListsMap,
ListViewSettings,
@ -507,7 +508,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
async getValueFromFormula(field: Field, doc: Doc) {
let value: Doc[] | DocValue | undefined;
let value: FormulaReturn;
const formula = doc.formulas[field.fieldtype];
if (formula === undefined) {
@ -515,7 +516,6 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
value = await formula();
if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) {
value = value.map((row) => this._initChild(row, field.fieldname));
}

View File

@ -1,4 +1,4 @@
import { DocValue } from 'frappe/core/types';
import { DocValue, DocValueMap } from 'frappe/core/types';
import { FieldType } from 'schemas/types';
import { QueryFilter } from 'utils/db/types';
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.
* - `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 Validation = (value: DocValue) => Promise<void>;
export type Required = () => boolean;

32
models/README.md Normal file
View 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`.

View File

@ -7,7 +7,7 @@ import {
} from 'frappe/model/types';
import { QueryFilter } from 'utils/db/types';
export default class Account extends Doc {
export class Account extends Doc {
async beforeInsert() {
if (this.accountType || !this.parentAccount) {
return;

View File

@ -1,21 +1,36 @@
import { LedgerPosting } from 'accounting/ledgerPosting';
import frappe from 'frappe';
import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc';
import { DefaultMap, FiltersMap, FormulaMap } from 'frappe/model/types';
import Money from 'pesa/dist/types/src/money';
import { getExchangeRate } from '../../../accounting/exchangeRate';
import { Party } from '../Party/Party';
import { Payment } from '../Payment/Payment';
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?: 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() {
const payments = await frappe.db.getAll('PaymentFor', {
fields: ['parent'],
filters: { referenceName: this.name as string },
filters: { referenceName: this.name! },
orderBy: 'name',
});
@ -43,13 +58,10 @@ export abstract class Transaction extends Doc {
// update outstanding amounts
await frappe.db.update(this.schemaName, {
name: this.name as string,
outstandingAmount: this.baseGrandTotal as Money,
outstandingAmount: this.baseGrandTotal!,
});
const party = (await frappe.doc.getDoc(
'Party',
this.party as string
)) as Party;
const party = (await frappe.doc.getDoc('Party', this.party!)) as Party;
await party.updateOutstandingAmount();
}
@ -87,7 +99,7 @@ export abstract class Transaction extends Doc {
return 1.0;
}
return await getExchangeRate({
fromCurrency: this.currency as string,
fromCurrency: this.currency!,
toCurrency: companyCurrency as string,
});
}
@ -95,7 +107,13 @@ export abstract class Transaction extends Doc {
async getTaxSummary() {
const taxes: Record<
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[]) {
@ -112,6 +130,7 @@ export abstract class Transaction extends Doc {
account,
rate,
amount: frappe.pesa(0),
baseAmount: frappe.pesa(0),
};
const amount = (row.amount as Money).mul(rate).div(100);
@ -122,7 +141,7 @@ export abstract class Transaction extends Doc {
return Object.keys(taxes)
.map((account) => {
const tax = taxes[account];
tax.baseAmount = tax.amount.mul(this.exchangeRate as number);
tax.baseAmount = tax.amount.mul(this.exchangeRate!);
return tax;
})
.filter((tax) => !tax.amount.isZero());
@ -139,6 +158,40 @@ export abstract class Transaction extends Doc {
async getGrandTotal() {
return ((this.taxes ?? []) as Doc[])
.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 }),
};
}

View 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,
};
},
};
}

View File

@ -20,6 +20,8 @@ import { Party } from '../Party/Party';
import { PaymentMethod, PaymentType } from './types';
export class Payment extends Doc {
party?: string;
async change({ changed }: { changed: string }) {
switch (changed) {
case 'for': {
@ -59,7 +61,7 @@ export class Payment extends Doc {
paymentType = 'Pay';
}
this.party = party;
this.party = party as string;
this.paymentType = paymentType;
}
@ -152,7 +154,7 @@ export class Payment extends Doc {
const writeoff = this.writeoff as Money;
const entries = new LedgerPosting({
reference: this,
party: this.party as string,
party: this.party!,
});
await entries.debit(paymentAccount as string, amount.sub(writeoff));
@ -164,7 +166,7 @@ export class Payment extends Doc {
const writeoffEntry = new LedgerPosting({
reference: this,
party: this.party as string,
party: this.party!,
});
const writeOffAccount = frappe.singles.AccountingSettings!
.writeOffAccount as string;
@ -227,10 +229,7 @@ export class Payment extends Doc {
const newOutstanding = outstandingAmount.sub(amount);
await referenceDoc.set('outstandingAmount', newOutstanding);
await referenceDoc.update();
const party = (await frappe.doc.getDoc(
'Party',
this.party as string
)) as Party;
const party = (await frappe.doc.getDoc('Party', this.party!)) as Party;
await party.updateOutstandingAmount();
}

View File

@ -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'),
};

View File

@ -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',
],
};
}

View File

@ -1,3 +0,0 @@
import TransactionDocument from '../Transaction/TransactionDocument';
export default class PurchaseInvoice extends TransactionDocument {};

View File

@ -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',
],
};

View File

@ -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;

View File

@ -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),
},
],
};

View File

@ -0,0 +1,3 @@
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
export class PurchaseInvoiceItem extends InvoiceItem {}

View File

@ -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;
}

View File

@ -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'),
};

View 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',
],
};
}

View File

@ -1,3 +0,0 @@
import TransactionDocument from '../Transaction/TransactionDocument';
export default class SalesInvoice extends TransactionDocument {};

View File

@ -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',
],
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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),
},
],
};

View File

@ -0,0 +1,3 @@
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
export class SalesInvoiceItem extends InvoiceItem {}

View File

@ -3,6 +3,11 @@ import { FormulaMap } from 'frappe/model/types';
import Money from 'pesa/dist/types/src/money';
export class TaxSummary extends Doc {
account?: string;
rate?: number;
amount?: Money;
baseAmount?: Money;
formulas: FormulaMap = {
baseAmount: async () => {
const amount = this.amount as Money;

View File

@ -1,7 +1,7 @@
import { openQuickEdit } from '@/utils';
import frappe from 'frappe';
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 { Router } from 'vue-router';
import { InvoiceStatus } from './types';
@ -26,12 +26,12 @@ export function getLedgerLinkAction(): Action {
};
}
export function getTransactionActions(schemaName: string) {
export function getTransactionActions(schemaName: string): Action[] {
return [
{
label: frappe.t`Make Payment`,
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) {
const payment = await frappe.doc.getEmptyDoc('Payment');
payment.once('afterInsert', async () => {
@ -64,8 +64,8 @@ export function getTransactionActions(schemaName: string) {
},
{
label: frappe.t`Print`,
condition: (doc: Doc) => doc.submitted,
action(doc: Doc, router: Router) {
condition: (doc: Doc) => doc.submitted as boolean,
action: async (doc: Doc, router: Router) => {
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 = {
Unpaid: frappe.t`Unpaid`,
Paid: frappe.t`Paid`,

View File

@ -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
View 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 };
}

View File

@ -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);
}
}

View File

@ -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';

View File

@ -2,7 +2,6 @@
"name": "PurchaseInvoiceItem",
"label": "Purchase Invoice Item",
"isChild": true,
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
"fields": [
{
"fieldname": "item",
@ -64,6 +63,13 @@
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
}
]
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"]
}

View File

@ -64,7 +64,13 @@
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"]
}
}