diff --git a/accounting/gst.js b/accounting/gst.js index 2bb19bbf..3635ae5d 100644 --- a/accounting/gst.js +++ b/accounting/gst.js @@ -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, diff --git a/frappe/model/doc.ts b/frappe/model/doc.ts index 1ffd3e63..6f43dd4d 100644 --- a/frappe/model/doc.ts +++ b/frappe/model/doc.ts @@ -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 { } 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 { static lists: ListsMap = {}; static filters: FiltersMap = {}; + static emptyMessages: EmptyMessageMap = {}; static listSettings: ListViewSettings = {}; static treeSettings?: TreeViewSettings; + + static actions: Action[] = []; } diff --git a/frappe/model/types.ts b/frappe/model/types.ts index b86c06db..07f5a680 100644 --- a/frappe/model/types.ts +++ b/frappe/model/types.ts @@ -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; export type FilterFunction = (doc: Doc) => QueryFilter; export type FiltersMap = Record; -export type ListFunction = () => string[]; +export type EmptyMessageFunction = (doc: Doc) => string; +export type EmptyMessageMap = Record; + +export type ListFunction = (doc?: Doc) => string[]; export type ListsMap = Record; +export interface Action { + label: string; + condition: (doc: Doc) => boolean; + action: (doc: Doc, router: Router) => Promise; +} + export interface ColumnConfig { label: string; fieldtype: FieldType; diff --git a/models/baseModels/Address/Address.js b/models/baseModels/Address/Address.js deleted file mode 100644 index c768c07e..00000000 --- a/models/baseModels/Address/Address.js +++ /dev/null @@ -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', -}; diff --git a/models/baseModels/Address/Address.ts b/models/baseModels/Address/Address.ts new file mode 100644 index 00000000..2acef2bf --- /dev/null +++ b/models/baseModels/Address/Address.ts @@ -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`; + }, + }; +} diff --git a/models/baseModels/Address/RegionalChanges.js b/models/baseModels/Address/RegionalChanges.js deleted file mode 100644 index cae759cb..00000000 --- a/models/baseModels/Address/RegionalChanges.js +++ /dev/null @@ -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; -} diff --git a/models/baseModels/Item/Item.js b/models/baseModels/Item/Item.js deleted file mode 100644 index 7833e2cf..00000000 --- a/models/baseModels/Item/Item.js +++ /dev/null @@ -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}`); - }, - }, - ], -}; diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts new file mode 100644 index 00000000..69bf875c --- /dev/null +++ b/models/baseModels/Item/Item.ts @@ -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'], + }; +} diff --git a/models/baseModels/Item/ItemList.js b/models/baseModels/Item/ItemList.js deleted file mode 100644 index 84981bf9..00000000 --- a/models/baseModels/Item/ItemList.js +++ /dev/null @@ -1,7 +0,0 @@ -import { t } from 'frappe'; - -export default { - doctype: 'Item', - title: t`Items`, - columns: ['name', 'unit', 'tax', 'rate'], -}; diff --git a/models/baseModels/Item/RegionalChanges.js b/models/baseModels/Item/RegionalChanges.js deleted file mode 100644 index 7b88b41d..00000000 --- a/models/baseModels/Item/RegionalChanges.js +++ /dev/null @@ -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; -} diff --git a/models/regionalModels/in/Address.ts b/models/regionalModels/in/Address.ts new file mode 100644 index 00000000..5b554654 --- /dev/null +++ b/models/regionalModels/in/Address.ts @@ -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(); + }, + }; +} diff --git a/regional/in.ts b/regional/in.ts new file mode 100644 index 00000000..96cf866b --- /dev/null +++ b/regional/in.ts @@ -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', +}; diff --git a/schemas/app/Address.json b/schemas/app/Address.json index 12fd1071..f41758f4 100644 --- a/schemas/app/Address.json +++ b/schemas/app/Address.json @@ -83,5 +83,6 @@ "state", "country", "postalCode" - ] + ], + "inlineEditDisplayField": "addressDisplay" } diff --git a/schemas/types.ts b/schemas/types.ts index ec90994b..fcd8535d 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -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 } diff --git a/src/utils.js b/src/utils.js index 145fb17f..04186b63 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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'); diff --git a/tsconfig.json b/tsconfig.json index 462722a4..e3a8ea0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "@/*": ["src/*"], "schemas/*": ["schemas/*"], "backend/*": ["backend/*"], + "regional/*": ["regional/*"], "utils/*": ["utils/*"] }, "lib": ["esnext", "dom", "dom.iterable", "scripthost"] diff --git a/utils/index.ts b/utils/index.ts index 6b1b3d62..5f46ca0a 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -55,3 +55,16 @@ export function getListFromMap(map: Record): 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(' '); +} diff --git a/vue.config.js b/vue.config.js index ab48daa1..571dc6bf 100644 --- a/vue.config.js +++ b/vue.config.js @@ -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(