mirror of
https://github.com/frappe/books.git
synced 2025-01-22 22:58:28 +00:00
Merge pull request #320 from frappe/inlining-fjs
refactor: inline frappejs code into books
This commit is contained in:
commit
da01afdf19
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@ -24,26 +24,9 @@ jobs:
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout FrappeJS
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'frappe/frappejs'
|
||||
path: framework
|
||||
|
||||
- name: Setup FrappeJS
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/framework
|
||||
yarn
|
||||
yarn link
|
||||
|
||||
- name: Setup Books
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn upgrade frappejs
|
||||
yarn link frappejs
|
||||
run: yarn
|
||||
|
||||
- name: Install RPM
|
||||
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install rpm
|
||||
@ -52,6 +35,4 @@ jobs:
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
APPLE_NOTARIZE: 0
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn electron:build -mwl --publish never
|
||||
run: yarn electron:build -mwl --publish never
|
||||
|
78
.github/workflows/publish.yml
vendored
78
.github/workflows/publish.yml
vendored
@ -16,26 +16,9 @@ jobs:
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout FrappeJS
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'frappe/frappejs'
|
||||
path: framework
|
||||
|
||||
- name: Setup FrappeJS
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/framework
|
||||
yarn
|
||||
yarn link
|
||||
|
||||
- name: Setup Books
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn upgrade frappejs
|
||||
yarn link frappejs
|
||||
run: yarn
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
@ -47,12 +30,10 @@ jobs:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: true
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
APPLE_NOTARIZE: 1
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn electron:build --mac --publish always
|
||||
run: yarn electron:build --mac --publish always
|
||||
|
||||
- name: Tar files
|
||||
run: tar -cvf dist-macOS.tar $GITHUB_WORKSPACE/main/dist_electron
|
||||
run: tar -cvf dist-macOS.tar dist_electron
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
@ -70,36 +51,17 @@ jobs:
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout FrappeJS
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'frappe/frappejs'
|
||||
path: framework
|
||||
|
||||
- name: Setup FrappeJS
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/framework
|
||||
yarn
|
||||
yarn link
|
||||
|
||||
- name: Setup Books
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn upgrade frappejs
|
||||
yarn link frappejs
|
||||
run: yarn
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn electron:build --linux --publish always
|
||||
run: yarn electron:build --linux --publish always
|
||||
|
||||
- name: Tar files
|
||||
run: tar -cvf dist-linux.tar $GITHUB_WORKSPACE/main/dist_electron
|
||||
run: tar -cvf dist-linux.tar dist_electron
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
@ -121,41 +83,19 @@ jobs:
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: main
|
||||
|
||||
- name: Checkout FrappeJS
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'frappe/frappejs'
|
||||
path: framework
|
||||
|
||||
- name: Setup FrappeJS
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/framework
|
||||
yarn
|
||||
yarn link
|
||||
|
||||
- name: Setup Books
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn upgrade frappejs
|
||||
yarn link frappejs
|
||||
run: yarn
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
yarn electron:build --win --publish always
|
||||
run: yarn electron:build --win --publish always
|
||||
|
||||
- name: Tar files
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/main
|
||||
tar -cvf dist-windows.tar dist_electron
|
||||
mv dist-windows.tar ../
|
||||
run: tar -cvf dist-windows.tar dist_electron
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { showMessageDialog } from '@/utils';
|
||||
import frappe from 'frappejs';
|
||||
import { _ } from 'frappejs/utils';
|
||||
import frappe from 'frappe';
|
||||
import { _ } from 'frappe/utils';
|
||||
import { DateTime } from 'luxon';
|
||||
import { exportCsv, saveExportData } from '../reports/commonExporter';
|
||||
import { getSavePath } from '../src/utils';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import countries from '../fixtures/countryInfo.json';
|
||||
import standardCOA from '../fixtures/verified/standardCOA.json';
|
||||
const accountFields = ['accountType', 'accountNumber', 'rootType', 'isGroup'];
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default class LedgerPosting {
|
||||
constructor({ reference, party, date, description }) {
|
||||
|
@ -1,173 +1,173 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
const _ = frappe._.bind(frappe);
|
||||
|
||||
export default {
|
||||
[_("Application of Funds (Assets)")]: {
|
||||
[_("Current Assets")]: {
|
||||
[_("Accounts Receivable")]: {
|
||||
[_("Debtors")]: {
|
||||
"accountType": "Receivable"
|
||||
}
|
||||
},
|
||||
[_("Bank Accounts")]: {
|
||||
"accountType": "Bank",
|
||||
"isGroup": 1
|
||||
},
|
||||
[_("Cash In Hand")]: {
|
||||
[_("Cash")]: {
|
||||
"accountType": "Cash"
|
||||
},
|
||||
"accountType": "Cash"
|
||||
},
|
||||
[_("Loans and Advances (Assets)")]: {
|
||||
"isGroup": 1
|
||||
},
|
||||
[_("Securities and Deposits")]: {
|
||||
[_("Earnest Money")]: {}
|
||||
},
|
||||
[_("Stock Assets")]: {
|
||||
[_("Stock In Hand")]: {
|
||||
"accountType": "Stock"
|
||||
},
|
||||
"accountType": "Stock",
|
||||
},
|
||||
[_("Tax Assets")]: {
|
||||
"isGroup": 1
|
||||
}
|
||||
[_('Application of Funds (Assets)')]: {
|
||||
[_('Current Assets')]: {
|
||||
[_('Accounts Receivable')]: {
|
||||
[_('Debtors')]: {
|
||||
accountType: 'Receivable',
|
||||
},
|
||||
[_("Fixed Assets")]: {
|
||||
[_("Capital Equipments")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Electronic Equipments")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Furnitures and Fixtures")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Office Equipments")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Plants and Machineries")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Buildings")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Softwares")]: {
|
||||
"accountType": "Fixed Asset"
|
||||
},
|
||||
[_("Accumulated Depreciation")]: {
|
||||
"accountType": "Accumulated Depreciation"
|
||||
}
|
||||
},
|
||||
[_('Bank Accounts')]: {
|
||||
accountType: 'Bank',
|
||||
isGroup: 1,
|
||||
},
|
||||
[_('Cash In Hand')]: {
|
||||
[_('Cash')]: {
|
||||
accountType: 'Cash',
|
||||
},
|
||||
[_("Investments")]: {
|
||||
"isGroup": 1
|
||||
accountType: 'Cash',
|
||||
},
|
||||
[_('Loans and Advances (Assets)')]: {
|
||||
isGroup: 1,
|
||||
},
|
||||
[_('Securities and Deposits')]: {
|
||||
[_('Earnest Money')]: {},
|
||||
},
|
||||
[_('Stock Assets')]: {
|
||||
[_('Stock In Hand')]: {
|
||||
accountType: 'Stock',
|
||||
},
|
||||
[_("Temporary Accounts")]: {
|
||||
[_("Temporary Opening")]: {
|
||||
"accountType": "Temporary"
|
||||
}
|
||||
},
|
||||
"rootType": "Asset"
|
||||
accountType: 'Stock',
|
||||
},
|
||||
[_('Tax Assets')]: {
|
||||
isGroup: 1,
|
||||
},
|
||||
},
|
||||
[_("Expenses")]: {
|
||||
[_("Direct Expenses")]: {
|
||||
[_("Stock Expenses")]: {
|
||||
[_("Cost of Goods Sold")]: {
|
||||
"accountType": "Cost of Goods Sold"
|
||||
},
|
||||
[_("Expenses Included In Valuation")]: {
|
||||
"accountType": "Expenses Included In Valuation"
|
||||
},
|
||||
[_("Stock Adjustment")]: {
|
||||
"accountType": "Stock Adjustment"
|
||||
}
|
||||
},
|
||||
},
|
||||
[_("Indirect Expenses")]: {
|
||||
[_("Administrative Expenses")]: {},
|
||||
[_("Commission on Sales")]: {},
|
||||
[_("Depreciation")]: {
|
||||
"accountType": "Depreciation"
|
||||
},
|
||||
[_("Entertainment Expenses")]: {},
|
||||
[_("Freight and Forwarding Charges")]: {
|
||||
"accountType": "Chargeable"
|
||||
},
|
||||
[_("Legal Expenses")]: {},
|
||||
[_("Marketing Expenses")]: {
|
||||
"accountType": "Chargeable"
|
||||
},
|
||||
[_("Miscellaneous Expenses")]: {
|
||||
"accountType": "Chargeable"
|
||||
},
|
||||
[_("Office Maintenance Expenses")]: {},
|
||||
[_("Office Rent")]: {},
|
||||
[_("Postal Expenses")]: {},
|
||||
[_("Print and Stationery")]: {},
|
||||
[_("Round Off")]: {
|
||||
"accountType": "Round Off"
|
||||
},
|
||||
[_("Salary")]: {},
|
||||
[_("Sales Expenses")]: {},
|
||||
[_("Telephone Expenses")]: {},
|
||||
[_("Travel Expenses")]: {},
|
||||
[_("Utility Expenses")]: {},
|
||||
[_("Write Off")]: {},
|
||||
[_("Exchange Gain/Loss")]: {},
|
||||
[_("Gain/Loss on Asset Disposal")]: {}
|
||||
},
|
||||
"rootType": "Expense"
|
||||
[_('Fixed Assets')]: {
|
||||
[_('Capital Equipments')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Electronic Equipments')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Furnitures and Fixtures')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Office Equipments')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Plants and Machineries')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Buildings')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Softwares')]: {
|
||||
accountType: 'Fixed Asset',
|
||||
},
|
||||
[_('Accumulated Depreciation')]: {
|
||||
accountType: 'Accumulated Depreciation',
|
||||
},
|
||||
},
|
||||
[_("Income")]: {
|
||||
[_("Direct Income")]: {
|
||||
[_("Sales")]: {},
|
||||
[_("Service")]: {}
|
||||
},
|
||||
[_("Indirect Income")]: {
|
||||
"isGroup": 1
|
||||
},
|
||||
"rootType": "Income"
|
||||
[_('Investments')]: {
|
||||
isGroup: 1,
|
||||
},
|
||||
[_("Source of Funds (Liabilities)")]: {
|
||||
[_("Current Liabilities")]: {
|
||||
[_("Accounts Payable")]: {
|
||||
[_("Creditors")]: {
|
||||
"accountType": "Payable"
|
||||
},
|
||||
[_("Payroll Payable")]: {},
|
||||
},
|
||||
[_("Stock Liabilities")]: {
|
||||
[_("Stock Received But Not Billed")]: {
|
||||
"accountType": "Stock Received But Not Billed"
|
||||
},
|
||||
},
|
||||
[_("Duties and Taxes")]: {
|
||||
"accountType": "Tax",
|
||||
"isGroup": 1
|
||||
},
|
||||
[_("Loans (Liabilities)")]: {
|
||||
[_("Secured Loans")]: {},
|
||||
[_("Unsecured Loans")]: {},
|
||||
[_("Bank Overdraft Account")]: {},
|
||||
},
|
||||
},
|
||||
"rootType": "Liability"
|
||||
[_('Temporary Accounts')]: {
|
||||
[_('Temporary Opening')]: {
|
||||
accountType: 'Temporary',
|
||||
},
|
||||
},
|
||||
[_("Equity")]: {
|
||||
[_("Capital Stock")]: {
|
||||
"accountType": "Equity"
|
||||
rootType: 'Asset',
|
||||
},
|
||||
[_('Expenses')]: {
|
||||
[_('Direct Expenses')]: {
|
||||
[_('Stock Expenses')]: {
|
||||
[_('Cost of Goods Sold')]: {
|
||||
accountType: 'Cost of Goods Sold',
|
||||
},
|
||||
[_("Dividends Paid")]: {
|
||||
"accountType": "Equity"
|
||||
[_('Expenses Included In Valuation')]: {
|
||||
accountType: 'Expenses Included In Valuation',
|
||||
},
|
||||
[_("Opening Balance Equity")]: {
|
||||
"accountType": "Equity"
|
||||
[_('Stock Adjustment')]: {
|
||||
accountType: 'Stock Adjustment',
|
||||
},
|
||||
[_("Retained Earnings")]: {
|
||||
"accountType": "Equity"
|
||||
},
|
||||
},
|
||||
[_('Indirect Expenses')]: {
|
||||
[_('Administrative Expenses')]: {},
|
||||
[_('Commission on Sales')]: {},
|
||||
[_('Depreciation')]: {
|
||||
accountType: 'Depreciation',
|
||||
},
|
||||
[_('Entertainment Expenses')]: {},
|
||||
[_('Freight and Forwarding Charges')]: {
|
||||
accountType: 'Chargeable',
|
||||
},
|
||||
[_('Legal Expenses')]: {},
|
||||
[_('Marketing Expenses')]: {
|
||||
accountType: 'Chargeable',
|
||||
},
|
||||
[_('Miscellaneous Expenses')]: {
|
||||
accountType: 'Chargeable',
|
||||
},
|
||||
[_('Office Maintenance Expenses')]: {},
|
||||
[_('Office Rent')]: {},
|
||||
[_('Postal Expenses')]: {},
|
||||
[_('Print and Stationery')]: {},
|
||||
[_('Round Off')]: {
|
||||
accountType: 'Round Off',
|
||||
},
|
||||
[_('Salary')]: {},
|
||||
[_('Sales Expenses')]: {},
|
||||
[_('Telephone Expenses')]: {},
|
||||
[_('Travel Expenses')]: {},
|
||||
[_('Utility Expenses')]: {},
|
||||
[_('Write Off')]: {},
|
||||
[_('Exchange Gain/Loss')]: {},
|
||||
[_('Gain/Loss on Asset Disposal')]: {},
|
||||
},
|
||||
rootType: 'Expense',
|
||||
},
|
||||
[_('Income')]: {
|
||||
[_('Direct Income')]: {
|
||||
[_('Sales')]: {},
|
||||
[_('Service')]: {},
|
||||
},
|
||||
[_('Indirect Income')]: {
|
||||
isGroup: 1,
|
||||
},
|
||||
rootType: 'Income',
|
||||
},
|
||||
[_('Source of Funds (Liabilities)')]: {
|
||||
[_('Current Liabilities')]: {
|
||||
[_('Accounts Payable')]: {
|
||||
[_('Creditors')]: {
|
||||
accountType: 'Payable',
|
||||
},
|
||||
"rootType": "Equity"
|
||||
}
|
||||
};
|
||||
[_('Payroll Payable')]: {},
|
||||
},
|
||||
[_('Stock Liabilities')]: {
|
||||
[_('Stock Received But Not Billed')]: {
|
||||
accountType: 'Stock Received But Not Billed',
|
||||
},
|
||||
},
|
||||
[_('Duties and Taxes')]: {
|
||||
accountType: 'Tax',
|
||||
isGroup: 1,
|
||||
},
|
||||
[_('Loans (Liabilities)')]: {
|
||||
[_('Secured Loans')]: {},
|
||||
[_('Unsecured Loans')]: {},
|
||||
[_('Bank Overdraft Account')]: {},
|
||||
},
|
||||
},
|
||||
rootType: 'Liability',
|
||||
},
|
||||
[_('Equity')]: {
|
||||
[_('Capital Stock')]: {
|
||||
accountType: 'Equity',
|
||||
},
|
||||
[_('Dividends Paid')]: {
|
||||
accountType: 'Equity',
|
||||
},
|
||||
[_('Opening Balance Equity')]: {
|
||||
accountType: 'Equity',
|
||||
},
|
||||
[_('Retained Earnings')]: {
|
||||
accountType: 'Equity',
|
||||
},
|
||||
rootType: 'Equity',
|
||||
},
|
||||
};
|
||||
|
830
frappe/backends/database.js
Normal file
830
frappe/backends/database.js
Normal file
@ -0,0 +1,830 @@
|
||||
const frappe = require('frappe');
|
||||
const Observable = require('frappe/utils/observable');
|
||||
const CacheManager = require('frappe/utils/cacheManager');
|
||||
const Knex = require('knex');
|
||||
|
||||
module.exports = class Database extends Observable {
|
||||
constructor() {
|
||||
super();
|
||||
this.initTypeMap();
|
||||
this.connectionParams = {};
|
||||
this.cache = new CacheManager();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.knex = Knex(this.connectionParams);
|
||||
this.knex.on('query-error', (error) => {
|
||||
error.type = this.getError(error);
|
||||
});
|
||||
this.executePostDbConnect();
|
||||
}
|
||||
|
||||
close() {
|
||||
//
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
for (let doctype in frappe.models) {
|
||||
// check if controller module
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
if (!meta.isSingle) {
|
||||
if (await this.tableExists(baseDoctype)) {
|
||||
await this.alterTable(baseDoctype);
|
||||
} else {
|
||||
await this.createTable(baseDoctype);
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.commit();
|
||||
await this.initializeSingles();
|
||||
}
|
||||
|
||||
async initializeSingles() {
|
||||
let singleDoctypes = frappe
|
||||
.getModels((model) => model.isSingle)
|
||||
.map((model) => model.name);
|
||||
|
||||
for (let doctype of singleDoctypes) {
|
||||
if (await this.singleExists(doctype)) {
|
||||
const singleValues = await this.getSingleFieldsToInsert(doctype);
|
||||
singleValues.forEach(({ fieldname, value }) => {
|
||||
let singleValue = frappe.newDoc({
|
||||
doctype: 'SingleValue',
|
||||
parent: doctype,
|
||||
fieldname,
|
||||
value,
|
||||
});
|
||||
singleValue.insert();
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let meta = frappe.getMeta(doctype);
|
||||
if (meta.fields.every((df) => df.default == null)) {
|
||||
continue;
|
||||
}
|
||||
let defaultValues = meta.fields.reduce((doc, df) => {
|
||||
if (df.default != null) {
|
||||
doc[df.fieldname] = df.default;
|
||||
}
|
||||
return doc;
|
||||
}, {});
|
||||
await this.updateSingle(doctype, defaultValues);
|
||||
}
|
||||
}
|
||||
|
||||
async singleExists(doctype) {
|
||||
let res = await this.knex('SingleValue')
|
||||
.count('parent as count')
|
||||
.where('parent', doctype)
|
||||
.first();
|
||||
return res.count > 0;
|
||||
}
|
||||
|
||||
async getSingleFieldsToInsert(doctype) {
|
||||
const existingFields = (
|
||||
await frappe.db
|
||||
.knex('SingleValue')
|
||||
.where({ parent: doctype })
|
||||
.select('fieldname')
|
||||
).map(({ fieldname }) => fieldname);
|
||||
|
||||
return frappe
|
||||
.getMeta(doctype)
|
||||
.fields.map(({ fieldname, default: value }) => ({
|
||||
fieldname,
|
||||
value,
|
||||
}))
|
||||
.filter(
|
||||
({ fieldname, value }) =>
|
||||
!existingFields.includes(fieldname) && value !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
tableExists(table) {
|
||||
return this.knex.schema.hasTable(table);
|
||||
}
|
||||
|
||||
async createTable(doctype, tableName = null) {
|
||||
let fields = this.getValidFields(doctype);
|
||||
return await this.runCreateTableQuery(tableName || doctype, fields);
|
||||
}
|
||||
|
||||
runCreateTableQuery(doctype, fields) {
|
||||
return this.knex.schema.createTable(doctype, (table) => {
|
||||
for (let field of fields) {
|
||||
this.buildColumnForTable(table, field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async alterTable(doctype) {
|
||||
// get columns
|
||||
let diff = await this.getColumnDiff(doctype);
|
||||
let newForeignKeys = await this.getNewForeignKeys(doctype);
|
||||
|
||||
return this.knex.schema
|
||||
.table(doctype, (table) => {
|
||||
if (diff.added.length) {
|
||||
for (let field of diff.added) {
|
||||
this.buildColumnForTable(table, field);
|
||||
}
|
||||
}
|
||||
|
||||
if (diff.removed.length) {
|
||||
this.removeColumns(doctype, diff.removed);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (newForeignKeys.length) {
|
||||
return this.addForeignKeys(doctype, newForeignKeys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildColumnForTable(table, field) {
|
||||
let columnType = this.getColumnType(field);
|
||||
if (!columnType) {
|
||||
// In case columnType is "Table"
|
||||
// childTable links are handled using the childTable's "parent" field
|
||||
return;
|
||||
}
|
||||
|
||||
let column = table[columnType](field.fieldname);
|
||||
|
||||
// primary key
|
||||
if (field.fieldname === 'name') {
|
||||
column.primary();
|
||||
}
|
||||
|
||||
// default value
|
||||
if (!!field.default && !(field.default instanceof Function)) {
|
||||
column.defaultTo(field.default);
|
||||
}
|
||||
|
||||
// required
|
||||
if (
|
||||
(!!field.required && !(field.required instanceof Function)) ||
|
||||
field.fieldtype === 'Currency'
|
||||
) {
|
||||
column.notNullable();
|
||||
}
|
||||
|
||||
// link
|
||||
if (field.fieldtype === 'Link' && field.target) {
|
||||
let meta = frappe.getMeta(field.target);
|
||||
table
|
||||
.foreign(field.fieldname)
|
||||
.references('name')
|
||||
.inTable(meta.getBaseDocType())
|
||||
.onUpdate('CASCADE')
|
||||
.onDelete('RESTRICT');
|
||||
}
|
||||
}
|
||||
|
||||
async getColumnDiff(doctype) {
|
||||
const tableColumns = await this.getTableColumns(doctype);
|
||||
const validFields = this.getValidFields(doctype);
|
||||
const diff = { added: [], removed: [] };
|
||||
|
||||
for (let field of validFields) {
|
||||
if (
|
||||
!tableColumns.includes(field.fieldname) &&
|
||||
this.getColumnType(field)
|
||||
) {
|
||||
diff.added.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
const validFieldNames = validFields.map((field) => field.fieldname);
|
||||
for (let column of tableColumns) {
|
||||
if (!validFieldNames.includes(column)) {
|
||||
diff.removed.push(column);
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
async removeColumns(doctype, removed) {
|
||||
for (let column of removed) {
|
||||
await this.runRemoveColumnQuery(doctype, column);
|
||||
}
|
||||
}
|
||||
|
||||
async getNewForeignKeys(doctype) {
|
||||
let foreignKeys = await this.getForeignKeys(doctype);
|
||||
let newForeignKeys = [];
|
||||
let meta = frappe.getMeta(doctype);
|
||||
for (let field of meta.getValidFields({ withChildren: false })) {
|
||||
if (
|
||||
field.fieldtype === 'Link' &&
|
||||
!foreignKeys.includes(field.fieldname)
|
||||
) {
|
||||
newForeignKeys.push(field);
|
||||
}
|
||||
}
|
||||
return newForeignKeys;
|
||||
}
|
||||
|
||||
async addForeignKeys(doctype, newForeignKeys) {
|
||||
for (let field of newForeignKeys) {
|
||||
this.addForeignKey(doctype, field);
|
||||
}
|
||||
}
|
||||
|
||||
async getForeignKeys(doctype, field) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getTableColumns(doctype) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async get(doctype, name = null, fields = '*') {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let doc;
|
||||
if (meta.isSingle) {
|
||||
doc = await this.getSingle(doctype);
|
||||
doc.name = doctype;
|
||||
} else {
|
||||
if (!name) {
|
||||
throw new frappe.errors.ValueError('name is mandatory');
|
||||
}
|
||||
doc = await this.getOne(doctype, name, fields);
|
||||
}
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
await this.loadChildren(doc, meta);
|
||||
return doc;
|
||||
}
|
||||
|
||||
async loadChildren(doc, meta) {
|
||||
// load children
|
||||
let tableFields = meta.getTableFields();
|
||||
for (let field of tableFields) {
|
||||
doc[field.fieldname] = await this.getAll({
|
||||
doctype: field.childtype,
|
||||
fields: ['*'],
|
||||
filters: { parent: doc.name },
|
||||
orderBy: 'idx',
|
||||
order: 'asc',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getSingle(doctype) {
|
||||
let values = await this.getAll({
|
||||
doctype: 'SingleValue',
|
||||
fields: ['fieldname', 'value'],
|
||||
filters: { parent: doctype },
|
||||
orderBy: 'fieldname',
|
||||
order: 'asc',
|
||||
});
|
||||
let doc = {};
|
||||
for (let row of values) {
|
||||
doc[row.fieldname] = row.value;
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of values from the singles table.
|
||||
* @param {...string | Object} fieldnames list of fieldnames to get the values of
|
||||
* @returns {Array<Object>} array of {parent, value, fieldname}.
|
||||
* @example
|
||||
* Database.getSingleValues('internalPrecision');
|
||||
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
||||
* @example
|
||||
* Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'});
|
||||
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
||||
*/
|
||||
async getSingleValues(...fieldnames) {
|
||||
fieldnames = fieldnames.map((fieldname) => {
|
||||
if (typeof fieldname === 'string') {
|
||||
return { fieldname };
|
||||
}
|
||||
return fieldname;
|
||||
});
|
||||
|
||||
let builder = frappe.db.knex('SingleValue');
|
||||
builder = builder.where(fieldnames[0]);
|
||||
|
||||
fieldnames.slice(1).forEach(({ fieldname, parent }) => {
|
||||
if (typeof parent === 'undefined') {
|
||||
builder = builder.orWhere({ fieldname });
|
||||
} else {
|
||||
builder = builder.orWhere({ fieldname, parent });
|
||||
}
|
||||
});
|
||||
|
||||
let values = [];
|
||||
try {
|
||||
values = await builder.select('fieldname', 'value', 'parent');
|
||||
} catch (error) {
|
||||
if (error.message.includes('no such table')) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return values.map((value) => {
|
||||
const fields = frappe.getMeta(value.parent).fields;
|
||||
return this.getDocFormattedDoc(fields, values);
|
||||
});
|
||||
}
|
||||
|
||||
async getOne(doctype, name, fields = '*') {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
|
||||
const doc = await this.knex
|
||||
.select(fields)
|
||||
.from(baseDoctype)
|
||||
.where('name', name)
|
||||
.first();
|
||||
|
||||
if (!doc) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
return this.getDocFormattedDoc(meta.fields, doc);
|
||||
}
|
||||
|
||||
getDocFormattedDoc(fields, doc) {
|
||||
// format for usage, not going into the db
|
||||
const docFields = Object.keys(doc);
|
||||
const filteredFields = fields.filter(({ fieldname }) =>
|
||||
docFields.includes(fieldname)
|
||||
);
|
||||
|
||||
const formattedValues = filteredFields.reduce((d, field) => {
|
||||
const { fieldname } = field;
|
||||
d[fieldname] = this.getDocFormattedValues(field, doc[fieldname]);
|
||||
return d;
|
||||
}, {});
|
||||
|
||||
return Object.assign(doc, formattedValues);
|
||||
}
|
||||
|
||||
getDocFormattedValues(field, value) {
|
||||
// format for usage, not going into the db
|
||||
try {
|
||||
if (field.fieldtype === 'Currency') {
|
||||
return frappe.pesa(value);
|
||||
}
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${
|
||||
field.fieldname
|
||||
}', label: '${field.label}'`;
|
||||
throw err;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
triggerChange(doctype, name) {
|
||||
this.trigger(`change:${doctype}`, { name }, 500);
|
||||
this.trigger(`change`, { doctype, name }, 500);
|
||||
// also trigger change for basedOn doctype
|
||||
let meta = frappe.getMeta(doctype);
|
||||
if (meta.basedOn) {
|
||||
this.triggerChange(meta.basedOn, name);
|
||||
}
|
||||
}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
doc = this.applyBaseDocTypeFilters(doctype, doc);
|
||||
|
||||
// insert parent
|
||||
if (meta.isSingle) {
|
||||
await this.updateSingle(doctype, doc);
|
||||
} else {
|
||||
await this.insertOne(baseDoctype, doc);
|
||||
}
|
||||
|
||||
// insert children
|
||||
await this.insertChildren(meta, doc, baseDoctype);
|
||||
|
||||
this.triggerChange(doctype, doc.name);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
async insertChildren(meta, doc, doctype) {
|
||||
let tableFields = meta.getTableFields();
|
||||
for (let field of tableFields) {
|
||||
let idx = 0;
|
||||
for (let child of doc[field.fieldname] || []) {
|
||||
this.prepareChild(doctype, doc.name, child, field, idx);
|
||||
await this.insertOne(field.childtype, child);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertOne(doctype, doc) {
|
||||
let fields = this.getValidFields(doctype);
|
||||
|
||||
if (!doc.name) {
|
||||
doc.name = frappe.getRandomString();
|
||||
}
|
||||
|
||||
let formattedDoc = this.getFormattedDoc(fields, doc);
|
||||
return this.knex(doctype).insert(formattedDoc);
|
||||
}
|
||||
|
||||
async update(doctype, doc) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
doc = this.applyBaseDocTypeFilters(doctype, doc);
|
||||
|
||||
// update parent
|
||||
if (meta.isSingle) {
|
||||
await this.updateSingle(doctype, doc);
|
||||
} else {
|
||||
await this.updateOne(baseDoctype, doc);
|
||||
}
|
||||
|
||||
// insert or update children
|
||||
await this.updateChildren(meta, doc, baseDoctype);
|
||||
|
||||
this.triggerChange(doctype, doc.name);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
async updateChildren(meta, doc, doctype) {
|
||||
let tableFields = meta.getTableFields();
|
||||
for (let field of tableFields) {
|
||||
let added = [];
|
||||
for (let child of doc[field.fieldname] || []) {
|
||||
this.prepareChild(doctype, doc.name, child, field, added.length);
|
||||
if (await this.exists(field.childtype, child.name)) {
|
||||
await this.updateOne(field.childtype, child);
|
||||
} else {
|
||||
await this.insertOne(field.childtype, child);
|
||||
}
|
||||
added.push(child.name);
|
||||
}
|
||||
await this.runDeleteOtherChildren(field, doc.name, added);
|
||||
}
|
||||
}
|
||||
|
||||
updateOne(doctype, doc) {
|
||||
let validFields = this.getValidFields(doctype);
|
||||
let fieldsToUpdate = Object.keys(doc).filter((f) => f !== 'name');
|
||||
let fields = validFields.filter((df) =>
|
||||
fieldsToUpdate.includes(df.fieldname)
|
||||
);
|
||||
let formattedDoc = this.getFormattedDoc(fields, doc);
|
||||
|
||||
return this.knex(doctype)
|
||||
.where('name', doc.name)
|
||||
.update(formattedDoc)
|
||||
.then(() => {
|
||||
let cacheKey = `${doctype}:${doc.name}`;
|
||||
if (this.cache.hexists(cacheKey)) {
|
||||
for (let fieldname in formattedDoc) {
|
||||
let value = formattedDoc[fieldname];
|
||||
this.cache.hset(cacheKey, fieldname, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runDeleteOtherChildren(field, parent, added) {
|
||||
// delete other children
|
||||
return this.knex(field.childtype)
|
||||
.where('parent', parent)
|
||||
.andWhere('name', 'not in', added)
|
||||
.delete();
|
||||
}
|
||||
|
||||
async updateSingle(doctype, doc) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
await this.deleteSingleValues(doctype);
|
||||
for (let field of meta.getValidFields({ withChildren: false })) {
|
||||
let value = doc[field.fieldname];
|
||||
if (value != null) {
|
||||
let singleValue = frappe.newDoc({
|
||||
doctype: 'SingleValue',
|
||||
parent: doctype,
|
||||
fieldname: field.fieldname,
|
||||
value: value,
|
||||
});
|
||||
await singleValue.insert();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteSingleValues(name) {
|
||||
return this.knex('SingleValue').where('parent', name).delete();
|
||||
}
|
||||
|
||||
async rename(doctype, oldName, newName) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
await this.knex(baseDoctype)
|
||||
.update({ name: newName })
|
||||
.where('name', oldName)
|
||||
.then(() => {
|
||||
this.clearValueCache(doctype, oldName);
|
||||
});
|
||||
await frappe.db.commit();
|
||||
|
||||
this.triggerChange(doctype, newName);
|
||||
}
|
||||
|
||||
prepareChild(parenttype, parent, child, field, idx) {
|
||||
if (!child.name) {
|
||||
child.name = frappe.getRandomString();
|
||||
}
|
||||
child.parent = parent;
|
||||
child.parenttype = parenttype;
|
||||
child.parentfield = field.fieldname;
|
||||
child.idx = idx;
|
||||
}
|
||||
|
||||
getValidFields(doctype) {
|
||||
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
|
||||
}
|
||||
|
||||
getFormattedDoc(fields, doc) {
|
||||
// format for storage, going into the db
|
||||
let formattedDoc = {};
|
||||
fields.map((field) => {
|
||||
let value = doc[field.fieldname];
|
||||
formattedDoc[field.fieldname] = this.getFormattedValue(field, value);
|
||||
});
|
||||
return formattedDoc;
|
||||
}
|
||||
|
||||
getFormattedValue(field, value) {
|
||||
// format for storage, going into the db
|
||||
const type = typeof value;
|
||||
if (field.fieldtype === 'Currency') {
|
||||
let currency = value;
|
||||
|
||||
if (type === 'number' || type === 'string') {
|
||||
currency = frappe.pesa(value);
|
||||
}
|
||||
|
||||
const currencyValue = currency.store;
|
||||
if (typeof currencyValue !== 'string') {
|
||||
throw new Error(
|
||||
`invalid currencyValue '${currencyValue}' of type '${typeof currencyValue}' on converting from '${value}' of type '${type}'`
|
||||
);
|
||||
}
|
||||
|
||||
return currencyValue;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
if (field.fieldtype === 'Date') {
|
||||
// date
|
||||
return value.toISOString().substr(0, 10);
|
||||
} else {
|
||||
// datetime
|
||||
return value.toISOString();
|
||||
}
|
||||
} else if (field.fieldtype === 'Link' && !value) {
|
||||
// empty value must be null to satisfy
|
||||
// foreign key constraint
|
||||
return null;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
applyBaseDocTypeFilters(doctype, doc) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
if (meta.filters) {
|
||||
for (let fieldname in meta.filters) {
|
||||
let value = meta.filters[fieldname];
|
||||
if (typeof value !== 'object') {
|
||||
doc[fieldname] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
async deleteMany(doctype, names) {
|
||||
for (const name of names) {
|
||||
await this.delete(doctype, name);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
await this.deleteOne(baseDoctype, name);
|
||||
|
||||
// delete children
|
||||
let tableFields = frappe.getMeta(doctype).getTableFields();
|
||||
for (let field of tableFields) {
|
||||
await this.deleteChildren(field.childtype, name);
|
||||
}
|
||||
|
||||
this.triggerChange(doctype, name);
|
||||
}
|
||||
|
||||
async deleteOne(doctype, name) {
|
||||
return this.knex(doctype)
|
||||
.where('name', name)
|
||||
.delete()
|
||||
.then(() => {
|
||||
this.clearValueCache(doctype, name);
|
||||
});
|
||||
}
|
||||
|
||||
deleteChildren(parenttype, parent) {
|
||||
return this.knex(parenttype).where('parent', parent).delete();
|
||||
}
|
||||
|
||||
async exists(doctype, name) {
|
||||
return (await this.getValue(doctype, name)) ? true : false;
|
||||
}
|
||||
|
||||
async getValue(doctype, filters, fieldname = 'name') {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
if (typeof filters === 'string') {
|
||||
filters = { name: filters };
|
||||
}
|
||||
if (meta.filters) {
|
||||
Object.assign(filters, meta.filters);
|
||||
}
|
||||
|
||||
let row = await this.getAll({
|
||||
doctype: baseDoctype,
|
||||
fields: [fieldname],
|
||||
filters: filters,
|
||||
start: 0,
|
||||
limit: 1,
|
||||
orderBy: 'name',
|
||||
order: 'asc',
|
||||
});
|
||||
return row.length ? row[0][fieldname] : null;
|
||||
}
|
||||
|
||||
async setValue(doctype, name, fieldname, value) {
|
||||
return await this.setValues(doctype, name, {
|
||||
[fieldname]: value,
|
||||
});
|
||||
}
|
||||
|
||||
async setValues(doctype, name, fieldValuePair) {
|
||||
let doc = Object.assign({}, fieldValuePair, { name });
|
||||
return this.updateOne(doctype, doc);
|
||||
}
|
||||
|
||||
async getCachedValue(doctype, name, fieldname) {
|
||||
let value = this.cache.hget(`${doctype}:${name}`, fieldname);
|
||||
if (value == null) {
|
||||
value = await this.getValue(doctype, name, fieldname);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async getAll({
|
||||
doctype,
|
||||
fields,
|
||||
filters,
|
||||
start,
|
||||
limit,
|
||||
groupBy,
|
||||
orderBy = 'creation',
|
||||
order = 'desc',
|
||||
} = {}) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
if (!fields) {
|
||||
fields = meta.getKeywordFields();
|
||||
fields.push('name');
|
||||
}
|
||||
if (typeof fields === 'string') {
|
||||
fields = [fields];
|
||||
}
|
||||
if (meta.filters) {
|
||||
filters = Object.assign({}, filters, meta.filters);
|
||||
}
|
||||
|
||||
let builder = this.knex.select(fields).from(baseDoctype);
|
||||
|
||||
this.applyFiltersToBuilder(builder, filters);
|
||||
|
||||
if (orderBy) {
|
||||
builder.orderBy(orderBy, order);
|
||||
}
|
||||
|
||||
if (groupBy) {
|
||||
builder.groupBy(groupBy);
|
||||
}
|
||||
|
||||
if (start) {
|
||||
builder.offset(start);
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
builder.limit(limit);
|
||||
}
|
||||
|
||||
const docs = await builder;
|
||||
return docs.map((doc) => this.getDocFormattedDoc(meta.fields, doc));
|
||||
}
|
||||
|
||||
applyFiltersToBuilder(builder, filters) {
|
||||
// {"status": "Open"} => `status = "Open"`
|
||||
|
||||
// {"status": "Open", "name": ["like", "apple%"]}
|
||||
// => `status="Open" and name like "apple%"
|
||||
|
||||
// {"date": [">=", "2017-09-09", "<=", "2017-11-01"]}
|
||||
// => `date >= 2017-09-09 and date <= 2017-11-01`
|
||||
|
||||
let filtersArray = [];
|
||||
|
||||
for (let field in filters) {
|
||||
let value = filters[field];
|
||||
let operator = '=';
|
||||
let comparisonValue = value;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
operator = value[0];
|
||||
comparisonValue = value[1];
|
||||
operator = operator.toLowerCase();
|
||||
|
||||
if (operator === 'includes') {
|
||||
operator = 'like';
|
||||
}
|
||||
|
||||
if (operator === 'like' && !comparisonValue.includes('%')) {
|
||||
comparisonValue = `%${comparisonValue}%`;
|
||||
}
|
||||
}
|
||||
|
||||
filtersArray.push([field, operator, comparisonValue]);
|
||||
|
||||
if (Array.isArray(value) && value.length > 2) {
|
||||
// multiple conditions
|
||||
let operator = value[2];
|
||||
let comparisonValue = value[3];
|
||||
filtersArray.push([field, operator, comparisonValue]);
|
||||
}
|
||||
}
|
||||
|
||||
filtersArray.map((filter) => {
|
||||
const [field, operator, comparisonValue] = filter;
|
||||
if (operator === '=') {
|
||||
builder.where(field, comparisonValue);
|
||||
} else {
|
||||
builder.where(field, operator, comparisonValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(query, params) {
|
||||
// run query
|
||||
return this.sql(query, params);
|
||||
}
|
||||
|
||||
sql(query, params) {
|
||||
// run sql
|
||||
return this.knex.raw(query, params);
|
||||
}
|
||||
|
||||
async commit() {
|
||||
try {
|
||||
await this.sql('commit');
|
||||
} catch (e) {
|
||||
if (e.type !== frappe.errors.CannotCommitError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearValueCache(doctype, name) {
|
||||
let cacheKey = `${doctype}:${name}`;
|
||||
this.cache.hclear(cacheKey);
|
||||
}
|
||||
|
||||
getColumnType(field) {
|
||||
return this.typeMap[field.fieldtype];
|
||||
}
|
||||
|
||||
getError(err) {
|
||||
return frappe.errors.DatabaseError;
|
||||
}
|
||||
|
||||
initTypeMap() {
|
||||
this.typeMap = {};
|
||||
}
|
||||
|
||||
executePostDbConnect() {
|
||||
frappe.initializeMoneyMaker();
|
||||
}
|
||||
};
|
235
frappe/backends/http.js
Normal file
235
frappe/backends/http.js
Normal file
@ -0,0 +1,235 @@
|
||||
const frappe = require('frappe');
|
||||
const Observable = require('frappe/utils/observable');
|
||||
const triggerEvent = (name) => frappe.events.trigger(`http:${name}`);
|
||||
|
||||
module.exports = class HTTPClient extends Observable {
|
||||
constructor({ server, protocol = 'http' }) {
|
||||
super();
|
||||
|
||||
this.server = server;
|
||||
this.protocol = protocol;
|
||||
frappe.config.serverURL = this.getURL();
|
||||
|
||||
// if the backend is http, then always client!
|
||||
frappe.isServer = false;
|
||||
|
||||
this.initTypeMap();
|
||||
}
|
||||
|
||||
connect() {}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
doc.doctype = doctype;
|
||||
let filesToUpload = this.getFilesToUpload(doc);
|
||||
let url = this.getURL('/api/resource', doctype);
|
||||
|
||||
const responseDoc = await this.fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(doc),
|
||||
});
|
||||
|
||||
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
|
||||
|
||||
return responseDoc;
|
||||
}
|
||||
|
||||
async get(doctype, name) {
|
||||
name = encodeURIComponent(name);
|
||||
let url = this.getURL('/api/resource', doctype, name);
|
||||
return await this.fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
async getAll({ doctype, fields, filters, start, limit, sortBy, order }) {
|
||||
let url = this.getURL('/api/resource', doctype);
|
||||
|
||||
url =
|
||||
url +
|
||||
'?' +
|
||||
frappe.getQueryString({
|
||||
fields: JSON.stringify(fields),
|
||||
filters: JSON.stringify(filters),
|
||||
start: start,
|
||||
limit: limit,
|
||||
sortBy: sortBy,
|
||||
order: order,
|
||||
});
|
||||
|
||||
return await this.fetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
async update(doctype, doc) {
|
||||
doc.doctype = doctype;
|
||||
let filesToUpload = this.getFilesToUpload(doc);
|
||||
let url = this.getURL('/api/resource', doctype, doc.name);
|
||||
|
||||
const responseDoc = await this.fetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(doc),
|
||||
});
|
||||
|
||||
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
|
||||
|
||||
return responseDoc;
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
let url = this.getURL('/api/resource', doctype, name);
|
||||
|
||||
return await this.fetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMany(doctype, names) {
|
||||
let url = this.getURL('/api/resource', doctype);
|
||||
|
||||
return await this.fetch(url, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(names),
|
||||
});
|
||||
}
|
||||
|
||||
async exists(doctype, name) {
|
||||
return (await this.getValue(doctype, name, 'name')) ? true : false;
|
||||
}
|
||||
|
||||
async getValue(doctype, name, fieldname) {
|
||||
let url = this.getURL('/api/resource', doctype, name, fieldname);
|
||||
|
||||
return (
|
||||
await this.fetch(url, {
|
||||
method: 'GET',
|
||||
})
|
||||
).value;
|
||||
}
|
||||
|
||||
async fetch(url, args) {
|
||||
triggerEvent('ajaxStart');
|
||||
|
||||
args.headers = this.getHeaders();
|
||||
let response = await frappe.fetch(url, args);
|
||||
|
||||
triggerEvent('ajaxStop');
|
||||
|
||||
if (response.status === 200) {
|
||||
let data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
triggerEvent('unauthorized');
|
||||
}
|
||||
|
||||
throw Error(await response.text());
|
||||
}
|
||||
|
||||
getFilesToUpload(doc) {
|
||||
const meta = frappe.getMeta(doc.doctype);
|
||||
const fileFields = meta.getFieldsWith({ fieldtype: 'File' });
|
||||
const filesToUpload = [];
|
||||
|
||||
if (fileFields.length > 0) {
|
||||
fileFields.forEach((df) => {
|
||||
const files = doc[df.fieldname] || [];
|
||||
if (files.length) {
|
||||
filesToUpload.push({
|
||||
fieldname: df.fieldname,
|
||||
files: files,
|
||||
});
|
||||
}
|
||||
delete doc[df.fieldname];
|
||||
});
|
||||
}
|
||||
|
||||
return filesToUpload;
|
||||
}
|
||||
|
||||
async uploadFilesAndUpdateDoc(filesToUpload, doctype, doc) {
|
||||
if (filesToUpload.length > 0) {
|
||||
// upload files
|
||||
for (const fileToUpload of filesToUpload) {
|
||||
const files = await this.uploadFiles(
|
||||
fileToUpload.files,
|
||||
doctype,
|
||||
doc.name,
|
||||
fileToUpload.fieldname
|
||||
);
|
||||
doc[fileToUpload.fieldname] = files[0].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFiles(fileList, doctype, name, fieldname) {
|
||||
let url = this.getURL('/api/upload', doctype, name, fieldname);
|
||||
|
||||
let formData = new FormData();
|
||||
for (const file of fileList) {
|
||||
formData.append('files', file, file.name);
|
||||
}
|
||||
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.status !== 200) {
|
||||
throw Error(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getURL(...parts) {
|
||||
return this.protocol + '://' + this.server + (parts || []).join('/');
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (frappe.session && frappe.session.token) {
|
||||
headers.token = frappe.session.token;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
initTypeMap() {
|
||||
this.typeMap = {
|
||||
AutoComplete: true,
|
||||
Currency: true,
|
||||
Int: true,
|
||||
Float: true,
|
||||
Percent: true,
|
||||
Check: true,
|
||||
'Small Text': true,
|
||||
'Long Text': true,
|
||||
Code: true,
|
||||
'Text Editor': true,
|
||||
Date: true,
|
||||
Datetime: true,
|
||||
Time: true,
|
||||
Text: true,
|
||||
Data: true,
|
||||
Link: true,
|
||||
DynamicLink: true,
|
||||
Password: true,
|
||||
Select: true,
|
||||
'Read Only': true,
|
||||
File: true,
|
||||
Attach: true,
|
||||
'Attach Image': true,
|
||||
Signature: true,
|
||||
Color: true,
|
||||
Barcode: true,
|
||||
Geolocation: true,
|
||||
};
|
||||
}
|
||||
|
||||
close() {}
|
||||
};
|
238
frappe/backends/mysql.js
Normal file
238
frappe/backends/mysql.js
Normal file
@ -0,0 +1,238 @@
|
||||
const frappe = require('frappe');
|
||||
const mysql = require('mysql');
|
||||
const Database = require('./database');
|
||||
const debug = false;
|
||||
|
||||
module.exports = class mysqlDatabase extends Database {
|
||||
constructor({ db_name, username, password, host }) {
|
||||
super();
|
||||
this.db_name = db_name;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.host = host;
|
||||
this.init_typeMap();
|
||||
}
|
||||
|
||||
connect(db_name) {
|
||||
if (db_name) {
|
||||
this.db_name = db_name;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.conn = new mysql.createConnection({
|
||||
host: this.host,
|
||||
user: this.username,
|
||||
password: this.password,
|
||||
database: this.db_name,
|
||||
});
|
||||
() => {
|
||||
if (debug) {
|
||||
this.conn.on('trace', (trace) => console.log(trace));
|
||||
}
|
||||
};
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async tableExists(table) {
|
||||
const name = await this.sql(`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = '${this.db_name}'
|
||||
AND table_name = '${table}'`);
|
||||
return name && name.length ? true : false;
|
||||
}
|
||||
|
||||
async runCreateTableQuery(doctype, columns, values) {
|
||||
const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
|
||||
${columns.join(', ')})`;
|
||||
|
||||
return await this.run(query, values);
|
||||
}
|
||||
|
||||
updateColumnDefinition(df, columns, indexes) {
|
||||
columns.push(
|
||||
`${df.fieldname} ${this.typeMap[df.fieldtype]} ${
|
||||
df.required && !df.default ? 'not null' : ''
|
||||
} ${df.default ? `default '${df.default}'` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
async getTableColumns(doctype) {
|
||||
return (await this.sql(`SHOW COLUMNS FROM ${doctype}`)).map((d) => d.Field);
|
||||
}
|
||||
|
||||
async runAddColumnQuery(doctype, fields) {
|
||||
await this.run(
|
||||
`ALTER TABLE ${doctype} ADD COLUMN ${this.get_column_definition(doctype)}`
|
||||
);
|
||||
}
|
||||
|
||||
getOne(doctype, name, fields = '*') {
|
||||
fields = this.prepareFields(fields);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.get(
|
||||
`select ${fields} from ${doctype}
|
||||
where name = ?`,
|
||||
name,
|
||||
(err, row) => {
|
||||
resolve(row || {});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async insertOne(doctype, doc) {
|
||||
let fields = this.get_keys(doctype);
|
||||
let placeholders = fields.map((d) => '?').join(', ');
|
||||
|
||||
if (!doc.name) {
|
||||
doc.name = frappe.getRandomString();
|
||||
}
|
||||
|
||||
return await this.run(
|
||||
`insert into ${doctype}
|
||||
(${fields.map((field) => field.fieldname).join(', ')})
|
||||
values (${placeholders})`,
|
||||
this.getFormattedValues(fields, doc)
|
||||
);
|
||||
}
|
||||
|
||||
async updateOne(doctype, doc) {
|
||||
let fields = this.getKeys(doctype);
|
||||
let assigns = fields.map((field) => `${field.fieldname} = ?`);
|
||||
let values = this.getFormattedValues(fields, doc);
|
||||
|
||||
// additional name for where clause
|
||||
values.push(doc.name);
|
||||
|
||||
return await this.run(
|
||||
`update ${doctype}
|
||||
set ${assigns.join(', ')} where name=?`,
|
||||
values
|
||||
);
|
||||
}
|
||||
|
||||
async runDeleteOtherChildren(field, added) {
|
||||
await this.run(
|
||||
`delete from ${field.childtype}
|
||||
where
|
||||
parent = ? and
|
||||
name not in (${added
|
||||
.slice(1)
|
||||
.map((d) => '?')
|
||||
.join(', ')})`,
|
||||
added
|
||||
);
|
||||
}
|
||||
|
||||
async deleteOne(doctype, name) {
|
||||
return await this.run(`delete from ${doctype} where name=?`, name);
|
||||
}
|
||||
|
||||
async deleteChildren(parenttype, parent) {
|
||||
await this.run(`delete from ${parent} where parent=?`, parent);
|
||||
}
|
||||
|
||||
getAll({
|
||||
doctype,
|
||||
fields,
|
||||
filters,
|
||||
start,
|
||||
limit,
|
||||
order_by = 'modified',
|
||||
order = 'desc',
|
||||
} = {}) {
|
||||
if (!fields) {
|
||||
fields = frappe.getMeta(doctype).getKeywordFields();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let conditions = this.getFilterConditions(filters);
|
||||
|
||||
this.conn.all(
|
||||
`select ${fields.join(', ')}
|
||||
from ${doctype}
|
||||
${conditions.conditions ? 'where' : ''} ${conditions.conditions}
|
||||
${order_by ? 'order by ' + order_by : ''} ${
|
||||
order_by ? order || 'asc' : ''
|
||||
}
|
||||
${limit ? 'limit ' + limit : ''} ${
|
||||
start ? 'offset ' + start : ''
|
||||
}`,
|
||||
conditions.values,
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
run(query, params) {
|
||||
// TODO promisify
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.query(query, params, (err) => {
|
||||
if (err) {
|
||||
if (debug) {
|
||||
console.error(err);
|
||||
}
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sql(query, params) {
|
||||
return new Promise((resolve) => {
|
||||
this.conn.query(query, params, (err, rows) => {
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async commit() {
|
||||
try {
|
||||
await this.run('commit');
|
||||
} catch (e) {
|
||||
if (e.errno !== 1) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init_typeMap() {
|
||||
this.typeMap = {
|
||||
AutoComplete: 'VARCHAR(140)',
|
||||
Currency: 'real',
|
||||
Int: 'INT',
|
||||
Float: 'decimal(18,6)',
|
||||
Percent: 'real',
|
||||
Check: 'INT(1)',
|
||||
'Small Text': 'text',
|
||||
'Long Text': 'text',
|
||||
Code: 'text',
|
||||
'Text Editor': 'text',
|
||||
Date: 'DATE',
|
||||
Datetime: 'DATETIME',
|
||||
Time: 'TIME',
|
||||
Text: 'text',
|
||||
Data: 'VARCHAR(140)',
|
||||
Link: ' varchar(140)',
|
||||
DynamicLink: 'text',
|
||||
Password: 'varchar(140)',
|
||||
Select: 'VARCHAR(140)',
|
||||
'Read Only': 'varchar(140)',
|
||||
File: 'text',
|
||||
Attach: 'text',
|
||||
'Attach Image': 'text',
|
||||
Signature: 'text',
|
||||
Color: 'text',
|
||||
Barcode: 'text',
|
||||
Geolocation: 'text',
|
||||
};
|
||||
}
|
||||
};
|
120
frappe/backends/sqlite.js
Normal file
120
frappe/backends/sqlite.js
Normal file
@ -0,0 +1,120 @@
|
||||
const frappe = require('frappe');
|
||||
const Database = require('./database');
|
||||
|
||||
class SqliteDatabase extends Database {
|
||||
constructor({ dbPath }) {
|
||||
super();
|
||||
this.dbPath = dbPath;
|
||||
this.connectionParams = {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: this.dbPath,
|
||||
},
|
||||
pool: {
|
||||
afterCreate(conn, done) {
|
||||
conn.run('PRAGMA foreign_keys=ON');
|
||||
done();
|
||||
},
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
asyncStackTraces: process.env.NODE_ENV === 'development',
|
||||
};
|
||||
}
|
||||
|
||||
async addForeignKeys(doctype, newForeignKeys) {
|
||||
await this.sql('PRAGMA foreign_keys=OFF');
|
||||
await this.sql('BEGIN TRANSACTION');
|
||||
|
||||
const tempName = 'TEMP' + doctype;
|
||||
|
||||
// create temp table
|
||||
await this.createTable(doctype, tempName);
|
||||
|
||||
// copy from old to new table
|
||||
await this.knex(tempName).insert(this.knex.select().from(doctype));
|
||||
|
||||
// drop old table
|
||||
await this.knex.schema.dropTable(doctype);
|
||||
|
||||
// rename new table
|
||||
await this.knex.schema.renameTable(tempName, doctype);
|
||||
|
||||
await this.sql('COMMIT');
|
||||
await this.sql('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
removeColumns() {
|
||||
// pass
|
||||
}
|
||||
|
||||
async getTableColumns(doctype) {
|
||||
return (await this.sql(`PRAGMA table_info(${doctype})`)).map((d) => d.name);
|
||||
}
|
||||
|
||||
async getForeignKeys(doctype) {
|
||||
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(
|
||||
(d) => d.from
|
||||
);
|
||||
}
|
||||
|
||||
initTypeMap() {
|
||||
// prettier-ignore
|
||||
this.typeMap = {
|
||||
'AutoComplete': 'text',
|
||||
'Currency': 'text',
|
||||
'Int': 'integer',
|
||||
'Float': 'float',
|
||||
'Percent': 'float',
|
||||
'Check': 'integer',
|
||||
'Small Text': 'text',
|
||||
'Long Text': 'text',
|
||||
'Code': 'text',
|
||||
'Text Editor': 'text',
|
||||
'Date': 'text',
|
||||
'Datetime': 'text',
|
||||
'Time': 'text',
|
||||
'Text': 'text',
|
||||
'Data': 'text',
|
||||
'Link': 'text',
|
||||
'DynamicLink': 'text',
|
||||
'Password': 'text',
|
||||
'Select': 'text',
|
||||
'Read Only': 'text',
|
||||
'File': 'text',
|
||||
'Attach': 'text',
|
||||
'AttachImage': 'text',
|
||||
'Signature': 'text',
|
||||
'Color': 'text',
|
||||
'Barcode': 'text',
|
||||
'Geolocation': 'text'
|
||||
};
|
||||
}
|
||||
|
||||
getError(err) {
|
||||
let errorType = frappe.errors.DatabaseError;
|
||||
if (err.message.includes('FOREIGN KEY')) {
|
||||
errorType = frappe.errors.LinkValidationError;
|
||||
}
|
||||
if (err.message.includes('SQLITE_ERROR: cannot commit')) {
|
||||
errorType = frappe.errors.CannotCommitError;
|
||||
}
|
||||
if (err.message.includes('SQLITE_CONSTRAINT: UNIQUE constraint failed:')) {
|
||||
errorType = frappe.errors.DuplicateEntryError;
|
||||
}
|
||||
return errorType;
|
||||
}
|
||||
|
||||
async prestigeTheTable(tableName, tableRows) {
|
||||
// Alter table hacx for sqlite in case of schema change.
|
||||
const tempName = `__${tableName}`;
|
||||
await this.knex.schema.dropTableIfExists(tempName);
|
||||
await this.knex.raw('PRAGMA foreign_keys=OFF');
|
||||
await this.createTable(tableName, tempName);
|
||||
await this.knex.batchInsert(tempName, tableRows);
|
||||
await this.knex.schema.dropTable(tableName);
|
||||
await this.knex.schema.renameTable(tempName, tableName);
|
||||
await this.knex.raw('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SqliteDatabase;
|
101
frappe/common/errors.js
Normal file
101
frappe/common/errors.js
Normal file
@ -0,0 +1,101 @@
|
||||
const frappe = require('frappe');
|
||||
|
||||
class BaseError extends Error {
|
||||
constructor(statusCode, message) {
|
||||
super(message);
|
||||
this.name = 'BaseError';
|
||||
this.statusCode = statusCode;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends BaseError {
|
||||
constructor(message) {
|
||||
super(417, message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends BaseError {
|
||||
constructor(message) {
|
||||
super(404, message);
|
||||
this.name = 'NotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
class ForbiddenError extends BaseError {
|
||||
constructor(message) {
|
||||
super(403, message);
|
||||
this.name = 'ForbiddenError';
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicateEntryError extends ValidationError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'DuplicateEntryError';
|
||||
}
|
||||
}
|
||||
|
||||
class LinkValidationError extends ValidationError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'LinkValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
class MandatoryError extends ValidationError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'MandatoryError';
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends BaseError {
|
||||
constructor(message) {
|
||||
super(500, message);
|
||||
this.name = 'DatabaseError';
|
||||
}
|
||||
}
|
||||
|
||||
class CannotCommitError extends DatabaseError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'CannotCommitError';
|
||||
}
|
||||
}
|
||||
|
||||
class ValueError extends ValidationError {}
|
||||
class Conflict extends ValidationError {}
|
||||
class InvalidFieldError extends ValidationError {}
|
||||
|
||||
function throwError(message, error = 'ValidationError') {
|
||||
const errorClass = {
|
||||
ValidationError: ValidationError,
|
||||
NotFoundError: NotFoundError,
|
||||
ForbiddenError: ForbiddenError,
|
||||
ValueError: ValueError,
|
||||
Conflict: Conflict,
|
||||
};
|
||||
const err = new errorClass[error](message);
|
||||
frappe.events.trigger('throw', { message, stackTrace: err.stack });
|
||||
throw err;
|
||||
}
|
||||
|
||||
frappe.throw = throwError;
|
||||
|
||||
module.exports = {
|
||||
BaseError,
|
||||
ValidationError,
|
||||
ValueError,
|
||||
Conflict,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
DuplicateEntryError,
|
||||
LinkValidationError,
|
||||
DatabaseError,
|
||||
CannotCommitError,
|
||||
MandatoryError,
|
||||
InvalidFieldError,
|
||||
throw: throwError,
|
||||
};
|
15
frappe/common/index.js
Normal file
15
frappe/common/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
const utils = require('../utils');
|
||||
const format = require('../utils/format');
|
||||
const errors = require('./errors');
|
||||
const BaseDocument = require('frappe/model/document');
|
||||
const BaseMeta = require('frappe/model/meta');
|
||||
|
||||
module.exports = {
|
||||
initLibs(frappe) {
|
||||
Object.assign(frappe, utils);
|
||||
Object.assign(frappe, format);
|
||||
frappe.errors = errors;
|
||||
frappe.BaseDocument = BaseDocument;
|
||||
frappe.BaseMeta = BaseMeta;
|
||||
},
|
||||
};
|
372
frappe/index.js
Normal file
372
frappe/index.js
Normal file
@ -0,0 +1,372 @@
|
||||
const Observable = require('./utils/observable');
|
||||
const utils = require('./utils');
|
||||
const { getMoneyMaker } = require('pesa');
|
||||
const {
|
||||
DEFAULT_INTERNAL_PRECISION,
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
} = require('./utils/consts');
|
||||
|
||||
module.exports = {
|
||||
initializeAndRegister(customModels = {}, force = false) {
|
||||
this.init(force);
|
||||
const common = require('frappe/common');
|
||||
this.registerLibs(common);
|
||||
const coreModels = require('frappe/models');
|
||||
this.registerModels(coreModels);
|
||||
this.registerModels(customModels);
|
||||
},
|
||||
|
||||
async initializeMoneyMaker(currency) {
|
||||
currency ??= 'XXX';
|
||||
|
||||
// to be called after db initialization
|
||||
const values =
|
||||
(await frappe.db?.getSingleValues(
|
||||
{
|
||||
fieldname: 'internalPrecision',
|
||||
parent: 'SystemSettings',
|
||||
},
|
||||
{
|
||||
fieldname: 'displayPrecision',
|
||||
parent: 'SystemSettings',
|
||||
}
|
||||
)) ?? [];
|
||||
|
||||
let { internalPrecision: precision, displayPrecision: display } =
|
||||
values.reduce((acc, { fieldname, value }) => {
|
||||
acc[fieldname] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (typeof precision === 'undefined') {
|
||||
precision = DEFAULT_INTERNAL_PRECISION;
|
||||
}
|
||||
|
||||
if (typeof precision === 'string') {
|
||||
precision = parseInt(precision);
|
||||
}
|
||||
|
||||
if (typeof display === 'undefined') {
|
||||
display = DEFAULT_DISPLAY_PRECISION;
|
||||
}
|
||||
|
||||
if (typeof display === 'string') {
|
||||
display = parseInt(display);
|
||||
}
|
||||
|
||||
this.pesa = getMoneyMaker({ currency, precision, display });
|
||||
},
|
||||
|
||||
init(force) {
|
||||
if (this._initialized && !force) return;
|
||||
this.initConfig();
|
||||
this.initGlobals();
|
||||
this.docs = new Observable();
|
||||
this.events = new Observable();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
initConfig() {
|
||||
this.config = {
|
||||
serverURL: '',
|
||||
backend: 'sqlite',
|
||||
port: 8000,
|
||||
};
|
||||
},
|
||||
|
||||
initGlobals() {
|
||||
this.metaCache = {};
|
||||
this.models = {};
|
||||
this.forms = {};
|
||||
this.views = {};
|
||||
this.flags = {};
|
||||
this.methods = {};
|
||||
this.errorLog = [];
|
||||
// temp params while calling routes
|
||||
this.params = {};
|
||||
},
|
||||
|
||||
registerLibs(common) {
|
||||
// add standard libs and utils to frappe
|
||||
common.initLibs(this);
|
||||
},
|
||||
|
||||
registerModels(models) {
|
||||
// register models from app/models/index.js
|
||||
for (let doctype in models) {
|
||||
let metaDefinition = models[doctype];
|
||||
if (!metaDefinition.name) {
|
||||
throw new Error(`Name is mandatory for ${doctype}`);
|
||||
}
|
||||
if (metaDefinition.name !== doctype) {
|
||||
throw new Error(
|
||||
`Model name mismatch for ${doctype}: ${metaDefinition.name}`
|
||||
);
|
||||
}
|
||||
let fieldnames = (metaDefinition.fields || [])
|
||||
.map((df) => df.fieldname)
|
||||
.sort();
|
||||
let duplicateFieldnames = utils.getDuplicates(fieldnames);
|
||||
if (duplicateFieldnames.length > 0) {
|
||||
throw new Error(
|
||||
`Duplicate fields in ${doctype}: ${duplicateFieldnames.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
this.models[doctype] = metaDefinition;
|
||||
}
|
||||
},
|
||||
|
||||
getModels(filterFunction) {
|
||||
let models = [];
|
||||
for (let doctype in this.models) {
|
||||
models.push(this.models[doctype]);
|
||||
}
|
||||
return filterFunction ? models.filter(filterFunction) : models;
|
||||
},
|
||||
|
||||
registerView(view, name, module) {
|
||||
if (!this.views[view]) this.views[view] = {};
|
||||
this.views[view][name] = module;
|
||||
},
|
||||
|
||||
registerMethod({ method, handler }) {
|
||||
this.methods[method] = handler;
|
||||
if (this.app) {
|
||||
// add to router if client-server
|
||||
this.app.post(
|
||||
`/api/method/${method}`,
|
||||
this.asyncHandler(async function (request, response) {
|
||||
let data = await handler(request.body);
|
||||
if (data === undefined) {
|
||||
data = {};
|
||||
}
|
||||
return response.json(data);
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async call({ method, args }) {
|
||||
if (this.isServer) {
|
||||
if (this.methods[method]) {
|
||||
return await this.methods[method](args);
|
||||
} else {
|
||||
throw new Error(`${method} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
let url = `/api/method/${method}`;
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(args || {}),
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
addToCache(doc) {
|
||||
if (!this.docs) return;
|
||||
|
||||
// add to `docs` cache
|
||||
if (doc.doctype && doc.name) {
|
||||
if (!this.docs[doc.doctype]) {
|
||||
this.docs[doc.doctype] = {};
|
||||
}
|
||||
this.docs[doc.doctype][doc.name] = doc;
|
||||
|
||||
// singles available as first level objects too
|
||||
if (doc.doctype === doc.name) {
|
||||
this[doc.name] = doc;
|
||||
}
|
||||
|
||||
// propogate change to `docs`
|
||||
doc.on('change', (params) => {
|
||||
this.docs.trigger('change', params);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeFromCache(doctype, name) {
|
||||
try {
|
||||
delete this.docs[doctype][name];
|
||||
} catch (e) {
|
||||
console.warn(`Document ${doctype} ${name} does not exist`);
|
||||
}
|
||||
},
|
||||
|
||||
isDirty(doctype, name) {
|
||||
return (
|
||||
(this.docs &&
|
||||
this.docs[doctype] &&
|
||||
this.docs[doctype][name] &&
|
||||
this.docs[doctype][name]._dirty) ||
|
||||
false
|
||||
);
|
||||
},
|
||||
|
||||
getDocFromCache(doctype, name) {
|
||||
if (this.docs && this.docs[doctype] && this.docs[doctype][name]) {
|
||||
return this.docs[doctype][name];
|
||||
}
|
||||
},
|
||||
|
||||
getMeta(doctype) {
|
||||
if (!this.metaCache[doctype]) {
|
||||
let model = this.models[doctype];
|
||||
if (!model) {
|
||||
throw new Error(`${doctype} is not a registered doctype`);
|
||||
}
|
||||
let metaClass = model.metaClass || this.BaseMeta;
|
||||
this.metaCache[doctype] = new metaClass(model);
|
||||
}
|
||||
|
||||
return this.metaCache[doctype];
|
||||
},
|
||||
|
||||
async getDoc(doctype, name, options = {skipDocumentCache: false}) {
|
||||
let doc = options.skipDocumentCache ? null : this.getDocFromCache(doctype, name);
|
||||
if (!doc) {
|
||||
doc = new (this.getDocumentClass(doctype))({
|
||||
doctype: doctype,
|
||||
name: name,
|
||||
});
|
||||
await doc.load();
|
||||
this.addToCache(doc);
|
||||
}
|
||||
return doc;
|
||||
},
|
||||
|
||||
getDocumentClass(doctype) {
|
||||
const meta = this.getMeta(doctype);
|
||||
return meta.documentClass || this.BaseDocument;
|
||||
},
|
||||
|
||||
async getSingle(doctype) {
|
||||
return await this.getDoc(doctype, doctype);
|
||||
},
|
||||
|
||||
async getDuplicate(doc) {
|
||||
const newDoc = await this.getNewDoc(doc.doctype);
|
||||
for (let field of this.getMeta(doc.doctype).getValidFields()) {
|
||||
if (['name', 'submitted'].includes(field.fieldname)) continue;
|
||||
if (field.fieldtype === 'Table') {
|
||||
newDoc[field.fieldname] = (doc[field.fieldname] || []).map((d) => {
|
||||
let newd = Object.assign({}, d);
|
||||
newd.name = '';
|
||||
return newd;
|
||||
});
|
||||
} else {
|
||||
newDoc[field.fieldname] = doc[field.fieldname];
|
||||
}
|
||||
}
|
||||
return newDoc;
|
||||
},
|
||||
|
||||
getNewDoc(doctype) {
|
||||
let doc = this.newDoc({ doctype: doctype });
|
||||
doc._notInserted = true;
|
||||
doc.name = frappe.getRandomString();
|
||||
this.addToCache(doc);
|
||||
return doc;
|
||||
},
|
||||
|
||||
async newCustomDoc(fields) {
|
||||
let doc = new this.BaseDocument({ isCustom: 1, fields });
|
||||
doc._notInserted = true;
|
||||
doc.name = this.getRandomString();
|
||||
this.addToCache(doc);
|
||||
return doc;
|
||||
},
|
||||
|
||||
createMeta(fields) {
|
||||
let meta = new this.BaseMeta({ isCustom: 1, fields });
|
||||
return meta;
|
||||
},
|
||||
|
||||
newDoc(data) {
|
||||
let doc = new (this.getDocumentClass(data.doctype))(data);
|
||||
doc.setDefaults();
|
||||
return doc;
|
||||
},
|
||||
|
||||
async insert(data) {
|
||||
return await this.newDoc(data).insert();
|
||||
},
|
||||
|
||||
async syncDoc(data) {
|
||||
let doc;
|
||||
if (await this.db.exists(data.doctype, data.name)) {
|
||||
doc = await this.getDoc(data.doctype, data.name);
|
||||
Object.assign(doc, data);
|
||||
await doc.update();
|
||||
} else {
|
||||
doc = this.newDoc(data);
|
||||
await doc.insert();
|
||||
}
|
||||
},
|
||||
|
||||
// only for client side
|
||||
async login(email, password) {
|
||||
if (email === 'Administrator') {
|
||||
this.session = {
|
||||
user: 'Administrator',
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await fetch(this.getServerURL() + '/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const res = await response.json();
|
||||
|
||||
this.session = {
|
||||
user: email,
|
||||
token: res.token,
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async signup(email, fullName, password) {
|
||||
let response = await fetch(this.getServerURL() + '/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, fullName, password }),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
getServerURL() {
|
||||
return this.config.serverURL || '';
|
||||
},
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
},
|
||||
};
|
725
frappe/model/document.js
Normal file
725
frappe/model/document.js
Normal file
@ -0,0 +1,725 @@
|
||||
const frappe = require('frappe');
|
||||
const Observable = require('frappe/utils/observable');
|
||||
const naming = require('./naming');
|
||||
const { isPesa } = require('../utils/index');
|
||||
const { DEFAULT_INTERNAL_PRECISION } = require('../utils/consts');
|
||||
|
||||
module.exports = class BaseDocument extends Observable {
|
||||
constructor(data) {
|
||||
super();
|
||||
this.fetchValuesCache = {};
|
||||
this.flags = {};
|
||||
this.setup();
|
||||
this.setValues(data);
|
||||
}
|
||||
|
||||
setup() {
|
||||
// add listeners
|
||||
}
|
||||
|
||||
setValues(data) {
|
||||
for (let fieldname in data) {
|
||||
let value = data[fieldname];
|
||||
if (fieldname.startsWith('_')) {
|
||||
// private property
|
||||
this[fieldname] = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
for (let row of value) {
|
||||
this.push(fieldname, row);
|
||||
}
|
||||
} else {
|
||||
this[fieldname] = value;
|
||||
}
|
||||
}
|
||||
// set unset fields as null
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
// check for null or undefined
|
||||
if (this[field.fieldname] == null) {
|
||||
this[field.fieldname] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (this.isCustom) {
|
||||
this._meta = frappe.createMeta(this.fields);
|
||||
}
|
||||
if (!this._meta) {
|
||||
this._meta = frappe.getMeta(this.doctype);
|
||||
}
|
||||
return this._meta;
|
||||
}
|
||||
|
||||
async getSettings() {
|
||||
if (!this._settings) {
|
||||
this._settings = await frappe.getSingle(this.meta.settings);
|
||||
}
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
// set value and trigger change
|
||||
async set(fieldname, value) {
|
||||
if (typeof fieldname === 'object') {
|
||||
const valueDict = fieldname;
|
||||
for (let fieldname in valueDict) {
|
||||
await this.set(fieldname, valueDict[fieldname]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this[fieldname] !== value) {
|
||||
this._dirty = true;
|
||||
// if child is dirty, parent is dirty too
|
||||
if (this.meta.isChild && this.parentdoc) {
|
||||
this.parentdoc._dirty = true;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
this[fieldname] = [];
|
||||
value.forEach((row, i) => {
|
||||
this.append(fieldname, row);
|
||||
row.idx = i;
|
||||
});
|
||||
} else {
|
||||
await this.validateField(fieldname, value);
|
||||
this[fieldname] = value;
|
||||
}
|
||||
|
||||
// always run applyChange from the parentdoc
|
||||
if (this.meta.isChild && this.parentdoc) {
|
||||
await this.applyChange(fieldname);
|
||||
await this.parentdoc.applyChange(this.parentfield);
|
||||
} else {
|
||||
await this.applyChange(fieldname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyChange(fieldname) {
|
||||
await this.applyFormula(fieldname);
|
||||
this.roundFloats();
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
changed: fieldname,
|
||||
});
|
||||
}
|
||||
|
||||
setDefaults() {
|
||||
for (let field of this.meta.fields) {
|
||||
if (this[field.fieldname] == null) {
|
||||
let defaultValue = getPreDefaultValues(field.fieldtype);
|
||||
|
||||
if (typeof field.default === 'function') {
|
||||
defaultValue = field.default(this);
|
||||
} else if (field.default !== undefined) {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) {
|
||||
defaultValue = frappe.pesa(defaultValue);
|
||||
}
|
||||
|
||||
this[field.fieldname] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.meta.basedOn && this.meta.filters) {
|
||||
this.setValues(this.meta.filters);
|
||||
}
|
||||
}
|
||||
|
||||
castValues() {
|
||||
for (let field of this.meta.fields) {
|
||||
let value = this[field.fieldname];
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (['Int', 'Check'].includes(field.fieldtype)) {
|
||||
value = parseInt(value, 10);
|
||||
} else if (field.fieldtype === 'Float') {
|
||||
value = parseFloat(value);
|
||||
} else if (field.fieldtype === 'Currency' && !isPesa(value)) {
|
||||
value = frappe.pesa(value);
|
||||
}
|
||||
this[field.fieldname] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setKeywords() {
|
||||
let keywords = [];
|
||||
for (let fieldname of this.meta.getKeywordFields()) {
|
||||
keywords.push(this[fieldname]);
|
||||
}
|
||||
this.keywords = keywords.join(', ');
|
||||
}
|
||||
|
||||
append(key, document = {}) {
|
||||
// push child row and trigger change
|
||||
this.push(key, document);
|
||||
this._dirty = true;
|
||||
this.applyChange(key);
|
||||
}
|
||||
|
||||
push(key, document = {}) {
|
||||
// push child row without triggering change
|
||||
if (!this[key]) {
|
||||
this[key] = [];
|
||||
}
|
||||
this[key].push(this._initChild(document, key));
|
||||
}
|
||||
|
||||
_initChild(data, key) {
|
||||
if (data instanceof BaseDocument) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data.doctype = this.meta.getField(key).childtype;
|
||||
data.parent = this.name;
|
||||
data.parenttype = this.doctype;
|
||||
data.parentfield = key;
|
||||
data.parentdoc = this;
|
||||
|
||||
if (!data.idx) {
|
||||
data.idx = (this[key] || []).length;
|
||||
}
|
||||
|
||||
if (!data.name) {
|
||||
data.name = frappe.getRandomString();
|
||||
}
|
||||
|
||||
const childDoc = new BaseDocument(data);
|
||||
childDoc.setDefaults();
|
||||
return childDoc;
|
||||
}
|
||||
|
||||
validateInsert() {
|
||||
this.validateMandatory();
|
||||
this.validateFields();
|
||||
}
|
||||
|
||||
validateMandatory() {
|
||||
let checkForMandatory = [this];
|
||||
let tableFields = this.meta.fields.filter((df) => df.fieldtype === 'Table');
|
||||
tableFields.map((df) => {
|
||||
let rows = this[df.fieldname];
|
||||
checkForMandatory = [...checkForMandatory, ...rows];
|
||||
});
|
||||
|
||||
let missingMandatory = checkForMandatory
|
||||
.map((doc) => getMissingMandatory(doc))
|
||||
.filter(Boolean);
|
||||
|
||||
if (missingMandatory.length > 0) {
|
||||
let fields = missingMandatory.join('\n');
|
||||
let message = frappe._('Value missing for {0}', fields);
|
||||
throw new frappe.errors.MandatoryError(message);
|
||||
}
|
||||
|
||||
function getMissingMandatory(doc) {
|
||||
let mandatoryFields = doc.meta.fields.filter((df) => {
|
||||
if (df.required instanceof Function) {
|
||||
return df.required(doc);
|
||||
}
|
||||
return df.required;
|
||||
});
|
||||
let message = mandatoryFields
|
||||
.filter((df) => {
|
||||
let value = doc[df.fieldname];
|
||||
if (df.fieldtype === 'Table') {
|
||||
return value == null || value.length === 0;
|
||||
}
|
||||
return value == null || value === '';
|
||||
})
|
||||
.map((df) => {
|
||||
return `"${df.label}"`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
if (message && doc.meta.isChild) {
|
||||
let parentfield = doc.parentdoc.meta.getField(doc.parentfield);
|
||||
message = `${parentfield.label} Row ${doc.idx + 1}: ${message}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
async validateFields() {
|
||||
let fields = this.meta.fields;
|
||||
for (let field of fields) {
|
||||
await this.validateField(field.fieldname, this.get(field.fieldname));
|
||||
}
|
||||
}
|
||||
|
||||
async validateField(key, value) {
|
||||
let field = this.meta.getField(key);
|
||||
if (!field) {
|
||||
throw new frappe.errors.InvalidFieldError(`Invalid field ${key}`);
|
||||
}
|
||||
if (field.fieldtype == 'Select') {
|
||||
this.meta.validateSelect(field, value);
|
||||
}
|
||||
if (field.validate && value != null) {
|
||||
let validator = null;
|
||||
if (typeof field.validate === 'object') {
|
||||
validator = this.getValidateFunction(field.validate);
|
||||
}
|
||||
if (typeof field.validate === 'function') {
|
||||
validator = field.validate;
|
||||
}
|
||||
if (validator) {
|
||||
await validator(value, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValidateFunction(validator) {
|
||||
let functions = {
|
||||
email(value) {
|
||||
let isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value);
|
||||
if (!isValid) {
|
||||
throw new frappe.errors.ValidationError(`Invalid email: ${value}`);
|
||||
}
|
||||
},
|
||||
phone(value) {
|
||||
let isValid = /[+]{0,1}[\d ]+/.test(value);
|
||||
if (!isValid) {
|
||||
throw new frappe.errors.ValidationError(`Invalid phone: ${value}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return functions[validator.type];
|
||||
}
|
||||
|
||||
getValidDict() {
|
||||
let data = {};
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
let value = this[field.fieldname];
|
||||
if (Array.isArray(value)) {
|
||||
value = value.map((doc) =>
|
||||
doc.getValidDict ? doc.getValidDict() : doc
|
||||
);
|
||||
}
|
||||
data[field.fieldname] = value;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
setStandardValues() {
|
||||
// set standard values on server-side only
|
||||
if (frappe.isServer) {
|
||||
if (this.isSubmittable && this.submitted == null) {
|
||||
this.submitted = 0;
|
||||
}
|
||||
|
||||
let now = new Date().toISOString();
|
||||
if (!this.owner) {
|
||||
this.owner = frappe.session.user;
|
||||
}
|
||||
|
||||
if (!this.creation) {
|
||||
this.creation = now;
|
||||
}
|
||||
|
||||
this.updateModified();
|
||||
}
|
||||
}
|
||||
|
||||
updateModified() {
|
||||
if (frappe.isServer) {
|
||||
let now = new Date().toISOString();
|
||||
this.modifiedBy = frappe.session.user;
|
||||
this.modified = now;
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
let data = await frappe.db.get(this.doctype, this.name);
|
||||
if (data && data.name) {
|
||||
this.syncValues(data);
|
||||
if (this.meta.isSingle) {
|
||||
this.setDefaults();
|
||||
this.castValues();
|
||||
}
|
||||
await this.loadLinks();
|
||||
} else {
|
||||
throw new frappe.errors.NotFoundError(
|
||||
`Not Found: ${this.doctype} ${this.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async loadLinks() {
|
||||
this._links = {};
|
||||
let inlineLinks = this.meta.fields.filter((df) => df.inline);
|
||||
for (let df of inlineLinks) {
|
||||
await this.loadLink(df.fieldname);
|
||||
}
|
||||
}
|
||||
|
||||
async loadLink(fieldname) {
|
||||
this._links = this._links || {};
|
||||
let df = this.meta.getField(fieldname);
|
||||
if (this[df.fieldname]) {
|
||||
this._links[df.fieldname] = await frappe.getDoc(
|
||||
df.target,
|
||||
this[df.fieldname]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getLink(fieldname) {
|
||||
return this._links ? this._links[fieldname] : null;
|
||||
}
|
||||
|
||||
syncValues(data) {
|
||||
this.clearValues();
|
||||
this.setValues(data);
|
||||
this._dirty = false;
|
||||
this.trigger('change', {
|
||||
doc: this,
|
||||
});
|
||||
}
|
||||
|
||||
clearValues() {
|
||||
let toClear = ['_dirty', '_notInserted'].concat(
|
||||
this.meta.getValidFields().map((df) => df.fieldname)
|
||||
);
|
||||
for (let key of toClear) {
|
||||
this[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
setChildIdx() {
|
||||
// renumber children
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
if (field.fieldtype === 'Table') {
|
||||
for (let i = 0; i < (this[field.fieldname] || []).length; i++) {
|
||||
this[field.fieldname][i].idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async compareWithCurrentDoc() {
|
||||
if (frappe.isServer && !this.isNew()) {
|
||||
let currentDoc = await frappe.db.get(this.doctype, this.name);
|
||||
|
||||
// check for conflict
|
||||
if (currentDoc && this.modified != currentDoc.modified) {
|
||||
throw new frappe.errors.Conflict(
|
||||
frappe._('Document {0} {1} has been modified after loading', [
|
||||
this.doctype,
|
||||
this.name,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (this.submitted && !this.meta.isSubmittable) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
frappe._('Document type {1} is not submittable', [this.doctype])
|
||||
);
|
||||
}
|
||||
|
||||
// set submit action flag
|
||||
this.flags = {};
|
||||
if (this.submitted && !currentDoc.submitted) {
|
||||
this.flags.submitAction = true;
|
||||
}
|
||||
|
||||
if (currentDoc.submitted && !this.submitted) {
|
||||
this.flags.revertAction = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyFormula(fieldname) {
|
||||
if (!this.meta.hasFormula()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let doc = this;
|
||||
let changed = false;
|
||||
|
||||
// children
|
||||
for (let tablefield of this.meta.getTableFields()) {
|
||||
let formulaFields = frappe
|
||||
.getMeta(tablefield.childtype)
|
||||
.getFormulaFields();
|
||||
if (formulaFields.length) {
|
||||
const value = this[tablefield.fieldname] || [];
|
||||
for (let row of value) {
|
||||
for (let field of formulaFields) {
|
||||
if (shouldApplyFormula(field, row)) {
|
||||
let val = await this.getValueFromFormula(field, row);
|
||||
let previousVal = row[field.fieldname];
|
||||
if (val !== undefined && previousVal !== val) {
|
||||
row[field.fieldname] = val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parent or child row
|
||||
for (let field of this.meta.getFormulaFields()) {
|
||||
if (shouldApplyFormula(field, doc)) {
|
||||
let previousVal = doc[field.fieldname];
|
||||
let val = await this.getValueFromFormula(field, doc);
|
||||
if (val !== undefined && previousVal !== val) {
|
||||
doc[field.fieldname] = val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
|
||||
function shouldApplyFormula(field, doc) {
|
||||
if (field.readOnly) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
fieldname &&
|
||||
field.formulaDependsOn &&
|
||||
field.formulaDependsOn.includes(fieldname)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!frappe.isServer || frappe.isElectron) {
|
||||
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getValueFromFormula(field, doc) {
|
||||
let value;
|
||||
|
||||
if (doc.meta.isChild) {
|
||||
value = await field.formula(doc, doc.parentdoc);
|
||||
} else {
|
||||
value = await field.formula(doc);
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('Float' === field.fieldtype) {
|
||||
value = this.round(value, field);
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Table' && Array.isArray(value)) {
|
||||
value = value.map((row) => {
|
||||
let doc = this._initChild(row, field.fieldname);
|
||||
doc.roundFloats();
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
roundFloats() {
|
||||
let fields = this.meta
|
||||
.getValidFields()
|
||||
.filter((df) => ['Float', 'Table'].includes(df.fieldtype));
|
||||
|
||||
for (let df of fields) {
|
||||
let value = this[df.fieldname];
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
// child
|
||||
if (Array.isArray(value)) {
|
||||
value.map((row) => row.roundFloats());
|
||||
continue;
|
||||
}
|
||||
// field
|
||||
let roundedValue = this.round(value, df);
|
||||
if (roundedValue && value !== roundedValue) {
|
||||
this[df.fieldname] = roundedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setName() {
|
||||
await naming.setName(this);
|
||||
}
|
||||
|
||||
async commit() {
|
||||
// re-run triggers
|
||||
this.setKeywords();
|
||||
this.setChildIdx();
|
||||
await this.applyFormula();
|
||||
await this.trigger('validate');
|
||||
}
|
||||
|
||||
async insert() {
|
||||
await this.setName();
|
||||
this.setStandardValues();
|
||||
await this.commit();
|
||||
await this.validateInsert();
|
||||
await this.trigger('beforeInsert');
|
||||
|
||||
let oldName = this.name;
|
||||
const data = await frappe.db.insert(this.doctype, this.getValidDict());
|
||||
this.syncValues(data);
|
||||
|
||||
if (oldName !== this.name) {
|
||||
frappe.removeFromCache(this.doctype, oldName);
|
||||
}
|
||||
|
||||
await this.trigger('afterInsert');
|
||||
await this.trigger('afterSave');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async update(...args) {
|
||||
if (args.length) {
|
||||
await this.set(...args);
|
||||
}
|
||||
await this.compareWithCurrentDoc();
|
||||
await this.commit();
|
||||
await this.trigger('beforeUpdate');
|
||||
|
||||
// before submit
|
||||
if (this.flags.submitAction) await this.trigger('beforeSubmit');
|
||||
if (this.flags.revertAction) await this.trigger('beforeRevert');
|
||||
|
||||
// update modifiedBy and modified
|
||||
this.updateModified();
|
||||
|
||||
const data = await frappe.db.update(this.doctype, this.getValidDict());
|
||||
this.syncValues(data);
|
||||
|
||||
await this.trigger('afterUpdate');
|
||||
await this.trigger('afterSave');
|
||||
|
||||
// after submit
|
||||
if (this.flags.submitAction) await this.trigger('afterSubmit');
|
||||
if (this.flags.revertAction) await this.trigger('afterRevert');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async insertOrUpdate() {
|
||||
if (this._notInserted) {
|
||||
return await this.insert();
|
||||
} else {
|
||||
return await this.update();
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.trigger('beforeDelete');
|
||||
await frappe.db.delete(this.doctype, this.name);
|
||||
await this.trigger('afterDelete');
|
||||
}
|
||||
|
||||
async submitOrRevert(isSubmit) {
|
||||
const wasSubmitted = this.submitted;
|
||||
this.submitted = isSubmit;
|
||||
try {
|
||||
await this.update();
|
||||
} catch (e) {
|
||||
this.submitted = wasSubmitted;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.cancelled = 0;
|
||||
await this.submitOrRevert(1);
|
||||
}
|
||||
|
||||
async revert() {
|
||||
await this.submitOrRevert(0);
|
||||
}
|
||||
|
||||
async rename(newName) {
|
||||
await this.trigger('beforeRename');
|
||||
await frappe.db.rename(this.doctype, this.name, newName);
|
||||
this.name = newName;
|
||||
await this.trigger('afterRename');
|
||||
}
|
||||
|
||||
// trigger methods on the class if they match
|
||||
// with the trigger name
|
||||
async trigger(event, params) {
|
||||
if (this[event]) {
|
||||
await this[event](params);
|
||||
}
|
||||
await super.trigger(event, params);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
getSum(tablefield, childfield, convertToFloat = true) {
|
||||
const sum = (this[tablefield] || [])
|
||||
.map((d) => {
|
||||
const value = d[childfield] ?? 0;
|
||||
if (!isPesa(value)) {
|
||||
try {
|
||||
return frappe.pesa(value);
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.reduce((a, b) => a.add(b), frappe.pesa(0));
|
||||
|
||||
if (convertToFloat) {
|
||||
return sum.float;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
getFrom(doctype, name, fieldname) {
|
||||
if (!name) return '';
|
||||
return frappe.db.getCachedValue(doctype, name, fieldname);
|
||||
}
|
||||
|
||||
round(value, df = null) {
|
||||
if (typeof df === 'string') {
|
||||
df = this.meta.getField(df);
|
||||
}
|
||||
const precision =
|
||||
frappe.SystemSettings.internalPrecision ?? DEFAULT_INTERNAL_PRECISION;
|
||||
return frappe.pesa(value).clip(precision).float;
|
||||
}
|
||||
|
||||
isNew() {
|
||||
return this._notInserted;
|
||||
}
|
||||
|
||||
getFieldMetaMap() {
|
||||
return this.meta.fields.reduce((obj, meta) => {
|
||||
obj[meta.fieldname] = meta;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
function getPreDefaultValues(fieldtype) {
|
||||
switch (fieldtype) {
|
||||
case 'Table':
|
||||
return [];
|
||||
case 'Currency':
|
||||
return frappe.pesa(0.0);
|
||||
case 'Int':
|
||||
case 'Float':
|
||||
return 0;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
114
frappe/model/index.js
Normal file
114
frappe/model/index.js
Normal file
@ -0,0 +1,114 @@
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
|
||||
module.exports = {
|
||||
extend: (base, target, options = {}) => {
|
||||
base = cloneDeep(base);
|
||||
const fieldsToMerge = (target.fields || []).map(df => df.fieldname);
|
||||
const fieldsToRemove = options.skipFields || [];
|
||||
const overrideProps = options.overrideProps || [];
|
||||
for (let prop of overrideProps) {
|
||||
if (base.hasOwnProperty(prop)) {
|
||||
delete base[prop];
|
||||
}
|
||||
}
|
||||
|
||||
let mergeFields = (baseFields, targetFields) => {
|
||||
let fields = cloneDeep(baseFields);
|
||||
fields = fields
|
||||
.filter(df => !fieldsToRemove.includes(df.fieldname))
|
||||
.map(df => {
|
||||
if (fieldsToMerge.includes(df.fieldname)) {
|
||||
let copy = cloneDeep(df);
|
||||
return Object.assign(
|
||||
copy,
|
||||
targetFields.find(tdf => tdf.fieldname === df.fieldname)
|
||||
);
|
||||
}
|
||||
return df;
|
||||
});
|
||||
let fieldsAdded = fields.map(df => df.fieldname);
|
||||
let fieldsToAdd = targetFields.filter(
|
||||
df => !fieldsAdded.includes(df.fieldname)
|
||||
);
|
||||
return fields.concat(fieldsToAdd);
|
||||
};
|
||||
|
||||
let fields = mergeFields(base.fields, target.fields || []);
|
||||
let out = Object.assign(base, target);
|
||||
out.fields = fields;
|
||||
|
||||
return out;
|
||||
},
|
||||
commonFields: [
|
||||
{
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
submittableFields: [
|
||||
{
|
||||
fieldname: 'submitted',
|
||||
fieldtype: 'Check',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
parentFields: [
|
||||
{
|
||||
fieldname: 'owner',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'modifiedBy',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'creation',
|
||||
fieldtype: 'Datetime',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'modified',
|
||||
fieldtype: 'Datetime',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'keywords',
|
||||
fieldtype: 'Text'
|
||||
}
|
||||
],
|
||||
childFields: [
|
||||
{
|
||||
fieldname: 'idx',
|
||||
fieldtype: 'Int',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parent',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parenttype',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parentfield',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
treeFields: [
|
||||
{
|
||||
fieldname: 'lft',
|
||||
fieldtype: 'Int'
|
||||
},
|
||||
{
|
||||
fieldname: 'rgt',
|
||||
fieldtype: 'Int'
|
||||
}
|
||||
]
|
||||
};
|
326
frappe/model/meta.js
Normal file
326
frappe/model/meta.js
Normal file
@ -0,0 +1,326 @@
|
||||
const BaseDocument = require('./document');
|
||||
const frappe = require('frappe');
|
||||
const model = require('./index');
|
||||
const { indicators: indicatorColor } = require('../../src/colors');
|
||||
|
||||
module.exports = class BaseMeta extends BaseDocument {
|
||||
constructor(data) {
|
||||
if (data.basedOn) {
|
||||
let config = frappe.models[data.basedOn];
|
||||
Object.assign(data, config, {
|
||||
name: data.name,
|
||||
label: data.label,
|
||||
filters: data.filters
|
||||
});
|
||||
}
|
||||
super(data);
|
||||
this.setDefaultIndicators();
|
||||
if (this.setupMeta) {
|
||||
this.setupMeta();
|
||||
}
|
||||
if (!this.titleField) {
|
||||
this.titleField = 'name';
|
||||
}
|
||||
}
|
||||
|
||||
setValues(data) {
|
||||
Object.assign(this, data);
|
||||
this.processFields();
|
||||
}
|
||||
|
||||
processFields() {
|
||||
// add name field
|
||||
if (!this.fields.find(df => df.fieldname === 'name') && !this.isSingle) {
|
||||
this.fields = [
|
||||
{
|
||||
label: frappe._('ID'),
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
readOnly: 1
|
||||
}
|
||||
].concat(this.fields);
|
||||
}
|
||||
|
||||
this.fields = this.fields.map(df => {
|
||||
// name field is always required
|
||||
if (df.fieldname === 'name') {
|
||||
df.required = 1;
|
||||
}
|
||||
|
||||
return df;
|
||||
});
|
||||
}
|
||||
|
||||
hasField(fieldname) {
|
||||
return this.getField(fieldname) ? true : false;
|
||||
}
|
||||
|
||||
getField(fieldname) {
|
||||
if (!this._field_map) {
|
||||
this._field_map = {};
|
||||
for (let field of this.fields) {
|
||||
this._field_map[field.fieldname] = field;
|
||||
}
|
||||
}
|
||||
return this._field_map[fieldname];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fields filtered by filters
|
||||
* @param {Object} filters
|
||||
*
|
||||
* Usage:
|
||||
* meta = frappe.getMeta('ToDo')
|
||||
* dataFields = meta.getFieldsWith({ fieldtype: 'Data' })
|
||||
*/
|
||||
getFieldsWith(filters) {
|
||||
return this.fields.filter(df => {
|
||||
let match = true;
|
||||
for (const key in filters) {
|
||||
const value = filters[key];
|
||||
match = df[key] === value;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
getLabel(fieldname) {
|
||||
let df = this.getField(fieldname);
|
||||
return df.getLabel || df.label;
|
||||
}
|
||||
|
||||
getTableFields() {
|
||||
if (this._tableFields === undefined) {
|
||||
this._tableFields = this.fields.filter(
|
||||
field => field.fieldtype === 'Table'
|
||||
);
|
||||
}
|
||||
return this._tableFields;
|
||||
}
|
||||
|
||||
getFormulaFields() {
|
||||
if (this._formulaFields === undefined) {
|
||||
this._formulaFields = this.fields.filter(field => field.formula);
|
||||
}
|
||||
return this._formulaFields;
|
||||
}
|
||||
|
||||
hasFormula() {
|
||||
if (this._hasFormula === undefined) {
|
||||
this._hasFormula = false;
|
||||
if (this.getFormulaFields().length) {
|
||||
this._hasFormula = true;
|
||||
} else {
|
||||
for (let tablefield of this.getTableFields()) {
|
||||
if (frappe.getMeta(tablefield.childtype).getFormulaFields().length) {
|
||||
this._hasFormula = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._hasFormula;
|
||||
}
|
||||
|
||||
getBaseDocType() {
|
||||
return this.basedOn || this.name;
|
||||
}
|
||||
|
||||
async set(fieldname, value) {
|
||||
this[fieldname] = value;
|
||||
await this.trigger(fieldname);
|
||||
}
|
||||
|
||||
get(fieldname) {
|
||||
return this[fieldname];
|
||||
}
|
||||
|
||||
getValidFields({ withChildren = true } = {}) {
|
||||
if (!this._validFields) {
|
||||
this._validFields = [];
|
||||
this._validFieldsWithChildren = [];
|
||||
|
||||
const _add = field => {
|
||||
this._validFields.push(field);
|
||||
this._validFieldsWithChildren.push(field);
|
||||
};
|
||||
|
||||
// fields validation
|
||||
this.fields.forEach((df, i) => {
|
||||
if (!df.fieldname) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
`DocType ${this.name}: "fieldname" is required for field at index ${i}`
|
||||
);
|
||||
}
|
||||
if (!df.fieldtype) {
|
||||
throw new frappe.errors.ValidationError(
|
||||
`DocType ${this.name}: "fieldtype" is required for field "${df.fieldname}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const doctypeFields = this.fields.map(field => field.fieldname);
|
||||
|
||||
// standard fields
|
||||
for (let field of model.commonFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctypeFields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSubmittable) {
|
||||
_add({
|
||||
fieldtype: 'Check',
|
||||
fieldname: 'submitted',
|
||||
label: frappe._('Submitted')
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isChild) {
|
||||
// child fields
|
||||
for (let field of model.childFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctypeFields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// parent fields
|
||||
for (let field of model.parentFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctypeFields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isTree) {
|
||||
// tree fields
|
||||
for (let field of model.treeFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctypeFields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doctype fields
|
||||
for (let field of this.fields) {
|
||||
let include = frappe.db.typeMap[field.fieldtype];
|
||||
|
||||
if (include) {
|
||||
_add(field);
|
||||
}
|
||||
|
||||
// include tables if (withChildren = True)
|
||||
if (!include && field.fieldtype === 'Table') {
|
||||
this._validFieldsWithChildren.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (withChildren) {
|
||||
return this._validFieldsWithChildren;
|
||||
} else {
|
||||
return this._validFields;
|
||||
}
|
||||
}
|
||||
|
||||
getKeywordFields() {
|
||||
if (!this._keywordFields) {
|
||||
this._keywordFields = this.keywordFields;
|
||||
if (!(this._keywordFields && this._keywordFields.length && this.fields)) {
|
||||
this._keywordFields = this.fields
|
||||
.filter(field => field.fieldtype !== 'Table' && field.required)
|
||||
.map(field => field.fieldname);
|
||||
}
|
||||
if (!(this._keywordFields && this._keywordFields.length)) {
|
||||
this._keywordFields = ['name'];
|
||||
}
|
||||
}
|
||||
return this._keywordFields;
|
||||
}
|
||||
|
||||
getQuickEditFields() {
|
||||
if (this.quickEditFields) {
|
||||
return this.quickEditFields.map(fieldname => this.getField(fieldname));
|
||||
}
|
||||
return this.getFieldsWith({ required: 1 });
|
||||
}
|
||||
|
||||
validateSelect(field, value) {
|
||||
let options = field.options;
|
||||
if (!options) return;
|
||||
if (!field.required && value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let validValues = options;
|
||||
|
||||
if (typeof options === 'string') {
|
||||
// values given as string
|
||||
validValues = options.split('\n');
|
||||
}
|
||||
if (typeof options[0] === 'object') {
|
||||
// options as array of {label, value} pairs
|
||||
validValues = options.map(o => o.value);
|
||||
}
|
||||
if (!validValues.includes(value)) {
|
||||
throw new frappe.errors.ValueError(
|
||||
// prettier-ignore
|
||||
`DocType ${this.name}: Invalid value "${value}" for "${field.label}". Must be one of ${options.join(', ')}`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async trigger(event, params = {}) {
|
||||
Object.assign(params, {
|
||||
doc: this,
|
||||
name: event
|
||||
});
|
||||
|
||||
await super.trigger(event, params);
|
||||
}
|
||||
|
||||
setDefaultIndicators() {
|
||||
if (!this.indicators) {
|
||||
if (this.isSubmittable) {
|
||||
this.indicators = {
|
||||
key: 'submitted',
|
||||
colors: {
|
||||
0: indicatorColor.GRAY,
|
||||
1: indicatorColor.BLUE
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getIndicatorColor(doc) {
|
||||
if (frappe.isDirty(this.name, doc.name)) {
|
||||
return indicatorColor.ORANGE;
|
||||
} else {
|
||||
if (this.indicators) {
|
||||
let value = doc[this.indicators.key];
|
||||
if (value) {
|
||||
return this.indicators.colors[value] || indicatorColor.GRAY;
|
||||
} else {
|
||||
return indicatorColor.GRAY;
|
||||
}
|
||||
} else {
|
||||
return indicatorColor.GRAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
92
frappe/model/naming.js
Normal file
92
frappe/model/naming.js
Normal file
@ -0,0 +1,92 @@
|
||||
const frappe = require('frappe');
|
||||
const { getRandomString } = require('frappe/utils');
|
||||
|
||||
module.exports = {
|
||||
async setName(doc) {
|
||||
if (frappe.isServer) {
|
||||
// if is server, always name again if autoincrement or other
|
||||
if (doc.meta.naming === 'autoincrement') {
|
||||
doc.name = await this.getNextId(doc.doctype);
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc.meta.settings) {
|
||||
const numberSeries = (await doc.getSettings()).numberSeries;
|
||||
if (numberSeries) {
|
||||
doc.name = await this.getSeriesNext(numberSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// name === doctype for Single
|
||||
if (doc.meta.isSingle) {
|
||||
doc.name = doc.meta.name;
|
||||
return;
|
||||
}
|
||||
|
||||
// assign a random name by default
|
||||
// override doc to set a name
|
||||
if (!doc.name) {
|
||||
doc.name = getRandomString();
|
||||
}
|
||||
},
|
||||
|
||||
async getNextId(doctype) {
|
||||
// get the last inserted row
|
||||
let lastInserted = await this.getLastInserted(doctype);
|
||||
let name = 1;
|
||||
if (lastInserted) {
|
||||
let lastNumber = parseInt(lastInserted.name);
|
||||
if (isNaN(lastNumber)) lastNumber = 0;
|
||||
name = lastNumber + 1;
|
||||
}
|
||||
return (name + '').padStart(9, '0');
|
||||
},
|
||||
|
||||
async getLastInserted(doctype) {
|
||||
const lastInserted = await frappe.db.getAll({
|
||||
doctype: doctype,
|
||||
fields: ['name'],
|
||||
limit: 1,
|
||||
order_by: 'creation',
|
||||
order: 'desc',
|
||||
});
|
||||
return lastInserted && lastInserted.length ? lastInserted[0] : null;
|
||||
},
|
||||
|
||||
async getSeriesNext(prefix) {
|
||||
let series;
|
||||
try {
|
||||
series = await frappe.getDoc('NumberSeries', prefix);
|
||||
} catch (e) {
|
||||
if (!e.statusCode || e.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
await this.createNumberSeries(prefix);
|
||||
series = await frappe.getDoc('NumberSeries', prefix);
|
||||
}
|
||||
let next = await series.next();
|
||||
return prefix + next;
|
||||
},
|
||||
|
||||
async createNumberSeries(prefix, setting, start = 1000) {
|
||||
if (!(await frappe.db.exists('NumberSeries', prefix))) {
|
||||
const series = frappe.newDoc({
|
||||
doctype: 'NumberSeries',
|
||||
name: prefix,
|
||||
current: start,
|
||||
});
|
||||
await series.insert();
|
||||
|
||||
if (setting) {
|
||||
const settingDoc = await frappe.getSingle(setting);
|
||||
settingDoc.numberSeries = series.name;
|
||||
await settingDoc.update();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
26
frappe/model/runPatches.js
Normal file
26
frappe/model/runPatches.js
Normal file
@ -0,0 +1,26 @@
|
||||
const frappe = require('frappe');
|
||||
|
||||
module.exports = async function runPatches(patchList) {
|
||||
const patchesAlreadyRun = (
|
||||
await frappe.db.knex('PatchRun').select('name')
|
||||
).map(({ name }) => name);
|
||||
|
||||
for (let patch of patchList) {
|
||||
if (patchesAlreadyRun.includes(patch.patchName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await runPatch(patch);
|
||||
}
|
||||
};
|
||||
|
||||
async function runPatch({ patchName, patchFunction }) {
|
||||
try {
|
||||
await patchFunction();
|
||||
const patchRun = frappe.getNewDoc('PatchRun');
|
||||
patchRun.name = patchName;
|
||||
await patchRun.insert();
|
||||
} catch (error) {
|
||||
console.error(`could not run ${patchName}`, error);
|
||||
}
|
||||
}
|
67
frappe/models/doctype/File/File.js
Normal file
67
frappe/models/doctype/File/File.js
Normal file
@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
name: 'File',
|
||||
doctype: 'DocType',
|
||||
isSingle: 0,
|
||||
keywordFields: [
|
||||
'name',
|
||||
'filename'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'name',
|
||||
label: 'File Path',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'filename',
|
||||
label: 'File Name',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'mimetype',
|
||||
label: 'MIME Type',
|
||||
fieldtype: 'Data',
|
||||
},
|
||||
{
|
||||
fieldname: 'size',
|
||||
label: 'File Size',
|
||||
fieldtype: 'Int',
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceDoctype',
|
||||
label: 'Reference DocType',
|
||||
fieldtype: 'Data',
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceName',
|
||||
label: 'Reference Name',
|
||||
fieldtype: 'Data',
|
||||
},
|
||||
{
|
||||
fieldname: 'referenceField',
|
||||
label: 'Reference Field',
|
||||
fieldtype: 'Data',
|
||||
},
|
||||
],
|
||||
layout: [
|
||||
{
|
||||
columns: [
|
||||
{ fields: ['filename'] },
|
||||
]
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{ fields: ['mimetype'] },
|
||||
{ fields: ['size'] },
|
||||
]
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{ fields: ['referenceDoctype'] },
|
||||
{ fields: ['referenceName'] },
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
22
frappe/models/doctype/NumberSeries/NumberSeries.js
Normal file
22
frappe/models/doctype/NumberSeries/NumberSeries.js
Normal file
@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
"name": "NumberSeries",
|
||||
"documentClass": require('./NumberSeriesDocument.js'),
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 0,
|
||||
"keywordFields": [],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"label": "Prefix",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "current",
|
||||
"label": "Current",
|
||||
"fieldtype": "Int",
|
||||
"required": 1
|
||||
}
|
||||
]
|
||||
}
|
15
frappe/models/doctype/NumberSeries/NumberSeriesDocument.js
Normal file
15
frappe/models/doctype/NumberSeries/NumberSeriesDocument.js
Normal file
@ -0,0 +1,15 @@
|
||||
const BaseDocument = require('frappe/model/document');
|
||||
|
||||
module.exports = class NumberSeries extends BaseDocument {
|
||||
validate() {
|
||||
if (this.current === null || this.current === undefined) {
|
||||
this.current = 0;
|
||||
}
|
||||
}
|
||||
async next() {
|
||||
this.validate();
|
||||
this.current++;
|
||||
await this.update();
|
||||
return this.current;
|
||||
}
|
||||
};
|
10
frappe/models/doctype/PatchRun/PatchRun.js
Normal file
10
frappe/models/doctype/PatchRun/PatchRun.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
name: 'PatchRun',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
label: 'Name'
|
||||
}
|
||||
]
|
||||
};
|
31
frappe/models/doctype/PrintFormat/PrintFormat.js
Normal file
31
frappe/models/doctype/PrintFormat/PrintFormat.js
Normal file
@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
name: "PrintFormat",
|
||||
label: "Print Format",
|
||||
doctype: "DocType",
|
||||
isSingle: 0,
|
||||
isChild: 0,
|
||||
keywordFields: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "name",
|
||||
label: "Name",
|
||||
fieldtype: "Data",
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: "for",
|
||||
label: "For",
|
||||
fieldtype: "Data",
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: "template",
|
||||
label: "Template",
|
||||
fieldtype: "Code",
|
||||
required: 1,
|
||||
options: {
|
||||
mode: 'text/html'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
15
frappe/models/doctype/Role/Role.js
Normal file
15
frappe/models/doctype/Role/Role.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
"name": "Role",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 0,
|
||||
"keywordFields": [],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"label": "Name",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
}
|
||||
]
|
||||
}
|
21
frappe/models/doctype/Session/Session.js
Normal file
21
frappe/models/doctype/Session/Session.js
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
"name": "Session",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 0,
|
||||
"keywordFields": [],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "username",
|
||||
"label": "Username",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "password",
|
||||
"label": "Password",
|
||||
"fieldtype": "Password",
|
||||
"required": 1
|
||||
}
|
||||
]
|
||||
}
|
27
frappe/models/doctype/SingleValue/SingleValue.js
Normal file
27
frappe/models/doctype/SingleValue/SingleValue.js
Normal file
@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
"name": "SingleValue",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 0,
|
||||
"keywordFields": [],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "parent",
|
||||
"label": "Parent",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"label": "Fieldname",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "value",
|
||||
"label": "Value",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
}
|
||||
]
|
||||
}
|
118
frappe/models/doctype/SystemSettings/SystemSettings.js
Normal file
118
frappe/models/doctype/SystemSettings/SystemSettings.js
Normal file
@ -0,0 +1,118 @@
|
||||
const { DateTime } = require('luxon');
|
||||
const { _ } = require('frappe/utils');
|
||||
const {
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
DEFAULT_INTERNAL_PRECISION,
|
||||
DEFAULT_LOCALE,
|
||||
} = require('../../../utils/consts');
|
||||
|
||||
let dateFormatOptions = (() => {
|
||||
let formats = [
|
||||
'dd/MM/yyyy',
|
||||
'MM/dd/yyyy',
|
||||
'dd-MM-yyyy',
|
||||
'MM-dd-yyyy',
|
||||
'yyyy-MM-dd',
|
||||
'd MMM, y',
|
||||
'MMM d, y',
|
||||
];
|
||||
|
||||
let today = DateTime.local();
|
||||
|
||||
return formats.map((format) => {
|
||||
return {
|
||||
label: today.toFormat(format),
|
||||
value: format,
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
name: 'SystemSettings',
|
||||
label: 'System Settings',
|
||||
doctype: 'DocType',
|
||||
isSingle: 1,
|
||||
isChild: 0,
|
||||
keywordFields: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'dateFormat',
|
||||
label: 'Date Format',
|
||||
fieldtype: 'Select',
|
||||
options: dateFormatOptions,
|
||||
default: 'MMM d, y',
|
||||
required: 1,
|
||||
description: _('Sets the app-wide date display format.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'locale',
|
||||
label: 'Locale',
|
||||
fieldtype: 'Data',
|
||||
default: DEFAULT_LOCALE,
|
||||
description: _('Set the local code, this is used for number formatting.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'displayPrecision',
|
||||
label: 'Display Precision',
|
||||
fieldtype: 'Int',
|
||||
default: DEFAULT_DISPLAY_PRECISION,
|
||||
required: 1,
|
||||
minValue: 0,
|
||||
maxValue: 9,
|
||||
validate(value, doc) {
|
||||
if (value >= 0 && value <= 9) {
|
||||
return;
|
||||
}
|
||||
throw new frappe.errors.ValidationError(
|
||||
_('Display Precision should have a value between 0 and 9.')
|
||||
);
|
||||
},
|
||||
description: _('Sets how many digits are shown after the decimal point.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'internalPrecision',
|
||||
label: 'Internal Precision',
|
||||
fieldtype: 'Int',
|
||||
minValue: 0,
|
||||
default: DEFAULT_INTERNAL_PRECISION,
|
||||
description: _(
|
||||
'Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.'
|
||||
),
|
||||
},
|
||||
{
|
||||
fieldname: 'hideGetStarted',
|
||||
label: 'Hide Get Started',
|
||||
fieldtype: 'Check',
|
||||
default: 0,
|
||||
description: _(
|
||||
'Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.'
|
||||
),
|
||||
},
|
||||
{
|
||||
fieldname: 'autoUpdate',
|
||||
label: 'Auto Update',
|
||||
fieldtype: 'Check',
|
||||
default: 1,
|
||||
description: _(
|
||||
'Automatically checks for updates and download them if available. The update will be applied after you restart the app.'
|
||||
),
|
||||
},
|
||||
{
|
||||
fieldname: 'autoReportErrors',
|
||||
label: 'Auto Report Errors',
|
||||
fieldtype: 'Check',
|
||||
default: 0,
|
||||
description: _(
|
||||
'Automatically report all errors. User will still be notified when an error pops up.'
|
||||
),
|
||||
},
|
||||
],
|
||||
quickEditFields: [
|
||||
'dateFormat',
|
||||
'locale',
|
||||
'displayPrecision',
|
||||
'hideGetStarted',
|
||||
'autoUpdate',
|
||||
'autoReportErrors',
|
||||
],
|
||||
};
|
61
frappe/models/doctype/ToDo/ToDo.js
Normal file
61
frappe/models/doctype/ToDo/ToDo.js
Normal file
@ -0,0 +1,61 @@
|
||||
const { indicators } = require('../../../../src/colors');
|
||||
const { BLUE, GREEN } = indicators;
|
||||
|
||||
module.exports = {
|
||||
name: 'ToDo',
|
||||
label: 'To Do',
|
||||
naming: 'autoincrement',
|
||||
isSingle: 0,
|
||||
keywordFields: ['subject', 'description'],
|
||||
titleField: 'subject',
|
||||
indicators: {
|
||||
key: 'status',
|
||||
colors: {
|
||||
Open: BLUE,
|
||||
Closed: GREEN,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'subject',
|
||||
label: 'Subject',
|
||||
placeholder: 'Subject',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'status',
|
||||
label: 'Status',
|
||||
fieldtype: 'Select',
|
||||
options: ['Open', 'Closed'],
|
||||
default: 'Open',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'description',
|
||||
label: 'Description',
|
||||
fieldtype: 'Text',
|
||||
},
|
||||
],
|
||||
|
||||
quickEditFields: ['status', 'description'],
|
||||
|
||||
actions: [
|
||||
{
|
||||
label: 'Close',
|
||||
condition: (doc) => doc.status !== 'Closed',
|
||||
action: async (doc) => {
|
||||
await doc.set('status', 'Closed');
|
||||
await doc.update();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Re-Open',
|
||||
condition: (doc) => doc.status !== 'Open',
|
||||
action: async (doc) => {
|
||||
await doc.set('status', 'Open');
|
||||
await doc.update();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
7
frappe/models/doctype/ToDo/ToDoList.js
Normal file
7
frappe/models/doctype/ToDo/ToDoList.js
Normal file
@ -0,0 +1,7 @@
|
||||
const BaseList = require('frappe/client/view/list');
|
||||
|
||||
module.exports = class ToDoList extends BaseList {
|
||||
getFields(list) {
|
||||
return ['name', 'subject', 'status'];
|
||||
}
|
||||
};
|
43
frappe/models/doctype/User/User.js
Normal file
43
frappe/models/doctype/User/User.js
Normal file
@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
"name": "User",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 0,
|
||||
"keywordFields": [
|
||||
"name",
|
||||
"fullName"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"label": "Email",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "password",
|
||||
"label": "Password",
|
||||
"fieldtype": "Password",
|
||||
"required": 1,
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "fullName",
|
||||
"label": "Full Name",
|
||||
"fieldtype": "Data",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "roles",
|
||||
"label": "Roles",
|
||||
"fieldtype": "Table",
|
||||
"childtype": "UserRole"
|
||||
},
|
||||
{
|
||||
"fieldname": "userId",
|
||||
"label": "User ID",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
}
|
||||
]
|
||||
}
|
15
frappe/models/doctype/UserRole/UserRole.js
Normal file
15
frappe/models/doctype/UserRole/UserRole.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
"name": "UserRole",
|
||||
"doctype": "DocType",
|
||||
"isSingle": 0,
|
||||
"isChild": 1,
|
||||
"keywordFields": [],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "role",
|
||||
"label": "Role",
|
||||
"fieldtype": "Link",
|
||||
"target": "Role"
|
||||
}
|
||||
]
|
||||
}
|
13
frappe/models/index.js
Normal file
13
frappe/models/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
NumberSeries: require('./doctype/NumberSeries/NumberSeries.js'),
|
||||
PrintFormat: require('./doctype/PrintFormat/PrintFormat.js'),
|
||||
Role: require('./doctype/Role/Role.js'),
|
||||
Session: require('./doctype/Session/Session.js'),
|
||||
SingleValue: require('./doctype/SingleValue/SingleValue.js'),
|
||||
SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'),
|
||||
ToDo: require('./doctype/ToDo/ToDo.js'),
|
||||
User: require('./doctype/User/User.js'),
|
||||
UserRole: require('./doctype/UserRole/UserRole.js'),
|
||||
File: require('./doctype/File/File.js'),
|
||||
PatchRun: require('./doctype/PatchRun/PatchRun.js')
|
||||
};
|
41
frappe/utils/cacheManager.js
Normal file
41
frappe/utils/cacheManager.js
Normal file
@ -0,0 +1,41 @@
|
||||
class CacheManager {
|
||||
constructor() {
|
||||
this.keyValueCache = {};
|
||||
this.hashCache = {};
|
||||
}
|
||||
|
||||
getValue(key) {
|
||||
return this.keyValueCache[key];
|
||||
}
|
||||
|
||||
setValue(key, value) {
|
||||
this.keyValueCache[key] = value;
|
||||
}
|
||||
|
||||
clearValue(key) {
|
||||
this.keyValueCache[key] = null;
|
||||
}
|
||||
|
||||
hget(hashName, key) {
|
||||
return (this.hashCache[hashName] || {})[key];
|
||||
}
|
||||
|
||||
hset(hashName, key, value) {
|
||||
this.hashCache[hashName] = this.hashCache[hashName] || {};
|
||||
this.hashCache[hashName][key] = value;
|
||||
}
|
||||
|
||||
hclear(hashName, key) {
|
||||
if (key) {
|
||||
(this.hashCache[hashName] || {})[key] = null;
|
||||
} else {
|
||||
this.hashCache[hashName] = {};
|
||||
}
|
||||
}
|
||||
|
||||
hexists(hashName) {
|
||||
return this.hashCache[hashName] != null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CacheManager;
|
3
frappe/utils/consts.js
Normal file
3
frappe/utils/consts.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const DEFAULT_INTERNAL_PRECISION = 11;
|
||||
export const DEFAULT_DISPLAY_PRECISION = 2;
|
||||
export const DEFAULT_LOCALE = 'en-IN';
|
117
frappe/utils/format.js
Normal file
117
frappe/utils/format.js
Normal file
@ -0,0 +1,117 @@
|
||||
const luxon = require('luxon');
|
||||
const frappe = require('frappe');
|
||||
const { DEFAULT_DISPLAY_PRECISION, DEFAULT_LOCALE } = require('./consts');
|
||||
|
||||
module.exports = {
|
||||
format(value, df, doc) {
|
||||
if (!df) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof df === 'string') {
|
||||
df = { fieldtype: df };
|
||||
}
|
||||
|
||||
if (df.fieldtype === 'Currency') {
|
||||
const currency = getCurrency(df, doc);
|
||||
value = formatCurrency(value, currency);
|
||||
} else if (df.fieldtype === 'Date') {
|
||||
let dateFormat;
|
||||
if (!frappe.SystemSettings) {
|
||||
dateFormat = 'yyyy-MM-dd';
|
||||
} else {
|
||||
dateFormat = frappe.SystemSettings.dateFormat;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// ISO String
|
||||
value = luxon.DateTime.fromISO(value);
|
||||
} else if (Object.prototype.toString.call(value) === '[object Date]') {
|
||||
// JS Date
|
||||
value = luxon.DateTime.fromJSDate(value);
|
||||
}
|
||||
|
||||
value = value.toFormat(dateFormat);
|
||||
if (value === 'Invalid DateTime') {
|
||||
value = '';
|
||||
}
|
||||
} else if (df.fieldtype === 'Check') {
|
||||
typeof parseInt(value) === 'number'
|
||||
? (value = parseInt(value))
|
||||
: (value = Boolean(value));
|
||||
} else {
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
} else {
|
||||
value = value + '';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
};
|
||||
|
||||
function formatCurrency(value, currency) {
|
||||
let valueString;
|
||||
try {
|
||||
valueString = formatNumber(value);
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}', type: ${typeof value}`;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const currencySymbol = frappe.currencySymbols[currency];
|
||||
if (currencySymbol) {
|
||||
return currencySymbol + ' ' + valueString;
|
||||
}
|
||||
|
||||
return valueString;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const numberFormatter = getNumberFormatter();
|
||||
if (typeof value === 'number') {
|
||||
return numberFormatter.format(value);
|
||||
}
|
||||
|
||||
if (value.round) {
|
||||
return numberFormatter.format(value.round());
|
||||
}
|
||||
|
||||
const formattedNumber = numberFormatter.format(value);
|
||||
if (formattedNumber === 'NaN') {
|
||||
throw Error(
|
||||
`invalid value passed to formatNumber: '${value}' of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
function getNumberFormatter() {
|
||||
if (frappe.currencyFormatter) {
|
||||
return frappe.currencyFormatter;
|
||||
}
|
||||
|
||||
const locale = frappe.SystemSettings.locale ?? DEFAULT_LOCALE;
|
||||
const display =
|
||||
frappe.SystemSettings.displayPrecision ?? DEFAULT_DISPLAY_PRECISION;
|
||||
|
||||
return (frappe.currencyFormatter = Intl.NumberFormat(locale, {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: display,
|
||||
}));
|
||||
}
|
||||
|
||||
function getCurrency(df, doc) {
|
||||
if (!(doc && df.getCurrency)) {
|
||||
return df.currency || frappe.AccountingSettings.currency || '';
|
||||
}
|
||||
|
||||
if (doc.meta && doc.meta.isChild) {
|
||||
return df.getCurrency(doc, doc.parentdoc);
|
||||
}
|
||||
|
||||
return df.getCurrency(doc);
|
||||
}
|
103
frappe/utils/index.js
Normal file
103
frappe/utils/index.js
Normal file
@ -0,0 +1,103 @@
|
||||
const { pesa } = require('pesa');
|
||||
const { T, t } = require('./translation');
|
||||
|
||||
Array.prototype.equals = function (array) {
|
||||
return (
|
||||
this.length == array.length &&
|
||||
this.every(function (item, i) {
|
||||
return item == array[i];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function slug(str) {
|
||||
return str
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
|
||||
return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
function getRandomString() {
|
||||
return Math.random().toString(36).substr(3);
|
||||
}
|
||||
|
||||
async function sleep(seconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function getQueryString(params) {
|
||||
if (!params) return '';
|
||||
let parts = [];
|
||||
for (let key in params) {
|
||||
if (key != null && params[key] != null) {
|
||||
parts.push(
|
||||
encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
|
||||
);
|
||||
}
|
||||
}
|
||||
return parts.join('&');
|
||||
}
|
||||
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) =>
|
||||
Promise.resolve(fn(req, res, next)).catch((err) => {
|
||||
console.log(err);
|
||||
// handle error
|
||||
res.status(err.statusCode || 500).send({ error: err.message });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array from 0 to n - 1
|
||||
* @param {Number} n
|
||||
*/
|
||||
function range(n) {
|
||||
return Array(n)
|
||||
.fill()
|
||||
.map((_, i) => i);
|
||||
}
|
||||
|
||||
function unique(list, key = (it) => it) {
|
||||
var seen = {};
|
||||
return list.filter((item) => {
|
||||
var k = key(item);
|
||||
return seen.hasOwnProperty(k) ? false : (seen[k] = true);
|
||||
});
|
||||
}
|
||||
|
||||
function getDuplicates(array) {
|
||||
let duplicates = [];
|
||||
for (let i in array) {
|
||||
let previous = array[i - 1];
|
||||
let current = array[i];
|
||||
|
||||
if (current === previous) {
|
||||
if (!duplicates.includes(current)) {
|
||||
duplicates.push(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
function isPesa(value) {
|
||||
return value instanceof pesa().constructor;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_: t,
|
||||
t,
|
||||
T,
|
||||
slug,
|
||||
getRandomString,
|
||||
sleep,
|
||||
getQueryString,
|
||||
asyncHandler,
|
||||
range,
|
||||
unique,
|
||||
getDuplicates,
|
||||
isPesa,
|
||||
};
|
1
frappe/utils/noop.js
Normal file
1
frappe/utils/noop.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = function () { return function () {}; };
|
128
frappe/utils/observable.js
Normal file
128
frappe/utils/observable.js
Normal file
@ -0,0 +1,128 @@
|
||||
module.exports = class Observable {
|
||||
constructor() {
|
||||
this._observable = {
|
||||
isHot: {},
|
||||
eventQueue: {},
|
||||
listeners: {},
|
||||
onceListeners: {}
|
||||
}
|
||||
}
|
||||
|
||||
// getter, setter stubs, so Observable can be used as a simple Document
|
||||
get(key) {
|
||||
return this[key];
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this[key] = value;
|
||||
this.trigger('change', {
|
||||
doc: this,
|
||||
fieldname: key
|
||||
});
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
this._addListener('listeners', event, listener);
|
||||
if (this._observable.socketClient) {
|
||||
this._observable.socketClient.on(event, listener);
|
||||
}
|
||||
}
|
||||
|
||||
// remove listener
|
||||
off(event, listener) {
|
||||
for (let type of ['listeners', 'onceListeners']) {
|
||||
let index = this._observable[type][event] && this._observable[type][event].indexOf(listener);
|
||||
if (index) {
|
||||
this._observable[type][event].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
once(event, listener) {
|
||||
this._addListener('onceListeners', event, listener);
|
||||
}
|
||||
|
||||
async trigger(event, params, throttle = false) {
|
||||
if (throttle) {
|
||||
if (this._throttled(event, params, throttle)) return;
|
||||
params = [params]
|
||||
}
|
||||
|
||||
await this._executeTriggers(event, params);
|
||||
}
|
||||
|
||||
async _executeTriggers(event, params) {
|
||||
let response = await this._triggerEvent('listeners', event, params);
|
||||
if (response === false) return false;
|
||||
|
||||
response = await this._triggerEvent('onceListeners', event, params);
|
||||
if (response === false) return false;
|
||||
|
||||
// emit via socket
|
||||
if (this._observable.socketServer) {
|
||||
this._observable.socketServer.emit(event, params);
|
||||
}
|
||||
|
||||
// clear once-listeners
|
||||
if (this._observable.onceListeners && this._observable.onceListeners[event]) {
|
||||
delete this._observable.onceListeners[event];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
clearListeners() {
|
||||
this._observable.listeners = {};
|
||||
this._observable.onceListeners = {};
|
||||
}
|
||||
|
||||
bindSocketClient(socket) {
|
||||
// also send events with sockets
|
||||
this._observable.socketClient = socket;
|
||||
}
|
||||
|
||||
bindSocketServer(socket) {
|
||||
// also send events with sockets
|
||||
this._observable.socketServer = socket;
|
||||
}
|
||||
|
||||
_throttled(event, params, throttle) {
|
||||
if (this._observable.isHot[event]) {
|
||||
// hot, add to queue
|
||||
if (!this._observable.eventQueue[event]) this._observable.eventQueue[event] = [];
|
||||
this._observable.eventQueue[event].push(params);
|
||||
|
||||
// aleady hot, quit
|
||||
return true;
|
||||
}
|
||||
this._observable.isHot[event] = true;
|
||||
|
||||
// cool-off
|
||||
setTimeout(() => {
|
||||
this._observable.isHot[event] = false;
|
||||
|
||||
// flush queue
|
||||
if (this._observable.eventQueue[event]) {
|
||||
let _queuedParams = this._observable.eventQueue[event];
|
||||
this._observable.eventQueue[event] = null;
|
||||
this._executeTriggers(event, _queuedParams);
|
||||
}
|
||||
}, throttle);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_addListener(type, event, listener) {
|
||||
if (!this._observable[type][event]) {
|
||||
this._observable[type][event] = [];
|
||||
}
|
||||
this._observable[type][event].push(listener);
|
||||
}
|
||||
|
||||
async _triggerEvent(type, event, params) {
|
||||
if (this._observable[type][event]) {
|
||||
for (let listener of this._observable[type][event]) {
|
||||
await listener(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
frappe/utils/translation.js
Normal file
80
frappe/utils/translation.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { ValueError } from '../common/errors';
|
||||
|
||||
function stringReplace(str, args) {
|
||||
if (!Array.isArray(args)) {
|
||||
args = [args];
|
||||
}
|
||||
|
||||
if (str == undefined) return str;
|
||||
|
||||
let unkeyed_index = 0;
|
||||
return str.replace(/\{(\w*)\}/g, (match, key) => {
|
||||
if (key === '') {
|
||||
key = unkeyed_index;
|
||||
unkeyed_index++;
|
||||
}
|
||||
if (key == +key) {
|
||||
return args[key] !== undefined ? args[key] : match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class TranslationString {
|
||||
constructor(...args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
get s() {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
ctx(context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
#translate(segment) {
|
||||
// TODO: implement translation backend
|
||||
return segment;
|
||||
}
|
||||
|
||||
#stitch() {
|
||||
if (typeof this.args[0] === 'string') {
|
||||
return stringReplace(this.args[0], this.args.slice(1));
|
||||
}
|
||||
|
||||
if (!(this.args[0] instanceof Array)) {
|
||||
throw new ValueError(
|
||||
`invalid args passed to TranslationString ${
|
||||
this.args
|
||||
} of type ${typeof this.args[0]}`
|
||||
);
|
||||
}
|
||||
|
||||
const strList = this.args[0];
|
||||
const argList = this.args.slice(1);
|
||||
return strList
|
||||
.map((s, i) => this.#translate(s) + (argList[i] ?? ''))
|
||||
.join('');
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.#stitch();
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.#stitch();
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.#stitch();
|
||||
}
|
||||
}
|
||||
|
||||
export function T(...args) {
|
||||
return new TranslationString(...args);
|
||||
}
|
||||
|
||||
export function t(...args) {
|
||||
return new TranslationString(...args).s;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import Account from './AccountDocument';
|
||||
|
||||
export default {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import frappe from 'frappe';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
|
||||
export default class Account extends BaseDocument {
|
||||
async validate() {
|
||||
@ -11,4 +11,4 @@ export default class Account extends BaseDocument {
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Account',
|
||||
title: _('Account'),
|
||||
columns: ['name', 'parentAccount', 'rootType']
|
||||
columns: ['name', 'parentAccount', 'rootType'],
|
||||
};
|
||||
|
@ -1,13 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'AccountingLedgerEntry',
|
||||
title: _('Accounting Ledger Entries'),
|
||||
columns: [
|
||||
'account',
|
||||
'party',
|
||||
'debit',
|
||||
'credit',
|
||||
'balance'
|
||||
]
|
||||
}
|
||||
columns: ['account', 'party', 'debit', 'credit', 'balance'],
|
||||
};
|
||||
|
@ -1,72 +1,73 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default {
|
||||
"name": "Email",
|
||||
"doctype": "DocType",
|
||||
"pageSettings": {
|
||||
hideTitle: true
|
||||
name: 'Email',
|
||||
doctype: 'DocType',
|
||||
pageSettings: {
|
||||
hideTitle: true,
|
||||
},
|
||||
isSingle: 0,
|
||||
isChild: 0,
|
||||
keywordFields: ['name'],
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'name',
|
||||
label: 'name',
|
||||
fieldtype: 'Data',
|
||||
required: 0,
|
||||
hidden: 1,
|
||||
disabled: 0,
|
||||
},
|
||||
"isSingle": 0,
|
||||
"isChild": 0,
|
||||
"keywordFields": ["name"],
|
||||
"fields": [{
|
||||
"fieldname": "name",
|
||||
"label": "name",
|
||||
"fieldtype": "Data",
|
||||
"required": 0,
|
||||
"hidden": 1,
|
||||
"disabled": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "fromEmailAddress",
|
||||
"label": "From",
|
||||
"fieldtype": "Data",
|
||||
"required": 1,
|
||||
"hidden": 0,
|
||||
formula: async () => {
|
||||
const accountingSettings = await frappe.getDoc('AccountingSettings');
|
||||
return accountingSettings.email;
|
||||
},
|
||||
"disabled": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "toEmailAddress",
|
||||
"label": "To",
|
||||
"fieldtype": "Data",
|
||||
"required": 1,
|
||||
"hidden": 0,
|
||||
"disabled": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
"label": "Date",
|
||||
"fieldtype": "Datetime",
|
||||
"required": 0,
|
||||
"hidden": 0,
|
||||
"disabled": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"label": "Subject",
|
||||
"fieldtype": "Data",
|
||||
"required": 0,
|
||||
"hidden": 0,
|
||||
"disabled": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "bodyText",
|
||||
"label": "Body",
|
||||
"fieldtype": "Text",
|
||||
"required": 0,
|
||||
"hidden": 0,
|
||||
"disabled": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "filePath",
|
||||
"label": "File Path",
|
||||
"fieldtype": "Text",
|
||||
"required": 0,
|
||||
"hidden": 1,
|
||||
}
|
||||
]
|
||||
};
|
||||
{
|
||||
fieldname: 'fromEmailAddress',
|
||||
label: 'From',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
hidden: 0,
|
||||
formula: async () => {
|
||||
const accountingSettings = await frappe.getDoc('AccountingSettings');
|
||||
return accountingSettings.email;
|
||||
},
|
||||
disabled: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'toEmailAddress',
|
||||
label: 'To',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
hidden: 0,
|
||||
disabled: 0,
|
||||
},
|
||||
{
|
||||
fieldname: 'date',
|
||||
label: 'Date',
|
||||
fieldtype: 'Datetime',
|
||||
required: 0,
|
||||
hidden: 0,
|
||||
disabled: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'subject',
|
||||
label: 'Subject',
|
||||
fieldtype: 'Data',
|
||||
required: 0,
|
||||
hidden: 0,
|
||||
disabled: 0,
|
||||
},
|
||||
{
|
||||
fieldname: 'bodyText',
|
||||
label: 'Body',
|
||||
fieldtype: 'Text',
|
||||
required: 0,
|
||||
hidden: 0,
|
||||
disabled: 0,
|
||||
},
|
||||
{
|
||||
fieldname: 'filePath',
|
||||
label: 'File Path',
|
||||
fieldtype: 'Text',
|
||||
required: 0,
|
||||
hidden: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,59 +1,54 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default {
|
||||
"name": "EmailAccount",
|
||||
"label": "Email Account",
|
||||
"doctype": "DocType",
|
||||
"isSingle": true,
|
||||
"isChild": false,
|
||||
"keywordFields": [
|
||||
"email"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "email",
|
||||
"label": "Email",
|
||||
"fieldtype": "Data",
|
||||
"required": 1,
|
||||
formula: async () => {
|
||||
const accountingSettings = await frappe.getDoc('AccountingSettings');
|
||||
return accountingSettings.email;
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname": "password",
|
||||
"label": "Password",
|
||||
"fieldtype": "Password",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "confirmPassword",
|
||||
"label": "Confirm Password",
|
||||
"fieldtype": "Password",
|
||||
"required": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "smtpHost",
|
||||
"label": "SMTP Host",
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
"smtp.gmail.com",
|
||||
"smtp.mail.yahoo.com",
|
||||
"smtp-mail.outlook.com",
|
||||
"smtp.mail.me.com",
|
||||
"smtp.aol.com"
|
||||
],
|
||||
"default": "smtp.gmail.com"
|
||||
},
|
||||
{
|
||||
"fieldname": "smtpPort",
|
||||
"label": "SMTP Port",
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
"465",
|
||||
"587"
|
||||
],
|
||||
"default": "465"
|
||||
}
|
||||
]
|
||||
};
|
||||
name: 'EmailAccount',
|
||||
label: 'Email Account',
|
||||
doctype: 'DocType',
|
||||
isSingle: true,
|
||||
isChild: false,
|
||||
keywordFields: ['email'],
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'email',
|
||||
label: 'Email',
|
||||
fieldtype: 'Data',
|
||||
required: 1,
|
||||
formula: async () => {
|
||||
const accountingSettings = await frappe.getDoc('AccountingSettings');
|
||||
return accountingSettings.email;
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: 'password',
|
||||
label: 'Password',
|
||||
fieldtype: 'Password',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'confirmPassword',
|
||||
label: 'Confirm Password',
|
||||
fieldtype: 'Password',
|
||||
required: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'smtpHost',
|
||||
label: 'SMTP Host',
|
||||
fieldtype: 'Select',
|
||||
options: [
|
||||
'smtp.gmail.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp-mail.outlook.com',
|
||||
'smtp.mail.me.com',
|
||||
'smtp.aol.com',
|
||||
],
|
||||
default: 'smtp.gmail.com',
|
||||
},
|
||||
{
|
||||
fieldname: 'smtpPort',
|
||||
label: 'SMTP Port',
|
||||
fieldtype: 'Select',
|
||||
options: ['465', '587'],
|
||||
default: '465',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import frappe from 'frappejs';
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
|
||||
export default class Event extends BaseDocument {
|
||||
alertEvent() {
|
||||
alert(this.title);
|
||||
}
|
||||
};
|
||||
alertEvent() {
|
||||
alert(this.title);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import Quotation from '../Quotation/Quotation';
|
||||
|
||||
export default model.extend(Quotation, {
|
||||
name: "Fulfillment",
|
||||
label: "Fulfillment",
|
||||
settings: "FulfillmentSettings",
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "items",
|
||||
"childtype": "FulfillmentItem"
|
||||
}
|
||||
]
|
||||
name: 'Fulfillment',
|
||||
label: 'Fulfillment',
|
||||
settings: 'FulfillmentSettings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'items',
|
||||
childtype: 'FulfillmentItem',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import QuotationItem from '../QuotationItem/QuotationItem';
|
||||
|
||||
export default model.extend(QuotationItem, {
|
||||
name: "FulfillmentItem"
|
||||
name: 'FulfillmentItem',
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import QuotationSettings from '../QuotationSettings/QuotationSettings';
|
||||
|
||||
export default model.extend(QuotationSettings, {
|
||||
"name": "FulfillmentSettings",
|
||||
"label": "Fulfillment Settings",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "numberSeries",
|
||||
"default": "OF"
|
||||
}
|
||||
]
|
||||
name: 'FulfillmentSettings',
|
||||
label: 'Fulfillment Settings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'numberSeries',
|
||||
default: 'OF',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import GSTR3B from './GSTR3BDocument.js';
|
||||
|
||||
export default {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
import format from './GSTR3BFormat';
|
||||
|
||||
export default class GSTR3B extends BaseDocument {
|
||||
@ -16,7 +16,7 @@ export default class GSTR3B extends BaseDocument {
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
'December',
|
||||
].indexOf(this.month);
|
||||
const month = monthIndex + 1 > 9 ? monthIndex + 1 : `0${monthIndex + 1}`;
|
||||
const lastDate = new Date(this.year, monthIndex + 1, 0).getDate();
|
||||
@ -25,22 +25,22 @@ export default class GSTR3B extends BaseDocument {
|
||||
'>=',
|
||||
`${this.year}-${month}-01`,
|
||||
'<=',
|
||||
`${this.year}-${month}-${lastDate}`
|
||||
]
|
||||
`${this.year}-${month}-${lastDate}`,
|
||||
],
|
||||
};
|
||||
const salesInvoices = frappe.db.getAll({
|
||||
doctype: 'SalesInvoice',
|
||||
filters,
|
||||
fields: ['*']
|
||||
fields: ['*'],
|
||||
});
|
||||
const purchaseInvoices = frappe.db.getAll({
|
||||
doctype: 'PurchaseInvoice',
|
||||
filters,
|
||||
fields: ['*']
|
||||
fields: ['*'],
|
||||
});
|
||||
const [gstr1Data, gstr2Data] = await Promise.all([
|
||||
salesInvoices,
|
||||
purchaseInvoices
|
||||
purchaseInvoices,
|
||||
]);
|
||||
let gstr3bData = [[], []];
|
||||
|
||||
@ -74,7 +74,7 @@ export default class GSTR3B extends BaseDocument {
|
||||
row.rate = 0;
|
||||
row.inState = true;
|
||||
row.reverseCharge = !party.gstin ? 'Y' : 'N';
|
||||
ledgerEntry.taxes.forEach(tax => {
|
||||
ledgerEntry.taxes.forEach((tax) => {
|
||||
row.rate += tax.rate;
|
||||
const taxAmt = (tax.rate * ledgerEntry.netTotal) / 100;
|
||||
if (tax.account === 'IGST') row.igstAmt = taxAmt;
|
||||
@ -113,7 +113,7 @@ export default class GSTR3B extends BaseDocument {
|
||||
jsonData['inter_sup']['unreg_details'].push({
|
||||
pos: ledgerEntry.place,
|
||||
txval: ledgerEntry.taxVal,
|
||||
iAmt: ledgerEntry.igstAmt || 0
|
||||
iAmt: ledgerEntry.igstAmt || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -147,4 +147,4 @@ export default class GSTR3B extends BaseDocument {
|
||||
return JSON.stringify(json, undefined, 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'GSTR3B',
|
||||
title: _('GSTR 3B Report'),
|
||||
columns: ['year', 'month']
|
||||
columns: ['year', 'month'],
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import { _ } from 'frappejs/utils';
|
||||
import frappe from 'frappe';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Item',
|
||||
title: _('Items'),
|
||||
columns: [
|
||||
'name',
|
||||
'unit',
|
||||
'tax',
|
||||
'rate',
|
||||
]
|
||||
}
|
||||
columns: ['name', 'unit', 'tax', 'rate'],
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import Badge from '@/components/Badge';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'JournalEntry',
|
||||
title: _('Journal Entry'),
|
||||
formRoute: name => `/edit/JournalEntry/${name}`,
|
||||
formRoute: (name) => `/edit/JournalEntry/${name}`,
|
||||
columns: [
|
||||
'date',
|
||||
{
|
||||
@ -35,10 +35,10 @@ export default {
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
getValue(doc) {
|
||||
return doc.name
|
||||
}
|
||||
return doc.name;
|
||||
},
|
||||
},
|
||||
'entryType',
|
||||
'referenceNumber'
|
||||
]
|
||||
'referenceNumber',
|
||||
],
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
import LedgerPosting from '../../../accounting/ledgerPosting';
|
||||
|
||||
export default class JournalEntryServer extends BaseDocument {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import frappe from 'frappejs';
|
||||
import { _ } from 'frappejs/utils';
|
||||
import router from '@/router';
|
||||
import frappe from 'frappe';
|
||||
import { _ } from 'frappe/utils';
|
||||
import PartyWidget from './PartyWidget.vue';
|
||||
|
||||
export default {
|
||||
@ -8,46 +8,46 @@ export default {
|
||||
label: 'Customer',
|
||||
basedOn: 'Party',
|
||||
filters: {
|
||||
customer: 1
|
||||
customer: 1,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: _('Create Invoice'),
|
||||
condition: doc => !doc.isNew(),
|
||||
action: async customer => {
|
||||
condition: (doc) => !doc.isNew(),
|
||||
action: async (customer) => {
|
||||
let doc = await frappe.getNewDoc('SalesInvoice');
|
||||
router.push({
|
||||
path: `/edit/SalesInvoice/${doc.name}`,
|
||||
query: {
|
||||
doctype: 'SalesInvoice',
|
||||
values: {
|
||||
customer: customer.name
|
||||
}
|
||||
}
|
||||
customer: customer.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: _('View Invoices'),
|
||||
condition: doc => !doc.isNew(),
|
||||
action: customer => {
|
||||
condition: (doc) => !doc.isNew(),
|
||||
action: (customer) => {
|
||||
router.push({
|
||||
name: 'ListView',
|
||||
params: {
|
||||
doctype: 'SalesInvoice',
|
||||
filters: {
|
||||
customer: customer.name
|
||||
}
|
||||
}
|
||||
customer: customer.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
quickEditWidget: doc => ({
|
||||
quickEditWidget: (doc) => ({
|
||||
render(h) {
|
||||
return h(PartyWidget, {
|
||||
props: { doc }
|
||||
props: { doc },
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Customer',
|
||||
title: _('Customers'),
|
||||
columns: ['name', 'phone', 'outstandingAmount']
|
||||
columns: ['name', 'phone', 'outstandingAmount'],
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import { _ } from 'frappejs/utils';
|
||||
import frappe from 'frappe';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
name: 'Party',
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Party',
|
||||
columns: ['name', 'phone', 'outstandingAmount']
|
||||
columns: ['name', 'phone', 'outstandingAmount'],
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import frappe from 'frappe';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
|
||||
export default class PartyServer extends BaseDocument {
|
||||
beforeInsert() {
|
||||
|
@ -50,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import { getStatusColumn } from '../Transaction/Transaction';
|
||||
import { routeTo } from '@/utils';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import router from '@/router';
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import { _ } from 'frappe/utils';
|
||||
import PartyWidget from './PartyWidget.vue';
|
||||
|
||||
export default {
|
||||
@ -8,46 +8,46 @@ export default {
|
||||
label: 'Supplier',
|
||||
basedOn: 'Party',
|
||||
filters: {
|
||||
supplier: 1
|
||||
supplier: 1,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
label: _('Create Bill'),
|
||||
condition: doc => !doc.isNew(),
|
||||
action: async supplier => {
|
||||
condition: (doc) => !doc.isNew(),
|
||||
action: async (supplier) => {
|
||||
let doc = await frappe.getNewDoc('PurchaseInvoice');
|
||||
router.push({
|
||||
path: `/edit/PurchaseInvoice/${doc.name}`,
|
||||
query: {
|
||||
doctype: 'PurchaseInvoice',
|
||||
values: {
|
||||
supplier: supplier.name
|
||||
}
|
||||
}
|
||||
supplier: supplier.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: _('View Bills'),
|
||||
condition: doc => !doc.isNew(),
|
||||
action: supplier => {
|
||||
condition: (doc) => !doc.isNew(),
|
||||
action: (supplier) => {
|
||||
router.push({
|
||||
name: 'ListView',
|
||||
params: {
|
||||
doctype: 'PurchaseInvoice',
|
||||
filters: {
|
||||
supplier: supplier.name
|
||||
}
|
||||
}
|
||||
supplier: supplier.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
quickEditWidget: doc => ({
|
||||
quickEditWidget: (doc) => ({
|
||||
render(h) {
|
||||
return h(PartyWidget, {
|
||||
props: { doc }
|
||||
props: { doc },
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Supplier',
|
||||
title: _('Supplier'),
|
||||
columns: ['name', 'phone', 'outstandingAmount']
|
||||
columns: ['name', 'phone', 'outstandingAmount'],
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import utils from '../../../accounting/utils';
|
||||
|
||||
export default {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import Badge from '@/components/Badge';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Payment',
|
||||
@ -28,12 +28,12 @@ export default {
|
||||
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
|
||||
components: { Badge }
|
||||
components: { Badge },
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
'paymentType',
|
||||
'date',
|
||||
'amount'
|
||||
]
|
||||
'amount',
|
||||
],
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import frappe from 'frappe';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
import LedgerPosting from '../../../accounting/ledgerPosting';
|
||||
|
||||
export default class PaymentServer extends BaseDocument {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import { _ } from 'frappejs/utils';
|
||||
import frappe from 'frappe';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
const referenceTypeMap = {
|
||||
SalesInvoice: _('Invoice'),
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
import { getStatusColumn } from '../Transaction/Transaction';
|
||||
|
||||
export default {
|
||||
doctype: 'PurchaseInvoice',
|
||||
title: _('Bills'),
|
||||
formRoute: name => `/edit/PurchaseInvoice/${name}`,
|
||||
formRoute: (name) => `/edit/PurchaseInvoice/${name}`,
|
||||
columns: [
|
||||
'supplier',
|
||||
'name',
|
||||
getStatusColumn('PurchaseInvoice'),
|
||||
'date',
|
||||
'grandTotal',
|
||||
'outstandingAmount'
|
||||
]
|
||||
'outstandingAmount',
|
||||
],
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import PurchaseInvoice from '../PurchaseInvoice/PurchaseInvoice';
|
||||
|
||||
export default model.extend(
|
||||
@ -10,11 +10,11 @@ export default model.extend(
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'items',
|
||||
childtype: 'PurchaseOrderItem'
|
||||
}
|
||||
]
|
||||
childtype: 'PurchaseOrderItem',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
skipFields: ['account']
|
||||
skipFields: ['account'],
|
||||
}
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import PurchaseInvoiceItem from '../PurchaseInvoiceItem/PurchaseInvoiceItem';
|
||||
|
||||
export default model.extend(PurchaseInvoiceItem, {
|
||||
name: "PurchaseOrderItem"
|
||||
name: 'PurchaseOrderItem',
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import PurchaseInvoiceSettings from '../PurchaseInvoiceSettings/PurchaseInvoiceSettings';
|
||||
|
||||
export default model.extend(PurchaseInvoiceSettings, {
|
||||
"name": "PurchaseOrderSettings",
|
||||
"label": "Purchase Order Settings",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "numberSeries",
|
||||
"default": "PO"
|
||||
}
|
||||
]
|
||||
name: 'PurchaseOrderSettings',
|
||||
label: 'Purchase Order Settings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'numberSeries',
|
||||
default: 'PO',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,14 +1,14 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import PurchaseOrder from '../PurchaseOrder/PurchaseOrder';
|
||||
|
||||
export default model.extend(PurchaseOrder, {
|
||||
name: "PurchaseReceipt",
|
||||
label: "Purchase Receipt",
|
||||
settings: "PurchaseReceiptSettings",
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "items",
|
||||
"childtype": "PurchaseReceiptItem"
|
||||
}
|
||||
]
|
||||
name: 'PurchaseReceipt',
|
||||
label: 'Purchase Receipt',
|
||||
settings: 'PurchaseReceiptSettings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'items',
|
||||
childtype: 'PurchaseReceiptItem',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,16 +1,20 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import PurchaseOrderItem from '../PurchaseOrderItem/PurchaseOrderItem';
|
||||
|
||||
export default model.extend(PurchaseOrderItem, {
|
||||
name: "PurchaseReceiptItem",
|
||||
export default model.extend(
|
||||
PurchaseOrderItem,
|
||||
{
|
||||
name: 'PurchaseReceiptItem',
|
||||
fields: [
|
||||
{
|
||||
"fieldname": "acceptedQuantity",
|
||||
"label": "Accepted Quantity",
|
||||
"fieldtype": "Float",
|
||||
"required": 1
|
||||
}
|
||||
]
|
||||
}, {
|
||||
skipFields: ['expenseAccount']
|
||||
});
|
||||
{
|
||||
fieldname: 'acceptedQuantity',
|
||||
label: 'Accepted Quantity',
|
||||
fieldtype: 'Float',
|
||||
required: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
skipFields: ['expenseAccount'],
|
||||
}
|
||||
);
|
||||
|
@ -1,13 +1,13 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import PurchaseOrderSettings from '../PurchaseOrderSettings/PurchaseOrderSettings';
|
||||
|
||||
export default model.extend(PurchaseOrderSettings, {
|
||||
"name": "PurchaseReceiptSettings",
|
||||
"label": "Purchase Receipt Settings",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "numberSeries",
|
||||
"default": "PREC"
|
||||
}
|
||||
]
|
||||
name: 'PurchaseReceiptSettings',
|
||||
label: 'Purchase Receipt Settings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'numberSeries',
|
||||
default: 'PREC',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import SalesInvoice from '../SalesInvoice/SalesInvoice';
|
||||
|
||||
const Quotation = model.extend(
|
||||
@ -10,14 +10,14 @@ const Quotation = model.extend(
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'items',
|
||||
childtype: 'QuotationItem'
|
||||
}
|
||||
childtype: 'QuotationItem',
|
||||
},
|
||||
],
|
||||
links: []
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
skipFields: ['account'],
|
||||
overrideProps: ['links']
|
||||
overrideProps: ['links'],
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import SalesInvoiceItem from '../SalesInvoiceItem/SalesInvoiceItem';
|
||||
|
||||
export default model.extend(SalesInvoiceItem, {
|
||||
name: "QuotationItem"
|
||||
name: 'QuotationItem',
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import SalesInvoiceSettings from '../SalesInvoiceSettings/SalesInvoiceSettings';
|
||||
|
||||
export default model.extend(SalesInvoiceSettings, {
|
||||
"name": "QuotationSettings",
|
||||
"label": "Quotation Settings",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "numberSeries",
|
||||
"default": "QTN"
|
||||
}
|
||||
]
|
||||
name: 'QuotationSettings',
|
||||
label: 'Quotation Settings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'numberSeries',
|
||||
default: 'QTN',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
import { getStatusColumn } from '../Transaction/Transaction';
|
||||
|
||||
export default {
|
||||
doctype: 'SalesInvoice',
|
||||
title: _('Invoices'),
|
||||
formRoute: name => `/edit/SalesInvoice/${name}`,
|
||||
formRoute: (name) => `/edit/SalesInvoice/${name}`,
|
||||
columns: [
|
||||
'customer',
|
||||
'name',
|
||||
getStatusColumn('SalesInvoice'),
|
||||
'date',
|
||||
'grandTotal',
|
||||
'outstandingAmount'
|
||||
]
|
||||
'outstandingAmount',
|
||||
],
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
export default {
|
||||
name: 'Base',
|
||||
props: ['doc', 'printSettings'],
|
||||
|
@ -1,4 +1,4 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import Quotation from '../Quotation/Quotation';
|
||||
|
||||
export default model.extend(Quotation, {
|
||||
@ -8,7 +8,7 @@ export default model.extend(Quotation, {
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'items',
|
||||
childtype: 'SalesOrderItem'
|
||||
}
|
||||
]
|
||||
childtype: 'SalesOrderItem',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import QuotationItem from '../QuotationItem/QuotationItem';
|
||||
|
||||
export default model.extend(QuotationItem, {
|
||||
name: "SalesOrderItem"
|
||||
name: 'SalesOrderItem',
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import model from 'frappejs/model';
|
||||
import model from 'frappe/model';
|
||||
import QuotationSettings from '../QuotationSettings/QuotationSettings';
|
||||
|
||||
export default model.extend(QuotationSettings, {
|
||||
"name": "SalesOrderSettings",
|
||||
"label": "Sales Order Settings",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "numberSeries",
|
||||
"default": "SO"
|
||||
}
|
||||
]
|
||||
name: 'SalesOrderSettings',
|
||||
label: 'Sales Order Settings',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'numberSeries',
|
||||
default: 'SO',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default async function generateTaxes(country) {
|
||||
if (country === 'India') {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import { _ } from 'frappe/utils';
|
||||
|
||||
export default {
|
||||
doctype: 'Tax',
|
||||
title: _('Taxes'),
|
||||
columns: ['name']
|
||||
columns: ['name'],
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Badge from '@/components/Badge';
|
||||
import { getInvoiceStatus, openQuickEdit, routeTo } from '@/utils';
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import utils from '../../../accounting/utils';
|
||||
import { statusColor } from '../../../src/colors';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe from 'frappejs';
|
||||
import BaseDocument from 'frappejs/model/document';
|
||||
import frappe from 'frappe';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
import { getExchangeRate } from '../../../accounting/exchangeRate';
|
||||
|
||||
export default class TransactionDocument extends BaseDocument {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default {
|
||||
async getPayments() {
|
||||
@ -6,7 +6,7 @@ export default {
|
||||
doctype: 'PaymentFor',
|
||||
fields: ['parent'],
|
||||
filters: { referenceName: this.name },
|
||||
orderBy: 'name'
|
||||
orderBy: 'name',
|
||||
});
|
||||
if (payments.length != 0) {
|
||||
return payments;
|
||||
@ -57,5 +57,5 @@ export default {
|
||||
}
|
||||
const entries = await this.getPosting();
|
||||
await entries.postReverse();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
async function setAugmentedModel(model, regionalInfo) {
|
||||
const getAugmentedModel = (
|
||||
|
@ -25,11 +25,12 @@
|
||||
"core-js": "^3.19.0",
|
||||
"csvjson-csv2json": "^5.0.6",
|
||||
"electron-store": "^8.0.1",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-charts": "1.6.1",
|
||||
"frappejs": "frappe/frappejs",
|
||||
"knex": "^0.95.12",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.0.2",
|
||||
"pesa": "^1.1.3",
|
||||
"sqlite3": "npm:@vscode/sqlite3@^5.0.7",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.3"
|
||||
@ -42,6 +43,7 @@
|
||||
"@vue/cli-plugin-router": "^4.5.0",
|
||||
"@vue/cli-service": "^4.5.0",
|
||||
"autoprefixer": "^9",
|
||||
"babel-loader": "^8.2.3",
|
||||
"electron": "^15.3.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-notarize": "^1.1.1",
|
||||
@ -56,7 +58,8 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
|
||||
"vue-cli-plugin-electron-builder": "^2.0.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"webpack": "^5.66.0"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default async function execute() {
|
||||
// Since sqlite has no ALTER TABLE to change column meta
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
function getTablesToConvert() {
|
||||
// Do not change loops to map, doesn't work for some reason.
|
||||
|
@ -1,15 +1,15 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default class AccountsReceivablePayable {
|
||||
async run(reportType, { date }) {
|
||||
const rows = await getReceivablePayable({
|
||||
reportType,
|
||||
date
|
||||
date,
|
||||
});
|
||||
|
||||
return { rows };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
let entries = [];
|
||||
@ -34,7 +34,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
if (outStandingAmount > 0.1 / 10) {
|
||||
const row = {
|
||||
date: entry.date,
|
||||
party: entry.party
|
||||
party: entry.party,
|
||||
};
|
||||
|
||||
// due date / bill date
|
||||
@ -51,7 +51,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
invoicedAmount,
|
||||
paidAmount,
|
||||
outStandingAmount,
|
||||
creditNoteAmount
|
||||
creditNoteAmount,
|
||||
});
|
||||
|
||||
// ageing
|
||||
@ -69,13 +69,13 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
doctype: referenceType,
|
||||
fields: ['name', 'date'],
|
||||
filters: {
|
||||
submitted: 1
|
||||
}
|
||||
submitted: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getValidEntries() {
|
||||
return entries.filter(entry => {
|
||||
return entries.filter((entry) => {
|
||||
return (
|
||||
entry.date <= date &&
|
||||
entry.referenceType === referenceType &&
|
||||
@ -111,7 +111,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
entry[reverseDebitOrCredit] -
|
||||
paymentAmount -
|
||||
creditNoteAmount,
|
||||
creditNoteAmount
|
||||
creditNoteAmount,
|
||||
};
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
}
|
||||
|
||||
function getFutureEntries() {
|
||||
return entries.filter(entry => entry.date > date);
|
||||
return entries.filter((entry) => entry.date > date);
|
||||
}
|
||||
|
||||
function getReturnEntries() {
|
||||
@ -139,12 +139,14 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
}
|
||||
|
||||
const partyType = reportType === 'Receivable' ? 'customer' : 'supplier';
|
||||
const partyList = (await frappe.db.getAll({
|
||||
doctype: 'Party',
|
||||
filters: {
|
||||
[partyType]: 1
|
||||
}
|
||||
})).map(d => d.name);
|
||||
const partyList = (
|
||||
await frappe.db.getAll({
|
||||
doctype: 'Party',
|
||||
filters: {
|
||||
[partyType]: 1,
|
||||
},
|
||||
})
|
||||
).map((d) => d.name);
|
||||
|
||||
return await frappe.db.getAll({
|
||||
doctype: 'AccountingLedgerEntry',
|
||||
@ -156,13 +158,13 @@ async function getReceivablePayable({ reportType = 'Receivable', date }) {
|
||||
'referenceType',
|
||||
'referenceName',
|
||||
'sum(debit) as debit',
|
||||
'sum(credit) as credit'
|
||||
'sum(credit) as credit',
|
||||
],
|
||||
filters: {
|
||||
party: ['in', partyList]
|
||||
party: ['in', partyList],
|
||||
},
|
||||
groupBy: ['referenceType', 'referenceName', 'party'],
|
||||
orderBy: 'date'
|
||||
orderBy: 'date',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,51 +1,55 @@
|
||||
import frappe from 'frappejs';
|
||||
import { unique } from 'frappejs/utils';
|
||||
import { unique } from 'frappe/utils';
|
||||
import { getData } from '../FinancialStatements/FinancialStatements';
|
||||
|
||||
class BalanceSheet {
|
||||
async run({ fromDate, toDate, periodicity }) {
|
||||
async run({ fromDate, toDate, periodicity }) {
|
||||
let asset = await getData({
|
||||
rootType: 'Asset',
|
||||
balanceMustBe: 'Debit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true,
|
||||
});
|
||||
|
||||
let asset = await getData({
|
||||
rootType: 'Asset',
|
||||
balanceMustBe: 'Debit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true
|
||||
});
|
||||
let liability = await getData({
|
||||
rootType: 'Liability',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true,
|
||||
});
|
||||
|
||||
let liability = await getData({
|
||||
rootType: 'Liability',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true
|
||||
});
|
||||
let equity = await getData({
|
||||
rootType: 'Equity',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true,
|
||||
});
|
||||
|
||||
let equity = await getData({
|
||||
rootType: 'Equity',
|
||||
balanceMustBe: 'Credit',
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity,
|
||||
accumulateValues: true
|
||||
});
|
||||
const rows = [
|
||||
...asset.accounts,
|
||||
asset.totalRow,
|
||||
[],
|
||||
...liability.accounts,
|
||||
liability.totalRow,
|
||||
[],
|
||||
...equity.accounts,
|
||||
equity.totalRow,
|
||||
[],
|
||||
];
|
||||
|
||||
const rows = [
|
||||
...asset.accounts, asset.totalRow, [],
|
||||
...liability.accounts, liability.totalRow, [],
|
||||
...equity.accounts, equity.totalRow, []
|
||||
];
|
||||
const columns = unique([
|
||||
...asset.periodList,
|
||||
...liability.periodList,
|
||||
...equity.periodList,
|
||||
]);
|
||||
|
||||
const columns = unique([
|
||||
...asset.periodList,
|
||||
...liability.periodList,
|
||||
...equity.periodList
|
||||
]);
|
||||
|
||||
return { rows, columns };
|
||||
}
|
||||
return { rows, columns };
|
||||
}
|
||||
}
|
||||
|
||||
export default BalanceSheet;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import getCommonExportActions from '../commonExporter';
|
||||
|
||||
export default {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
class BankReconciliation {
|
||||
async run(params) {
|
||||
@ -25,9 +25,9 @@ class BankReconciliation {
|
||||
'name',
|
||||
'referenceDate',
|
||||
'referenceId',
|
||||
'clearanceDate'
|
||||
'clearanceDate',
|
||||
],
|
||||
filters: filters
|
||||
filters: filters,
|
||||
});
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
@ -37,8 +37,8 @@ class BankReconciliation {
|
||||
filters: {
|
||||
referenceType: 'Payment',
|
||||
account: data[i].paymentAccount,
|
||||
referenceName: data[i].name
|
||||
}
|
||||
referenceName: data[i].name,
|
||||
},
|
||||
});
|
||||
data[i].credit = ledger[0].credit;
|
||||
data[i].debit = ledger[0].debit;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import csv2json from 'csvjson-csv2json';
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import ReconciliationValidation from '../../src/components/ReconciliationValidation';
|
||||
|
||||
export const fileImportHandler = (file, report) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import frappe from 'frappejs';
|
||||
import { getPeriodList } from '../FinancialStatements/FinancialStatements';
|
||||
import frappe from 'frappe';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getPeriodList } from '../FinancialStatements/FinancialStatements';
|
||||
|
||||
class Cashflow {
|
||||
async run({ fromDate, toDate, periodicity }) {
|
||||
@ -15,10 +15,10 @@ class Cashflow {
|
||||
.where('reverted', 0)
|
||||
.sum({
|
||||
inflow: 'debit',
|
||||
outflow: 'credit'
|
||||
outflow: 'credit',
|
||||
})
|
||||
.select({
|
||||
'month-year': dateAsMonthYear
|
||||
'month-year': dateAsMonthYear,
|
||||
})
|
||||
.where('account', 'in', cashAndBankAccounts)
|
||||
.whereBetween('date', [fromDate, toDate])
|
||||
@ -26,9 +26,9 @@ class Cashflow {
|
||||
|
||||
let periodList = getPeriodList(fromDate, toDate, periodicity);
|
||||
|
||||
let data = periodList.map(periodKey => {
|
||||
let data = periodList.map((periodKey) => {
|
||||
let monthYear = this.getMonthYear(periodKey, 'MMM yyyy');
|
||||
let cashflowForPeriod = res.find(d => d['month-year'] === monthYear);
|
||||
let cashflowForPeriod = res.find((d) => d['month-year'] === monthYear);
|
||||
if (cashflowForPeriod) {
|
||||
cashflowForPeriod.periodKey = periodKey;
|
||||
return cashflowForPeriod;
|
||||
@ -37,13 +37,13 @@ class Cashflow {
|
||||
inflow: 0,
|
||||
outflow: 0,
|
||||
periodKey,
|
||||
'month-year': monthYear
|
||||
'month-year': monthYear,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
periodList
|
||||
periodList,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
import { DateTime } from 'luxon';
|
||||
import { convertPesaValuesToFloat } from '../../src/utils';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import frappe from 'frappejs';
|
||||
import frappe from 'frappe';
|
||||
|
||||
class GeneralLedger {
|
||||
async run(params) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user