2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

refactor: Item and Address models to ts Doc subcls

This commit is contained in:
18alantom 2022-04-11 18:14:36 +05:30
parent cb54100db4
commit 781c1d70e9
18 changed files with 258 additions and 412 deletions

View File

@ -1,49 +1,10 @@
import { showMessageDialog } from '@/utils';
import frappe, { t } from 'frappe';
import { DateTime } from 'luxon';
import { stateCodeMap } from '../regional/in';
import { exportCsv, saveExportData } from '../reports/commonExporter';
import { getSavePath } from '../src/utils';
// prettier-ignore
export const stateCodeMap = {
'JAMMU AND KASHMIR': '1',
'HIMACHAL PRADESH': '2',
'PUNJAB': '3',
'CHANDIGARH': '4',
'UTTARAKHAND': '5',
'HARYANA': '6',
'DELHI': '7',
'RAJASTHAN': '8',
'UTTAR PRADESH': '9',
'BIHAR': '10',
'SIKKIM': '11',
'ARUNACHAL PRADESH': '12',
'NAGALAND': '13',
'MANIPUR': '14',
'MIZORAM': '15',
'TRIPURA': '16',
'MEGHALAYA': '17',
'ASSAM': '18',
'WEST BENGAL': '19',
'JHARKHAND': '20',
'ODISHA': '21',
'CHATTISGARH': '22',
'MADHYA PRADESH': '23',
'GUJARAT': '24',
'DADRA AND NAGAR HAVELI AND DAMAN AND DIU': '26',
'MAHARASHTRA': '27',
'KARNATAKA': '29',
'GOA': '30',
'LAKSHADWEEP': '31',
'KERALA': '32',
'TAMIL NADU': '33',
'PUDUCHERRY': '34',
'ANDAMAN AND NICOBAR ISLANDS': '35',
'TELANGANA': '36',
'ANDHRA PRADESH': '37',
'LADAKH': '38',
};
const GST = {
'GST-0': 0,
'GST-0.25': 0.25,

View File

@ -27,8 +27,10 @@ import {
} from './helpers';
import { setName } from './naming';
import {
Action,
DefaultMap,
DependsOnMap,
EmptyMessageMap,
FiltersMap,
FormulaMap,
ListsMap,
@ -504,7 +506,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
async getValueFromFormula(field: Field, doc: Doc) {
let value: Doc[] | DocValue;
let value: Doc[] | DocValue | undefined;
const formula = doc.formulas[field.fieldtype];
if (formula === undefined) {
@ -704,6 +706,9 @@ export default class Doc extends Observable<DocValue | Doc[]> {
static lists: ListsMap = {};
static filters: FiltersMap = {};
static emptyMessages: EmptyMessageMap = {};
static listSettings: ListViewSettings = {};
static treeSettings?: TreeViewSettings;
static actions: Action[] = [];
}

View File

@ -1,6 +1,7 @@
import { DocValue } from 'frappe/core/types';
import { FieldType } from 'schemas/types';
import { QueryFilter } from 'utils/db/types';
import { Router } from 'vue-router';
import Doc from './doc';
/**
@ -39,9 +40,18 @@ export type DocMap = Record<string, Doc | undefined>;
export type FilterFunction = (doc: Doc) => QueryFilter;
export type FiltersMap = Record<string, FilterFunction>;
export type ListFunction = () => string[];
export type EmptyMessageFunction = (doc: Doc) => string;
export type EmptyMessageMap = Record<string, EmptyMessageFunction>;
export type ListFunction = (doc?: Doc) => string[];
export type ListsMap = Record<string, ListFunction>;
export interface Action {
label: string;
condition: (doc: Doc) => boolean;
action: (doc: Doc, router: Router) => Promise<void>;
}
export interface ColumnConfig {
label: string;
fieldtype: FieldType;

View File

@ -1,121 +0,0 @@
import { t } from 'frappe';
import { stateCodeMap } from '../../../accounting/gst';
import countryList from '../../../fixtures/countryInfo.json';
import { titleCase } from '../../../src/utils';
function getStates(doc) {
switch (doc.country) {
case 'India':
return Object.keys(stateCodeMap).map(titleCase).sort();
default:
return [];
}
}
export default {
name: 'Address',
doctype: 'DocType',
regional: 1,
isSingle: 0,
keywordFields: [
'addressLine1',
'addressLine2',
'city',
'state',
'country',
'postalCode',
],
fields: [
{
fieldname: 'addressLine1',
label: t`Address Line 1`,
placeholder: t`Address Line 1`,
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'addressLine2',
label: t`Address Line 2`,
placeholder: t`Address Line 2`,
fieldtype: 'Data',
},
{
fieldname: 'city',
label: t`City / Town`,
placeholder: t`City / Town`,
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'state',
label: t`State`,
placeholder: t`State`,
fieldtype: 'AutoComplete',
emptyMessage: (doc) => {
if (doc.country) {
return 'Enter State';
}
return 'Enter Country to load States';
},
getList: getStates,
},
{
fieldname: 'country',
label: t`Country`,
placeholder: t`Country`,
fieldtype: 'AutoComplete',
getList: () => Object.keys(countryList).sort(),
required: 1,
},
{
fieldname: 'postalCode',
label: t`Postal Code`,
placeholder: t`Postal Code`,
fieldtype: 'Data',
},
{
fieldname: 'emailAddress',
label: t`Email Address`,
placeholder: t`Email Address`,
fieldtype: 'Data',
},
{
fieldname: 'phone',
label: t`Phone`,
placeholder: t`Phone`,
fieldtype: 'Data',
},
{
fieldname: 'fax',
label: t`Fax`,
fieldtype: 'Data',
},
{
fieldname: 'addressDisplay',
fieldtype: 'Text',
label: t`Address Display`,
readOnly: true,
formula: (doc) => {
return [
doc.addressLine1,
doc.addressLine2,
doc.city,
doc.state,
doc.country,
doc.postalCode,
]
.filter(Boolean)
.join(', ');
},
},
],
quickEditFields: [
'addressLine1',
'addressLine2',
'city',
'state',
'country',
'postalCode',
],
inlineEditDisplayField: 'addressDisplay',
};

View File

@ -0,0 +1,48 @@
import frappe from 'frappe';
import Doc from 'frappe/model/doc';
import { EmptyMessageMap, FormulaMap, ListsMap } from 'frappe/model/types';
import { stateCodeMap } from 'regional/in';
import { titleCase } from 'utils';
import countryInfo from '../../../fixtures/countryInfo.json';
export class Address extends Doc {
formulas: FormulaMap = {
addressDisplay: async () => {
return [
this.addressLine1,
this.addressLine2,
this.city,
this.state,
this.country,
this.postalCode,
]
.filter(Boolean)
.join(', ');
},
};
static lists: ListsMap = {
state(doc?: Doc) {
const country = doc?.country as string | undefined;
switch (country) {
case 'India':
return Object.keys(stateCodeMap).map(titleCase).sort();
default:
return [] as string[];
}
},
country() {
return Object.keys(countryInfo).sort();
},
};
static emptyMessages: EmptyMessageMap = {
state: (doc: Doc) => {
if (doc.country) {
return frappe.t`Enter State`;
}
return frappe.t`Enter Country to load States`;
},
};
}

View File

@ -1,31 +0,0 @@
import { t } from 'frappe';
import { cloneDeep } from 'lodash';
import { stateCodeMap } from '../../../accounting/gst';
import { titleCase } from '../../../src/utils';
import AddressOriginal from './Address';
export default function getAugmentedAddress({ country }) {
const Address = cloneDeep(AddressOriginal);
if (!country) {
return Address;
}
const stateList = Object.keys(stateCodeMap).map(titleCase).sort();
if (country === 'India') {
Address.fields = [
...Address.fields,
{
fieldname: 'pos',
label: t`Place of Supply`,
fieldtype: 'AutoComplete',
placeholder: t`Place of Supply`,
formula: (doc) => (stateList.includes(doc.state) ? doc.state : ''),
getList: () => stateList,
},
];
Address.quickEditFields = [...Address.quickEditFields, 'pos'];
}
return Address;
}

View File

@ -1,168 +0,0 @@
import frappe, { t } from 'frappe';
const itemForMap = {
purchases: t`Purchases`,
sales: t`Sales`,
both: t`Both`,
};
export default {
name: 'Item',
label: t`Item`,
doctype: 'DocType',
isSingle: 0,
regional: 1,
keywordFields: ['name', 'description'],
fields: [
{
fieldname: 'name',
label: t`Item Name`,
fieldtype: 'Data',
placeholder: t`Item Name`,
required: 1,
},
{
fieldname: 'image',
label: t`Image`,
fieldtype: 'AttachImage',
},
{
fieldname: 'description',
label: t`Description`,
placeholder: t`Item Description`,
fieldtype: 'Text',
},
{
fieldname: 'unit',
label: t`Unit Type`,
fieldtype: 'Select',
placeholder: t`Unit Type`,
default: 'Unit',
options: ['Unit', 'Kg', 'Gram', 'Hour', 'Day'],
},
{
fieldname: 'itemType',
label: t`Type`,
placeholder: t`Type`,
fieldtype: 'Select',
default: 'Product',
options: ['Product', 'Service'],
},
{
fieldname: 'for',
label: t`For`,
fieldtype: 'Select',
options: Object.keys(itemForMap),
map: itemForMap,
default: 'both',
},
{
fieldname: 'incomeAccount',
label: t`Income`,
fieldtype: 'Link',
target: 'Account',
placeholder: t`Income`,
required: 1,
disableCreation: true,
getFilters: () => {
return {
isGroup: 0,
rootType: 'Income',
};
},
formulaDependsOn: ['itemType'],
async formula(doc) {
let accountName = 'Service';
if (doc.itemType === 'Product') {
accountName = 'Sales';
}
const accountExists = await frappe.db.exists('Account', accountName);
return accountExists ? accountName : '';
},
},
{
fieldname: 'expenseAccount',
label: t`Expense`,
fieldtype: 'Link',
target: 'Account',
placeholder: t`Expense`,
required: 1,
disableCreation: true,
getFilters: () => {
return {
isGroup: 0,
rootType: 'Expense',
};
},
formulaDependsOn: ['itemType'],
async formula() {
const cogs = await frappe.db.getAllRaw('Account', {
filters: {
accountType: 'Cost of Goods Sold',
},
});
if (cogs.length === 0) {
return '';
} else {
return cogs[0].name;
}
},
},
{
fieldname: 'tax',
label: t`Tax`,
fieldtype: 'Link',
target: 'Tax',
placeholder: t`Tax`,
},
{
fieldname: 'rate',
label: t`Rate`,
fieldtype: 'Currency',
validate(value) {
if (value.isNegative()) {
throw new frappe.errors.ValidationError(t`Rate can't be negative.`);
}
},
},
],
quickEditFields: [
'rate',
'unit',
'itemType',
'for',
'tax',
'description',
'incomeAccount',
'expenseAccount',
],
actions: [
{
label: t`New Invoice`,
condition: (doc) => !doc.isNew(),
action: async (doc, router) => {
const invoice = await frappe.getEmptyDoc('SalesInvoice');
invoice.append('items', {
item: doc.name,
rate: doc.rate,
tax: doc.tax,
});
router.push(`/edit/SalesInvoice/${invoice.name}`);
},
},
{
label: t`New Bill`,
condition: (doc) => !doc.isNew(),
action: async (doc, router) => {
const invoice = await frappe.getEmptyDoc('PurchaseInvoice');
invoice.append('items', {
item: doc.name,
rate: doc.rate,
tax: doc.tax,
});
router.push(`/edit/PurchaseInvoice/${invoice.name}`);
},
},
],
};

View File

@ -0,0 +1,98 @@
import frappe from 'frappe';
import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc';
import {
Action,
DependsOnMap,
FiltersMap,
FormulaMap,
ListViewSettings,
ValidationMap,
} from 'frappe/model/types';
import Money from 'pesa/dist/types/src/money';
export class Item extends Doc {
formulas: FormulaMap = {
incomeAccount: async () => {
let accountName = 'Service';
if (this.itemType === 'Product') {
accountName = 'Sales';
}
const accountExists = await frappe.db.exists('Account', accountName);
return accountExists ? accountName : '';
},
expenseAccount: async () => {
const cogs = await frappe.db.getAllRaw('Account', {
filters: {
accountType: 'Cost of Goods Sold',
},
});
if (cogs.length === 0) {
return '';
} else {
return cogs[0].name as string;
}
},
};
static filters: FiltersMap = {
incomeAccount: () => ({
isGroup: false,
rootType: 'Income',
}),
expenseAccount: () => ({
isGroup: false,
rootType: 'Expense',
}),
};
dependsOn: DependsOnMap = {
incomeAccount: ['itemType'],
expenseAccount: ['itemType'],
};
validations: ValidationMap = {
rate: async (value: DocValue) => {
if ((value as Money).isNegative()) {
throw new frappe.errors.ValidationError(
frappe.t`Rate can't be negative.`
);
}
},
};
actions: Action[] = [
{
label: frappe.t`New Invoice`,
condition: (doc) => !doc.isNew,
action: async (doc, router) => {
const invoice = await frappe.doc.getEmptyDoc('SalesInvoice');
invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/SalesInvoice/${invoice.name}`);
},
},
{
label: frappe.t`New Bill`,
condition: (doc) => !doc.isNew,
action: async (doc, router) => {
const invoice = await frappe.doc.getEmptyDoc('PurchaseInvoice');
invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/PurchaseInvoice/${invoice.name}`);
},
},
];
listSettings: ListViewSettings = {
columns: ['name', 'unit', 'tax', 'rate'],
};
}

View File

@ -1,7 +0,0 @@
import { t } from 'frappe';
export default {
doctype: 'Item',
title: t`Items`,
columns: ['name', 'unit', 'tax', 'rate'],
};

View File

@ -1,29 +0,0 @@
import { t } from 'frappe';
import { cloneDeep } from 'lodash';
import ItemOriginal from './Item';
export default function getAugmentedItem({ country }) {
const Item = cloneDeep(ItemOriginal);
if (!country) {
return Item;
}
if (country === 'India') {
const nameFieldIndex = Item.fields.findIndex((i) => i.fieldname === 'name');
Item.fields = [
...Item.fields.slice(0, nameFieldIndex + 1),
{
fieldname: 'hsnCode',
label: t`HSN/SAC`,
fieldtype: 'Int',
placeholder: t`HSN/SAC Code`,
},
...Item.fields.slice(nameFieldIndex + 1, Item.fields.length),
];
Item.quickEditFields.unshift('hsnCode');
}
return Item;
}

View File

@ -0,0 +1,37 @@
import { FormulaMap, ListsMap } from 'frappe/model/types';
import { Address as BaseAddress } from 'models/baseModels/Address/Address';
import { stateCodeMap } from 'regional/in';
import { titleCase } from 'utils';
export class Address extends BaseAddress {
formulas: FormulaMap = {
addressDisplay: async () => {
return [
this.addressLine1,
this.addressLine2,
this.city,
this.state,
this.country,
this.postalCode,
]
.filter(Boolean)
.join(', ');
},
pos: async () => {
const stateList = Object.keys(stateCodeMap).map(titleCase).sort();
const state = this.state as string;
if (stateList.includes(state)) {
return state;
}
return '';
},
};
static lists: ListsMap = {
...BaseAddress.lists,
pos: () => {
return Object.keys(stateCodeMap).map(titleCase).sort();
},
};
}

39
regional/in.ts Normal file
View File

@ -0,0 +1,39 @@
// prettier-ignore
export const stateCodeMap = {
'JAMMU AND KASHMIR': '1',
'HIMACHAL PRADESH': '2',
'PUNJAB': '3',
'CHANDIGARH': '4',
'UTTARAKHAND': '5',
'HARYANA': '6',
'DELHI': '7',
'RAJASTHAN': '8',
'UTTAR PRADESH': '9',
'BIHAR': '10',
'SIKKIM': '11',
'ARUNACHAL PRADESH': '12',
'NAGALAND': '13',
'MANIPUR': '14',
'MIZORAM': '15',
'TRIPURA': '16',
'MEGHALAYA': '17',
'ASSAM': '18',
'WEST BENGAL': '19',
'JHARKHAND': '20',
'ODISHA': '21',
'CHATTISGARH': '22',
'MADHYA PRADESH': '23',
'GUJARAT': '24',
'DADRA AND NAGAR HAVELI AND DAMAN AND DIU': '26',
'MAHARASHTRA': '27',
'KARNATAKA': '29',
'GOA': '30',
'LAKSHADWEEP': '31',
'KERALA': '32',
'TAMIL NADU': '33',
'PUDUCHERRY': '34',
'ANDAMAN AND NICOBAR ISLANDS': '35',
'TELANGANA': '36',
'ANDHRA PRADESH': '37',
'LADAKH': '38',
};

View File

@ -83,5 +83,6 @@
"state",
"country",
"postalCode"
]
],
"inlineEditDisplayField": "addressDisplay"
}

View File

@ -124,6 +124,7 @@ export interface Schema {
keywordFields?: string[]; // Used to get fields that are to be used for search.
quickEditFields?: string[]; // Used to get fields for the quickEditForm
treeSettings?: TreeSettings; // Used to determine root nodes
inlineEditDisplayField?:string,// Display field if inline editable
naming?: Naming; // Used for assigning name, default is 'random' else 'numberSeries' if present
}

View File

@ -423,19 +423,6 @@ export function showToast(props) {
replaceAndAppendMount(toast, 'toast-target');
}
export function titleCase(phrase) {
return phrase
.split(' ')
.map((word) => {
const wordLower = word.toLowerCase();
if (['and', 'an', 'a', 'from', 'by', 'on'].includes(wordLower)) {
return wordLower;
}
return wordLower[0].toUpperCase() + wordLower.slice(1);
})
.join(' ');
}
export async function getIsSetupComplete() {
try {
const { setupComplete } = await frappe.getSingle('AccountingSettings');

View File

@ -18,6 +18,7 @@
"@/*": ["src/*"],
"schemas/*": ["schemas/*"],
"backend/*": ["backend/*"],
"regional/*": ["regional/*"],
"utils/*": ["utils/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]

View File

@ -55,3 +55,16 @@ export function getListFromMap<T>(map: Record<string, T>): T[] {
export function getIsNullOrUndef(value: unknown): boolean {
return value === null || value === undefined;
}
export function titleCase(phrase: string): string {
return phrase
.split(' ')
.map((word) => {
const wordLower = word.toLowerCase();
if (['and', 'an', 'a', 'from', 'by', 'on'].includes(wordLower)) {
return wordLower;
}
return wordLower[0].toUpperCase() + wordLower.slice(1);
})
.join(' ');
}

View File

@ -44,6 +44,7 @@ module.exports = {
schemas: path.resolve(__dirname, './schemas'),
backend: path.resolve(__dirname, './backend'),
utils: path.resolve(__dirname, './utils'),
regional: path.resolve(__dirname, './regional'),
});
config.plugins.push(