add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
import { ACCOUNT_TYPES } from '@/data/AccountTypes';
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
name: {
name: 'account.field.name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'account.field.description',
column: 'description',
fieldType: 'text',
},
slug: {
name: 'account.field.slug',
column: 'slug',
fieldType: 'text',
columnable: false,
filterable: false,
},
code: {
name: 'account.field.code',
column: 'code',
fieldType: 'text',
},
root_type: {
name: 'account.field.root_type',
fieldType: 'enumeration',
options: [
{ key: 'asset', label: 'Asset' },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
filterCustomQuery: RootTypeFieldFilterQuery,
sortable: false,
},
normal: {
name: 'account.field.normal',
fieldType: 'enumeration',
options: [
{ key: 'debit', label: 'account.field.normal.debit' },
{ key: 'credit', label: 'account.field.normal.credit' },
],
filterCustomQuery: NormalTypeFieldFilterQuery,
sortable: false,
},
type: {
name: 'account.field.type',
column: 'account_type',
fieldType: 'enumeration',
options: ACCOUNT_TYPES.map((accountType) => ({
label: accountType.label,
key: accountType.key
})),
},
active: {
name: 'account.field.active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
balance: {
name: 'account.field.balance',
column: 'amount',
fieldType: 'number',
},
currency: {
name: 'account.field.currency',
column: 'currency_code',
fieldType: 'text',
filterable: false,
},
created_at: {
name: 'account.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Filter query of root type field .
*/
function RootTypeFieldFilterQuery(query, role) {
query.modify('filterByRootType', role.value);
}
/**
* Filter query of normal field .
*/
function NormalTypeFieldFilterQuery(query, role) {
query.modify('filterByAccountNormal', role.value);
}

View File

@@ -0,0 +1,419 @@
/* eslint-disable global-require */
import { mixin, Model } from 'objection';
import { castArray } from 'lodash';
import TenantModel from '@/models/TenantModel';
import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder';
import { flatToNestedArray } from 'utils';
import DependencyGraph from '@/lib/DependencyGraph';
import AccountTypesUtils from '@/lib/AccountTypes';
import AccountSettings from './Account.Settings';
import ModelSettings from './ModelSetting';
import {
ACCOUNT_TYPES,
getAccountsSupportsMultiCurrency,
} from '@/data/AccountTypes';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Accounts/constants';
import ModelSearchable from './ModelSearchable';
export default class Account extends mixin(TenantModel, [
ModelSettings,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'accounts';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'accountTypeLabel',
'accountParentType',
'accountRootType',
'accountNormal',
'accountNormalFormatted',
'isBalanceSheetAccount',
'isPLSheet',
];
}
/**
* Account normal.
*/
get accountNormal() {
return AccountTypesUtils.getType(this.accountType, 'normal');
}
get accountNormalFormatted() {
const paris = {
credit: 'Credit',
debit: 'Debit',
};
return paris[this.accountNormal] || '';
}
/**
* Retrieve account type label.
*/
get accountTypeLabel() {
return AccountTypesUtils.getType(this.accountType, 'label');
}
/**
* Retrieve account parent type.
*/
get accountParentType() {
return AccountTypesUtils.getType(this.accountType, 'parentType');
}
/**
* Retrieve account root type.
*/
get accountRootType() {
return AccountTypesUtils.getType(this.accountType, 'rootType');
}
/**
* Retrieve whether the account is balance sheet account.
*/
get isBalanceSheetAccount() {
return this.isBalanceSheet();
}
/**
* Retrieve whether the account is profit/loss sheet account.
*/
get isPLSheet() {
return this.isProfitLossSheet();
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
const TABLE_NAME = Account.tableName;
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('accounts.active', !active);
},
filterAccounts(query, accountIds) {
if (accountIds.length > 0) {
query.whereIn(`${TABLE_NAME}.id`, accountIds);
}
},
filterAccountTypes(query, typesIds) {
if (typesIds.length > 0) {
query.whereIn('account_types.accoun_type_id', typesIds);
}
},
viewRolesBuilder(query, conditionals, expression) {
buildFilterQuery(Account.tableName, conditionals, expression)(query);
},
sortColumnBuilder(query, columnKey, direction) {
buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
},
/**
* Filter by root type.
*/
filterByRootType(query, rootType) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.rootType === rootType
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
/**
* Filter by account normal
*/
filterByAccountNormal(query, accountNormal) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.normal === accountNormal
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
/**
* Finds account by the given slug.
* @param {*} query
* @param {*} slug
*/
findBySlug(query, slug) {
query.where('slug', slug).first();
},
/**
*
* @param {*} query
* @param {*} baseCyrrency
*/
preventMutateBaseCurrency(query) {
const accountsTypes = getAccountsSupportsMultiCurrency();
const accountsTypesKeys = accountsTypes.map((type) => type.key);
query
.whereIn('accountType', accountsTypesKeys)
.where('seededAt', null)
.first();
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const Item = require('models/Item');
const InventoryAdjustment = require('models/InventoryAdjustment');
const ManualJournalEntry = require('models/ManualJournalEntry');
const Expense = require('models/Expense');
const ExpenseEntry = require('models/ExpenseCategory');
const ItemEntry = require('models/ItemEntry');
return {
/**
* Account model may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'accounts.id',
to: 'accounts_transactions.accountId',
},
},
/**
*
*/
itemsCostAccount: {
relation: Model.HasManyRelation,
modelClass: Item.default,
join: {
from: 'accounts.id',
to: 'items.costAccountId',
},
},
/**
*
*/
itemsSellAccount: {
relation: Model.HasManyRelation,
modelClass: Item.default,
join: {
from: 'accounts.id',
to: 'items.sellAccountId',
},
},
/**
*
*/
inventoryAdjustments: {
relation: Model.HasManyRelation,
modelClass: InventoryAdjustment.default,
join: {
from: 'accounts.id',
to: 'inventory_adjustments.adjustmentAccountId',
},
},
/**
*
*/
manualJournalEntries: {
relation: Model.HasManyRelation,
modelClass: ManualJournalEntry.default,
join: {
from: 'accounts.id',
to: 'manual_journals_entries.accountId',
},
},
/**
*
*/
expensePayments: {
relation: Model.HasManyRelation,
modelClass: Expense.default,
join: {
from: 'accounts.id',
to: 'expenses_transactions.paymentAccountId',
},
},
/**
*
*/
expenseEntries: {
relation: Model.HasManyRelation,
modelClass: ExpenseEntry.default,
join: {
from: 'accounts.id',
to: 'expense_transaction_categories.expenseAccountId',
},
},
/**
*
*/
entriesCostAccount: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'accounts.id',
to: 'items_entries.costAccountId',
},
},
/**
*
*/
entriesSellAccount: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'accounts.id',
to: 'items_entries.sellAccountId',
},
},
};
}
/**
* Detarmines whether the given type equals the account type.
* @param {string} accountType
* @return {boolean}
*/
isAccountType(accountType) {
const types = castArray(accountType);
return types.indexOf(this.accountType) !== -1;
}
/**
* Detarmines whether the given root type equals the account type.
* @param {string} rootType
* @return {boolean}
*/
isRootType(rootType) {
return AccountTypesUtils.isRootTypeEqualsKey(this.accountType, rootType);
}
/**
* Detarmine whether the given parent type equals the account type.
* @param {string} parentType
* @return {boolean}
*/
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
);
}
/**
* Detarmines whether the account is balance sheet account.
* @return {boolean}
*/
isBalanceSheet() {
return AccountTypesUtils.isTypeBalanceSheet(this.accountType);
}
/**
* Detarmines whether the account is profit/loss account.
* @return {boolean}
*/
isProfitLossSheet() {
return AccountTypesUtils.isTypePLSheet(this.accountType);
}
/**
* Detarmines whether the account is income statement account
* @return {boolean}
*/
isIncomeSheet() {
return this.isProfitLossSheet();
}
/**
* Converts flatten accounts list to nested array.
* @param {Array} accounts
* @param {Object} options
*/
static toNestedArray(accounts, options = { children: 'children' }) {
return flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
}
/**
* Transformes the accounts list to depenedency graph structure.
* @param {IAccount[]} accounts
*/
static toDependencyGraph(accounts) {
return DependencyGraph.fromArray(accounts, {
itemId: 'id',
parentItemId: 'parentAccountId',
});
}
/**
* Model settings.
*/
static get meta() {
return AccountSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,230 @@
import { Model, raw } from 'objection';
import moment from 'moment';
import { isEmpty, castArray } from 'lodash';
import TenantModel from 'models/TenantModel';
export default class AccountTransaction extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'accounts_transactions';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['referenceTypeFormatted'];
}
/**
* Retrieve formatted reference type.
* @return {string}
*/
get referenceTypeFormatted() {
return AccountTransaction.getReferenceTypeFormatted(this.referenceType);
}
/**
* Reference type formatted.
*/
static getReferenceTypeFormatted(referenceType) {
const mapped = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment receive',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
Expense: 'Expense',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
InvoiceWriteOff: 'Invoice write-off',
CreditNote: 'transaction_type.credit_note',
VendorCredit: 'transaction_type.vendor_credit',
RefundCreditNote: 'transaction_type.refund_credit_note',
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
};
return mapped[referenceType] || '';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters accounts by the given ids.
* @param {Query} query
* @param {number[]} accountsIds
*/
filterAccounts(query, accountsIds) {
if (Array.isArray(accountsIds) && accountsIds.length > 0) {
query.whereIn('account_id', accountsIds);
}
},
filterTransactionTypes(query, types) {
if (Array.isArray(types) && types.length > 0) {
query.whereIn('reference_type', types);
} else if (typeof types === 'string') {
query.where('reference_type', types);
}
},
filterDateRange(query, startDate, endDate, type = 'day') {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const fromDate = moment(startDate)
.utcOffset(0)
.startOf(type)
.format(dateFormat);
const toDate = moment(endDate)
.utcOffset(0)
.endOf(type)
.format(dateFormat);
if (startDate) {
query.where('date', '>=', fromDate);
}
if (endDate) {
query.where('date', '<=', toDate);
}
},
filterAmountRange(query, fromAmount, toAmount) {
if (fromAmount) {
query.andWhere((q) => {
q.where('credit', '>=', fromAmount);
q.orWhere('debit', '>=', fromAmount);
});
}
if (toAmount) {
query.andWhere((q) => {
q.where('credit', '<=', toAmount);
q.orWhere('debit', '<=', toAmount);
});
}
},
sumationCreditDebit(query) {
query.select(['accountId']);
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('account_id');
},
filterContactType(query, contactType) {
query.where('contact_type', contactType);
},
filterContactIds(query, contactIds) {
query.whereIn('contact_id', contactIds);
},
openingBalance(query, fromDate) {
query.modify('filterDateRange', null, fromDate);
query.modify('sumationCreditDebit');
},
closingBalance(query, toDate) {
query.modify('filterDateRange', null, toDate);
query.modify('sumationCreditDebit');
},
contactsOpeningBalance(
query,
openingDate,
receivableAccounts,
customersIds
) {
// Filter by date.
query.modify('filterDateRange', null, openingDate);
// Filter by customers.
query.whereNot('contactId', null);
query.whereIn('accountId', castArray(receivableAccounts));
if (!isEmpty(customersIds)) {
query.whereIn('contactId', castArray(customersIds));
}
// Group by the contact transactions.
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
},
creditDebitSummation(query) {
query.sum('credit as credit');
query.sum('debit as debit');
},
groupByDateFormat(query, groupType = 'month') {
const groupBy = {
day: '%Y-%m-%d',
month: '%Y-%m',
year: '%Y',
};
const dateFormat = groupBy[groupType];
query.select(raw(`DATE_FORMAT(DATE, '${dateFormat}')`).as('date'));
query.groupByRaw(`DATE_FORMAT(DATE, '${dateFormat}')`);
},
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
filterByProjects(query, projectsIds) {
const formattedProjectsIds = castArray(projectsIds);
query.whereIn('projectId', formattedProjectsIds);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('models/Account');
const Contact = require('models/Contact');
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'accounts_transactions.accountId',
to: 'accounts.id',
},
},
contact: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
join: {
from: 'accounts_transactions.contactId',
to: 'contacts.id',
},
},
};
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,38 @@
export default class Auth {
/**
* Retrieve the authenticated user.
*/
static get user() {
return null;
}
/**
* Sets the authenticated user.
* @param {User} user
*/
static setAuthenticatedUser(user) {
this.user = user;
}
/**
* Retrieve the authenticated user ID.
*/
static userId() {
if (!this.user) {
return false;
}
return this.user.id;
}
/**
* Whether the user is logged or not.
*/
static isLogged() {
return !!this.user;
}
static loggedOut() {
this.user = null;
}
}

View File

@@ -0,0 +1,94 @@
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
vendor: {
name: 'bill.field.vendor',
column: 'vendor_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
bill_number: {
name: 'bill.field.bill_number',
column: 'bill_number',
columnable: true,
fieldType: 'text',
},
bill_date: {
name: 'bill.field.bill_date',
column: 'bill_date',
columnable: true,
fieldType: 'date',
},
due_date: {
name: 'bill.field.due_date',
column: 'due_date',
columnable: true,
fieldType: 'date',
},
reference_no: {
name: 'bill.field.reference_no',
column: 'reference_no',
columnable: true,
fieldType: 'text',
},
status: {
name: 'bill.field.status',
fieldType: 'enumeration',
columnable: true,
options: [
{ label: 'bill.field.status.paid', key: 'paid' },
{ label: 'bill.field.status.partially-paid', key: 'partially-paid' },
{ label: 'bill.field.status.overdue', key: 'overdue' },
{ label: 'bill.field.status.unpaid', key: 'unpaid' },
{ label: 'bill.field.status.opened', key: 'opened' },
{ label: 'bill.field.status.draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
amount: {
name: 'bill.field.amount',
column: 'amount',
fieldType: 'number',
},
payment_amount: {
name: 'bill.field.payment_amount',
column: 'payment_amount',
fieldType: 'number',
},
note: {
name: 'bill.field.note',
column: 'note',
fieldType: 'text',
},
created_at: {
name: 'bill.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('statusFilter', role.value);
}
/**
* Status field sort custom query.
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -0,0 +1,431 @@
import { Model, raw, mixin } from 'objection';
import { castArray, difference } from 'lodash';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/constants';
import ModelSearchable from './ModelSearchable';
export default class Bill extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'bills';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the bills in draft status.
*/
draft(query) {
query.where('opened_at', null);
},
/**
* Filters the opened bills.
*/
published(query) {
query.whereNot('openedAt', null);
},
/**
* Filters the opened bills.
*/
opened(query) {
query.whereNot('opened_at', null);
},
/**
* Filters the unpaid bills.
*/
unpaid(query) {
query.where('payment_amount', 0);
},
/**
* Filters the due bills.
*/
dueBills(query) {
query.where(
raw(`COALESCE(AMOUNT, 0) -
COALESCE(PAYMENT_AMOUNT, 0) -
COALESCE(CREDITED_AMOUNT, 0) > 0
`)
);
},
/**
* Filters the overdue bills.
*/
overdue(query) {
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
},
/**
* Filters the not overdue invoices.
*/
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
query.where('due_date', '>=', asDate);
},
/**
* Filters the partially paid bills.
*/
partiallyPaid(query) {
query.whereNot('payment_amount', 0);
query.whereNot(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
},
/**
* Filters the paid bills.
*/
paid(query) {
query.where(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
},
/**
* Filters the bills from the given date.
*/
fromDate(query, fromDate) {
query.where('bill_date', '<=', fromDate);
},
/**
* Sort the bills by full-payment bills.
*/
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
/**
* Filters by branches.
*/
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
dueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueBills');
query.modify('notOverdue');
query.modify('fromDate', asDate);
},
overdueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueBills');
query.modify('overdue', asDate);
query.modify('fromDate', asDate);
},
/**
*
*/
billable(query) {
query.where(raw('AMOUNT > INVOICED_AMOUNT'));
},
};
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'balance',
'dueAmount',
'isOpen',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount',
'localAmount',
'localAllocatedCostAmount',
'billableAmount',
];
}
/**
* Invoice amount in organization base currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Retrieves the local allocated cost amount.
* @returns {number}
*/
get localAllocatedCostAmount() {
return this.allocatedCostAmount * this.exchangeRate;
}
/**
* Retrieves the local landed cost amount.
* @returns {number}
*/
get localLandedCostAmount() {
return this.landedCostAmount * this.exchangeRate;
}
/**
* Retrieves the local unallocated cost amount.
* @returns {number}
*/
get localUnallocatedCostAmount() {
return this.unallocatedCostAmount * this.exchangeRate;
}
/**
* Retrieve the balance of bill.
* @return {number}
*/
get balance() {
return this.paymentAmount + this.creditedAmount;
}
/**
* Due amount of the given.
* @return {number}
*/
get dueAmount() {
return Math.max(this.amount - this.balance, 0);
}
/**
* Detarmine whether the bill is open.
* @return {boolean}
*/
get isOpen() {
return !!this.openedAt;
}
/**
* Deetarmine whether the bill paid partially.
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.amount && this.dueAmount > 0;
}
/**
* Deetarmine whether the bill paid fully.
* @return {boolean}
*/
get isFullyPaid() {
return this.dueAmount === 0;
}
/**
* Detarmines whether the bill paid fully or partially.
* @return {boolean}
*/
get isPaid() {
return this.isPartiallyPaid || this.isFullyPaid;
}
/**
* Retrieve the remaining days in number
* @return {number|null}
*/
get remainingDays() {
const currentMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(dueDateMoment.diff(currentMoment, 'days'), 0);
}
/**
* Retrieve the overdue days in number.
* @return {number|null}
*/
get overdueDays() {
const currentMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(currentMoment.diff(dueDateMoment, 'days'), 0);
}
/**
* Detarmines the due date is over.
* @return {boolean}
*/
get isOverdue() {
return this.overdueDays > 0;
}
/**
* Retrieve the unallocated cost amount.
* @return {number}
*/
get unallocatedCostAmount() {
return Math.max(this.landedCostAmount - this.allocatedCostAmount, 0);
}
/**
* Retrieves the calculated amount which have not been invoiced.
*/
get billableAmount() {
return Math.max(this.amount - this.invoicedAmount, 0);
}
/**
* Bill model settings.
*/
static get meta() {
return BillSettings;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Vendor = require('models/Vendor');
const ItemEntry = require('models/ItemEntry');
const BillLandedCost = require('models/BillLandedCost');
const Branch = require('models/Branch');
return {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor.default,
join: {
from: 'bills.vendorId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'vendor');
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'bills.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'Bill');
builder.orderBy('index', 'ASC');
},
},
locatedLandedCosts: {
relation: Model.HasManyRelation,
modelClass: BillLandedCost.default,
join: {
from: 'bills.id',
to: 'bill_located_costs.billId',
},
},
/**
* Bill may belongs to associated branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'bills.branchId',
to: 'branches.id',
},
},
};
}
/**
* Retrieve the not found bills ids as array that associated to the given vendor.
* @param {Array} billsIds
* @param {number} vendorId -
* @return {Array}
*/
static async getNotFoundBills(billsIds, vendorId) {
const storedBills = await this.query().onBuild((builder) => {
builder.whereIn('id', billsIds);
if (vendorId) {
builder.where('vendor_id', vendorId);
}
});
const storedBillsIds = storedBills.map((t) => t.id);
const notFoundBillsIds = difference(billsIds, storedBillsIds);
return notFoundBillsIds;
}
static changePaymentAmount(billId, amount) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query()
.where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount));
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'bill_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,92 @@
import { Model } from 'objection';
import { lowerCase } from 'lodash';
import TenantModel from 'models/TenantModel';
export default class BillLandedCost extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'bill_located_costs';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount', 'allocationMethodFormatted'];
}
/**
* Retrieves the cost local amount.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Allocation method formatted.
*/
get allocationMethodFormatted() {
const allocationMethod = lowerCase(this.allocationMethod);
const keyLabelsPairs = {
value: 'allocation_method.value.label',
quantity: 'allocation_method.quantity.label',
};
return keyLabelsPairs[allocationMethod] || '';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const BillLandedCostEntry = require('models/BillLandedCostEntry');
const Bill = require('models/Bill');
const ItemEntry = require('models/ItemEntry');
const ExpenseCategory = require('models/ExpenseCategory');
return {
bill: {
relation: Model.BelongsToOneRelation,
modelClass: Bill.default,
join: {
from: 'bill_located_costs.billId',
to: 'bills.id',
},
},
allocateEntries: {
relation: Model.HasManyRelation,
modelClass: BillLandedCostEntry.default,
join: {
from: 'bill_located_costs.id',
to: 'bill_located_cost_entries.billLocatedCostId',
},
},
allocatedFromBillEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'bill_located_costs.fromTransactionEntryId',
to: 'items_entries.id',
},
},
allocatedFromExpenseEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ExpenseCategory.default,
join: {
from: 'bill_located_costs.fromTransactionEntryId',
to: 'expense_transaction_categories.id',
},
},
};
}
}

View File

@@ -0,0 +1,32 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class BillLandedCostEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'bill_located_cost_entries';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const ItemEntry = require('models/ItemEntry');
return {
itemEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'bill_located_cost_entries.entryId',
to: 'items_entries.id',
},
filter(builder) {
builder.where('reference_type', 'Bill');
},
},
};
}
}

View File

@@ -0,0 +1,66 @@
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
vendor: {
name: 'bill_payment.field.vendor',
column: 'vendor_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
amount: {
name: 'bill_payment.field.amount',
column: 'amount',
fieldType: 'number',
},
due_amount: {
name: 'bill_payment.field.due_amount',
column: 'due_amount',
fieldType: 'number',
},
payment_account: {
name: 'bill_payment.field.payment_account',
column: 'payment_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'paymentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
payment_number: {
name: 'bill_payment.field.payment_number',
column: 'payment_number',
fieldType: 'text',
},
payment_date: {
name: 'bill_payment.field.payment_date',
column: 'payment_date',
fieldType: 'date',
},
reference_no: {
name: 'bill_payment.field.reference_no',
column: 'reference',
fieldType: 'text',
},
description: {
name: 'bill_payment.field.description',
column: 'description',
fieldType: 'text',
},
created_at: {
name: 'bill_payment.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,144 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import BillPaymentSettings from './BillPayment.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
import ModelSearchable from './ModelSearchable';
export default class BillPayment extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'bills_payments';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
}
/**
* Payment amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Model settings.
*/
static get meta() {
return BillPaymentSettings;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const BillPaymentEntry = require('models/BillPaymentEntry');
const AccountTransaction = require('models/AccountTransaction');
const Vendor = require('models/Vendor');
const Account = require('models/Account');
const Branch = require('models/Branch');
return {
entries: {
relation: Model.HasManyRelation,
modelClass: BillPaymentEntry.default,
join: {
from: 'bills_payments.id',
to: 'bills_payments_entries.billPaymentId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor.default,
join: {
from: 'bills_payments.vendorId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'vendor');
},
},
paymentAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'bills_payments.paymentAccountId',
to: 'accounts.id',
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'bills_payments.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'BillPayment');
},
},
/**
* Bill payment may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'bills_payments.branchId',
to: 'branches.id',
},
},
};
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'payment_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,45 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class BillPaymentEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'bills_payments_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Bill = require('models/Bill');
const BillPayment = require('models/BillPayment');
return {
payment: {
relation: Model.BelongsToOneRelation,
modelClass: BillPayment.default,
join: {
from: 'bills_payments_entries.billPaymentId',
to: 'bills_payments.id',
},
},
bill: {
relation: Model.BelongsToOneRelation,
modelClass: Bill.default,
join: {
from: 'bills_payments_entries.billId',
to: 'bills.id',
},
},
};
}
}

View File

@@ -0,0 +1,172 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Branch extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'branches';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters accounts by the given ids.
* @param {Query} query
* @param {number[]} accountsIds
*/
isPrimary(query) {
query.where('primary', true);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleInvoice = require('models/SaleInvoice');
const SaleEstimate = require('models/SaleEstimate');
const SaleReceipt = require('models/SaleReceipt');
const Bill = require('models/Bill');
const PaymentReceive = require('models/PaymentReceive');
const PaymentMade = require('models/BillPayment');
const VendorCredit = require('models/VendorCredit');
const CreditNote = require('models/CreditNote');
const AccountTransaction = require('models/AccountTransaction');
const InventoryTransaction = require('models/InventoryTransaction');
return {
/**
* Branch may belongs to associated sale invoices.
*/
invoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
join: {
from: 'branches.id',
to: 'sales_invoices.branchId',
},
},
/**
* Branch may belongs to associated sale estimates.
*/
estimates: {
relation: Model.HasManyRelation,
modelClass: SaleEstimate.default,
join: {
from: 'branches.id',
to: 'sales_estimates.branchId',
},
},
/**
* Branch may belongs to associated sale receipts.
*/
receipts: {
relation: Model.HasManyRelation,
modelClass: SaleReceipt.default,
join: {
from: 'branches.id',
to: 'sales_receipts.branchId',
},
},
/**
* Branch may belongs to associated payment receives.
*/
paymentReceives: {
relation: Model.HasManyRelation,
modelClass: PaymentReceive.default,
join: {
from: 'branches.id',
to: 'payment_receives.branchId',
},
},
/**
* Branch may belongs to assocaited bills.
*/
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'branches.id',
to: 'bills.branchId',
},
},
/**
* Branch may belongs to associated payment mades.
*/
paymentMades: {
relation: Model.HasManyRelation,
modelClass: PaymentMade.default,
join: {
from: 'branches.id',
to: 'bills_payments.branchId',
},
},
/**
* Branch may belongs to associated credit notes.
*/
creditNotes: {
relation: Model.HasManyRelation,
modelClass: CreditNote.default,
join: {
from: 'branches.id',
to: 'credit_notes.branchId',
},
},
/**
* Branch may belongs to associated to vendor credits.
*/
vendorCredit: {
relation: Model.HasManyRelation,
modelClass: VendorCredit.default,
join: {
from: 'branches.id',
to: 'vendor_credits.branchId',
},
},
/**
* Branch may belongs to associated to accounts transactions.
*/
accountsTransactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'branches.id',
to: 'accounts_transactions.branchId',
},
},
/**
* Branch may belongs to associated to inventory transactions.
*/
inventoryTransactions: {
relation: Model.HasManyRelation,
modelClass: InventoryTransaction.default,
join: {
from: 'branches.id',
to: 'inventory_transactions.branchId',
},
},
};
}
}

View File

@@ -0,0 +1,54 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
name: {
name: 'account.field.name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'account.field.description',
column: 'description',
fieldType: 'text',
},
slug: {
name: 'account.field.slug',
column: 'slug',
fieldType: 'text',
columnable: false,
filterable: false,
},
code: {
name: 'account.field.code',
column: 'code',
fieldType: 'text',
},
active: {
name: 'account.field.active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
balance: {
name: 'account.field.balance',
column: 'amount',
fieldType: 'number',
},
currency: {
name: 'account.field.currency',
column: 'currency_code',
fieldType: 'text',
filterable: false,
},
created_at: {
name: 'account.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,132 @@
/* eslint-disable global-require */
import { mixin, Model } from 'objection';
import { castArray } from 'lodash';
import TenantModel from 'models/TenantModel';
import AccountTypesUtils from '@/lib/AccountTypes';
import CashflowAccountSettings from './CashflowAccount.Settings';
import ModelSettings from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Accounts/constants';
import ModelSearchable from './ModelSearchable';
export default class CashflowAccount extends mixin(TenantModel, [
ModelSettings,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'accounts';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['accountTypeLabel'];
}
/**
* Retrieve account type label.
*/
get accountTypeLabel() {
return AccountTypesUtils.getType(this.accountType, 'label');
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('accounts.active', !active);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
return {
/**
* Account model may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'accounts.id',
to: 'accounts_transactions.accountId',
},
},
};
}
/**
* Detarmines whether the given type equals the account type.
* @param {string} accountType
* @return {boolean}
*/
isAccountType(accountType) {
const types = castArray(accountType);
return types.indexOf(this.accountType) !== -1;
}
/**
* Detarmine whether the given parent type equals the account type.
* @param {string} parentType
* @return {boolean}
*/
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
);
}
/**
* Model settings.
*/
static get meta() {
return CashflowAccountSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
}

View File

@@ -0,0 +1,148 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
import {
getCashflowAccountTransactionsTypes,
getCashflowTransactionType,
} from '@/services/Cashflow/utils';
import AccountTransaction from './AccountTransaction';
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
export default class CashflowTransaction extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'cashflow_transactions';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'localAmount',
'transactionTypeFormatted',
'isPublished',
'typeMeta',
'isCashCredit',
'isCashDebit',
];
}
/**
* Retrieves the local amount of cashflow transaction.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Detarmines whether the cashflow transaction is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
/**
* Transaction type formatted.
*/
get transactionTypeFormatted() {
return AccountTransaction.getReferenceTypeFormatted(this.transactionType);
}
get typeMeta() {
return getCashflowTransactionType(this.transactionType);
}
/**
* Detarmines whether the cashflow transaction cash credit type.
* @returns {boolean}
*/
get isCashCredit() {
return this.typeMeta?.direction === CASHFLOW_DIRECTION.OUT;
}
/**
* Detarmines whether the cashflow transaction cash debit type.
* @returns {boolean}
*/
get isCashDebit() {
return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const CashflowTransactionLine = require('models/CashflowTransactionLine');
const AccountTransaction = require('models/AccountTransaction');
const Account = require('models/Account');
return {
/**
* Cashflow transaction entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: CashflowTransactionLine.default,
join: {
from: 'cashflow_transactions.id',
to: 'cashflow_transaction_lines.cashflowTransactionId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
/**
* Cashflow transaction has associated account transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'cashflow_transactions.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
const referenceTypes = getCashflowAccountTransactionsTypes();
builder.whereIn('reference_type', referenceTypes);
},
},
/**
* Cashflow transaction may has assocaited cashflow account.
*/
cashflowAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'cashflow_transactions.cashflowAccountId',
to: 'accounts.id',
},
},
/**
* Cashflow transcation may has associated to credit account.
*/
creditAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'cashflow_transactions.creditAccountId',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -0,0 +1,45 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class CashflowTransactionLine extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'cashflow_transaction_lines';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('models/Account');
return {
cashflowAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'cashflow_transaction_lines.cashflowAccountId',
to: 'accounts.id',
},
},
creditAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'cashflow_transaction_lines.creditAccountId',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -0,0 +1,201 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Contact extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'contacts';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Defined virtual attributes.
*/
static get virtualAttributes() {
return ['contactNormal', 'closingBalance', 'formattedContactService'];
}
/**
* Retrieve the contact normal by the given contact type.
*/
static getContactNormalByType(contactType) {
const types = {
vendor: 'credit',
customer: 'debit',
};
return types[contactType];
}
/**
* Retrieve the contact normal by the given contact service.
* @param {string} contactService
*/
static getFormattedContactService(contactService) {
const types = {
customer: 'Customer',
vendor: 'Vendor',
};
return types[contactService];
}
/**
* Retrieve the contact normal.
*/
get contactNormal() {
return Contact.getContactNormalByType(this.contactService);
}
/**
* Retrieve formatted contact service.
*/
get formattedContactService() {
return Contact.getFormattedContactService(this.contactService);
}
/**
* Closing balance attribute.
*/
get closingBalance() {
return this.balance;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterContactIds(query, customerIds) {
query.whereIn('id', customerIds);
},
customer(query) {
query.where('contact_service', 'customer');
},
vendor(query) {
query.where('contact_service', 'vendor');
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleEstimate = require('models/SaleEstimate');
const SaleReceipt = require('models/SaleReceipt');
const SaleInvoice = require('models/SaleInvoice');
const PaymentReceive = require('models/PaymentReceive');
const Bill = require('models/Bill');
const BillPayment = require('models/BillPayment');
const AccountTransaction = require('models/AccountTransaction');
return {
/**
* Contact may has many sales invoices.
*/
salesInvoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
join: {
from: 'contacts.id',
to: 'sales_invoices.customerId',
},
},
/**
* Contact may has many sales estimates.
*/
salesEstimates: {
relation: Model.HasManyRelation,
modelClass: SaleEstimate.default,
join: {
from: 'contacts.id',
to: 'sales_estimates.customerId',
},
},
/**
* Contact may has many sales receipts.
*/
salesReceipts: {
relation: Model.HasManyRelation,
modelClass: SaleReceipt.default,
join: {
from: 'contacts.id',
to: 'sales_receipts.customerId',
},
},
/**
* Contact may has many payments receives.
*/
paymentReceives: {
relation: Model.HasManyRelation,
modelClass: PaymentReceive.default,
join: {
from: 'contacts.id',
to: 'payment_receives.customerId',
},
},
/**
* Contact may has many bills.
*/
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'contacts.id',
to: 'bills.vendorId',
},
},
/**
* Contact may has many bills payments.
*/
billPayments: {
relation: Model.HasManyRelation,
modelClass: BillPayment.default,
join: {
from: 'contacts.id',
to: 'bills_payments.vendorId',
},
},
/**
* Contact may has many accounts transactions.
*/
accountsTransactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'contacts.id',
to: 'accounts_transactions.contactId',
},
},
};
}
static get fields() {
return {
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
created_at: {
column: 'created_at',
},
};
}
}

View File

@@ -0,0 +1,80 @@
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
customer: {
name: 'credit_note.field.customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
credit_date: {
name: 'credit_note.field.credit_note_date',
column: 'credit_note_date',
fieldType: 'date',
},
credit_number: {
name: 'credit_note.field.credit_note_number',
column: 'credit_note_number',
fieldType: 'text',
},
reference_no: {
name: 'credit_note.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
amount: {
name: 'credit_note.field.amount',
column: 'amount',
fieldType: 'number',
},
currency_code: {
name: 'credit_note.field.currency_code',
column: 'currency_code',
fieldType: 'number',
},
note: {
name: 'credit_note.field.note',
column: 'note',
fieldType: 'text',
},
terms_conditions: {
name: 'credit_note.field.terms_conditions',
column: 'terms_conditions',
fieldType: 'text',
},
status: {
name: 'credit_note.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'credit_note.field.status.draft' },
{ key: 'published', label: 'credit_note.field.status.published' },
{ key: 'open', label: 'credit_note.field.status.open' },
{ key: 'closed', label: 'credit_note.field.status.closed' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
created_at: {
name: 'credit_note.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,278 @@
import { mixin, Model, raw } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants';
import ModelSearchable from './ModelSearchable';
import CreditNoteMeta from './CreditNote.Meta';
export default class CreditNote extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'credit_notes';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'localAmount',
'isDraft',
'isPublished',
'isOpen',
'isClosed',
'creditsRemaining',
'creditsUsed',
];
}
/**
* Credit note amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Detarmines whether the credit note is draft.
* @returns {boolean}
*/
get isDraft() {
return !this.openedAt;
}
/**
* Detarmines whether vendor credit is published.
* @returns {boolean}
*/
get isPublished() {
return !!this.openedAt;
}
/**
* Detarmines whether the credit note is open.
* @return {boolean}
*/
get isOpen() {
return !!this.openedAt && this.creditsRemaining > 0;
}
/**
* Detarmines whether the credit note is closed.
* @return {boolean}
*/
get isClosed() {
return this.openedAt && this.creditsRemaining === 0;
}
/**
* Retrieve the credits remaining.
*/
get creditsRemaining() {
return Math.max(this.amount - this.refundedAmount - this.invoicesAmount, 0);
}
get creditsUsed() {
return this.refundedAmount + this.invoicesAmount;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the credit notes in draft status.
*/
draft(query) {
query.where('opened_at', null);
},
/**
* Filters the.
*/
published(query) {
query.whereNot('opened_at', null);
},
/**
* Filters the open credit notes.
*/
open(query) {
query
.where(
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) <
COALESCE(AMOUNT)`)
)
.modify('published');
},
/**
* Filters the closed credit notes.
*/
closed(query) {
query
.where(
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) =
COALESCE(AMOUNT)`)
)
.modify('published');
},
/**
* Status filter.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'published':
query.modify('published');
break;
case 'open':
default:
query.modify('open');
break;
case 'closed':
query.modify('closed');
break;
}
},
/**
*
*/
sortByStatus(query, order) {
query.orderByRaw(
`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) = COALESCE(AMOUNT) ${order}`
);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer');
const Branch = require('models/Branch');
return {
/**
* Credit note associated entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'credit_notes.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'CreditNote');
builder.orderBy('index', 'ASC');
},
},
/**
* Belongs to customer model.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
join: {
from: 'credit_notes.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'Customer');
},
},
/**
* Credit note associated GL entries.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'credit_notes.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'CreditNote');
},
},
/**
* Credit note may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'credit_notes.branchId',
to: 'branches.id',
},
},
};
}
/**
* Sale invoice meta.
*/
static get meta() {
return CreditNoteMeta;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model searchable.
*/
static get searchable() {
return true;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'credit_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
* @returns {boolean}
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,53 @@
import { mixin, Model } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSearchable from './ModelSearchable';
export default class CreditNoteAppliedInvoice extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'credit_note_applied_invoice';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleInvoice = require('models/SaleInvoice');
const CreditNote = require('models/CreditNote');
return {
saleInvoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'credit_note_applied_invoice.invoiceId',
to: 'sales_invoices.id',
},
},
creditNote: {
relation: Model.BelongsToOneRelation,
modelClass: CreditNote.default,
join: {
from: 'credit_note_applied_invoice.creditNoteId',
to: 'credit_notes.id',
},
},
};
}
}

View File

@@ -0,0 +1,25 @@
import { mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSearchable from './ModelSearchable';
export default class CreditNoteAppliedInvoiceEntry extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'credit_associated_transaction_entry';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {};
}
}

View File

@@ -0,0 +1,21 @@
import TenantModel from 'models/TenantModel';
export default class Currency extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'currencies';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
static get resourceable() {
return true;
}
}

View File

@@ -0,0 +1,20 @@
export default (Model) =>
class CustomViewBaseModel extends Model {
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return [];
}
/**
* Retrieve the default view by the given slug.
*/
static getDefaultViewBySlug(viewSlug) {
return this.defaultViews.find((view) => view.slug === viewSlug) || null;
}
static getDefaultViews() {
return this.defaultViews;
}
};

View File

@@ -0,0 +1,92 @@
export default {
fields: {
first_name: {
name: 'customer.field.first_name',
column: 'first_name',
fieldType: 'text',
},
last_name: {
name: 'customer.field.last_name',
column: 'last_name',
fieldType: 'text',
},
display_name: {
name: 'customer.field.display_name',
column: 'display_name',
fieldType: 'text',
},
email: {
name: 'customer.field.email',
column: 'email',
fieldType: 'text',
},
work_phone: {
name: 'customer.field.work_phone',
column: 'work_phone',
fieldType: 'text',
},
personal_phone: {
name: 'customer.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
},
company_name: {
name: 'customer.field.company_name',
column: 'company_name',
fieldType: 'text',
},
website: {
name: 'customer.field.website',
column: 'website',
fieldType: 'text',
},
created_at: {
name: 'customer.field.created_at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'customer.field.balance',
column: 'balance',
fieldType: 'number',
},
opening_balance: {
name: 'customer.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
},
opening_balance_at: {
name: 'customer.field.opening_balance_at',
column: 'opening_balance_at',
filterable: false,
fieldType: 'date',
},
currency_code: {
name: 'customer.field.currency',
column: 'currency_code',
fieldType: 'text',
},
status: {
name: 'customer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'active', label: 'customer.field.status.active' },
{ key: 'inactive', label: 'customer.field.status.inactive' },
{ key: 'overdue', label: 'customer.field.status.overdue' },
{ key: 'unpaid', label: 'customer.field.status.unpaid' },
],
filterCustomQuery: statusFieldFilterQuery,
},
},
};
function statusFieldFilterQuery(query, role) {
switch (role.value) {
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
}

View File

@@ -0,0 +1,180 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination';
import ModelSetting from './ModelSetting';
import CustomerSettings from './Customer.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Contacts/Customers/constants';
import ModelSearchable from './ModelSearchable';
class CustomerQueryBuilder extends PaginationQueryBuilder {
constructor(...args) {
super(...args);
this.onBuild((builder) => {
if (builder.isFind() || builder.isDelete() || builder.isUpdate()) {
builder.where('contact_service', 'customer');
}
});
}
}
export default class Customer extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Query builder.
*/
static get QueryBuilder() {
return CustomerQueryBuilder;
}
/**
* Table name
*/
static get tableName() {
return 'contacts';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Defined virtual attributes.
*/
static get virtualAttributes() {
return ['localOpeningBalance', 'closingBalance', 'contactNormal'];
}
/**
* Closing balance attribute.
*/
get closingBalance() {
return this.balance;
}
/**
* Retrieves the local opening balance.
* @returns {number}
*/
get localOpeningBalance() {
return this.openingBalance
? this.openingBalance * this.openingBalanceExchangeRate
: 0;
}
/**
* Retrieve the contact noraml;
*/
get contactNormal() {
return 'debit';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/**
* Filters the active customers.
*/
active(query) {
query.where('active', 1);
},
/**
* Filters the inactive customers.
*/
inactive(query) {
query.where('active', 0);
},
/**
* Filters the customers that have overdue invoices.
*/
overdue(query) {
query.select(
'*',
Customer.relatedQuery('overDueInvoices', query.knex())
.count()
.as('countOverdue')
);
query.having('countOverdue', '>', 0);
},
/**
* Filters the unpaid customers.
*/
unpaid(query) {
query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0');
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleInvoice = require('models/SaleInvoice');
return {
salesInvoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
join: {
from: 'contacts.id',
to: 'sales_invoices.customerId',
},
},
overDueInvoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
join: {
from: 'contacts.id',
to: 'sales_invoices.customerId',
},
filter: (query) => {
query.modify('overdue');
},
},
};
}
static get meta() {
return CustomerSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'display_name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'first_name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'last_name', comparator: 'equals' },
{ condition: 'or', fieldKey: 'company_name', comparator: 'equals' },
{ condition: 'or', fieldKey: 'email', comparator: 'equals' },
{ condition: 'or', fieldKey: 'work_phone', comparator: 'equals' },
{ condition: 'or', fieldKey: 'personal_phone', comparator: 'equals' },
{ condition: 'or', fieldKey: 'website', comparator: 'equals' },
];
}
}

View File

@@ -0,0 +1,34 @@
import moment from 'moment';
export default (Model) => {
return class DateSession extends Model {
get timestamps() {
return ['createdAt', 'updatedAt'];
}
$beforeUpdate(opt, context) {
const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => {
const key = this.timestamps[1];
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
$beforeInsert(context) {
const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => {
const key = this.timestamps[0];
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
}
}

View File

@@ -0,0 +1,44 @@
import bcrypt from 'bcryptjs';
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ExchangeRate extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'exchange_rates';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Model defined fields.
*/
static get fields(){
return {
currency_code: {
label: 'Currency',
column: 'currency_code'
},
exchange_rate: {
label: 'Exchange rate',
column: 'exchange_rate',
},
date: {
label: 'Date',
column: 'date',
},
created_at: {
label: "Created at",
column: "created_at",
columnType: "date",
},
}
}
}

View File

@@ -0,0 +1,71 @@
/**
* Expense - Settings.
*/
export default {
defaultFilterField: 'description',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
'payment_date': {
name: 'expense.field.payment_date',
column: 'payment_date',
fieldType: 'date',
},
'payment_account': {
name: 'expense.field.payment_account',
column: 'payment_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'paymentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'amount': {
name: 'expense.field.amount',
column: 'total_amount',
fieldType: 'number',
},
'reference_no': {
name: 'expense.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
'description': {
name: 'expense.field.description',
column: 'description',
fieldType: 'text',
},
'published': {
name: 'expense.field.published',
column: 'published_at',
fieldType: 'date',
},
'status': {
name: 'expense.field.status',
fieldType: 'enumeration',
options: [
{ label: 'expense.field.status.draft', key: 'draft' },
{ label: 'expense.field.status.published', key: 'published' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'expense.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -0,0 +1,263 @@
import { Model, mixin, raw } from 'objection';
import TenantModel from 'models/TenantModel';
import { viewRolesBuilder } from '@/lib/ViewRolesBuilder';
import ModelSetting from './ModelSetting';
import ExpenseSettings from './Expense.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Expenses/constants';
import ModelSearchable from './ModelSearchable';
import moment from 'moment';
export default class Expense extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'expenses_transactions';
}
/**
* Account transaction reference type.
*/
static get referenceType() {
return 'Expense';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'isPublished',
'unallocatedCostAmount',
'localAmount',
'localLandedCostAmount',
'localUnallocatedCostAmount',
'localAllocatedCostAmount',
'billableAmount',
];
}
/**
* Retrieves the local amount of expense.
* @returns {number}
*/
get localAmount() {
return this.totalAmount * this.exchangeRate;
}
/**
* Rertieves the local landed cost amount of expense.
* @returns {number}
*/
get localLandedCostAmount() {
return this.landedCostAmount * this.exchangeRate;
}
/**
* Retrieves the local allocated cost amount.
* @returns {number}
*/
get localAllocatedCostAmount() {
return this.allocatedCostAmount * this.exchangeRate;
}
/**
* Retrieve the unallocated cost amount.
* @return {number}
*/
get unallocatedCostAmount() {
return Math.max(this.totalAmount - this.allocatedCostAmount, 0);
}
/**
* Retrieves the local unallocated cost amount.
* @returns {number}
*/
get localUnallocatedCostAmount() {
return this.unallocatedCostAmount * this.exchangeRate;
}
/**
* Detarmines whether the expense is published.
* @returns {boolean}
*/
get isPublished() {
return Boolean(this.publishedAt);
}
/**
* Retrieves the calculated amount which have not been invoiced.
*/
get billableAmount() {
return Math.max(this.totalAmount - this.invoicedAmount, 0);
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterByDateRange(query, startDate, endDate) {
if (startDate) {
query.where('date', '>=', startDate);
}
if (endDate) {
query.where('date', '<=', endDate);
}
},
filterByAmountRange(query, from, to) {
if (from) {
query.where('amount', '>=', from);
}
if (to) {
query.where('amount', '<=', to);
}
},
filterByExpenseAccount(query, accountId) {
if (accountId) {
query.where('expense_account_id', accountId);
}
},
filterByPaymentAccount(query, accountId) {
if (accountId) {
query.where('payment_account_id', accountId);
}
},
viewRolesBuilder(query, conditionals, expression) {
viewRolesBuilder(conditionals, expression)(query);
},
filterByDraft(query) {
query.where('published_at', null);
},
filterByPublished(query) {
query.whereNot('published_at', null);
},
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
default:
query.modify('filterByPublished');
break;
}
},
publish(query) {
query.update({
publishedAt: moment().toMySqlDateTime(),
});
},
/**
* Filters the expenses have billable amount.
*/
billable(query) {
query.where(raw('AMOUNT > INVOICED_AMOUNT'));
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('models/Account');
const ExpenseCategory = require('models/ExpenseCategory');
const Media = require('models/Media');
const Branch = require('models/Branch');
return {
paymentAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'expenses_transactions.paymentAccountId',
to: 'accounts.id',
},
},
categories: {
relation: Model.HasManyRelation,
modelClass: ExpenseCategory.default,
join: {
from: 'expenses_transactions.id',
to: 'expense_transaction_categories.expenseId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
/**
* Expense transction may belongs to a branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'expenses_transactions.branchId',
to: 'branches.id',
},
},
media: {
relation: Model.ManyToManyRelation,
modelClass: Media.default,
join: {
from: 'expenses_transactions.id',
through: {
from: 'media_links.model_id',
to: 'media_links.media_id',
},
to: 'media.id',
},
filter(query) {
query.where('model_name', 'Expense');
},
},
};
}
static get meta() {
return ExpenseSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,44 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ExpenseCategory extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'expense_transaction_categories';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['unallocatedCostAmount'];
}
/**
* Remain unallocated landed cost.
* @return {number}
*/
get unallocatedCostAmount() {
return Math.max(this.amount - this.allocatedCostAmount, 0);
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('models/Account');
return {
expenseAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'expense_transaction_categories.expenseAccountId',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -0,0 +1,59 @@
export default {
defaultFilterField: 'date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'date',
},
fields: {
date: {
name: 'inventory_adjustment.field.date',
column: 'date',
fieldType: 'date',
},
type: {
name: 'inventory_adjustment.field.type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'increment', name: 'inventory_adjustment.field.type.increment' },
{ key: 'decrement', name: 'inventory_adjustment.field.type.decrement' },
],
},
adjustment_account: {
name: 'inventory_adjustment.field.adjustment_account',
column: 'adjustment_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'adjustmentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
reason: {
name: 'inventory_adjustment.field.reason',
column: 'reason',
fieldType: 'text',
},
reference_no: {
name: 'inventory_adjustment.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
description: {
name: 'inventory_adjustment.field.description',
column: 'description',
fieldType: 'text',
},
published_at: {
name: 'inventory_adjustment.field.published_at',
column: 'published_at',
fieldType: 'date',
},
created_at: {
name: 'inventory_adjustment.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,113 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import InventoryAdjustmentSettings from './InventoryAdjustment.Settings';
import ModelSetting from './ModelSetting';
export default class InventoryAdjustment extends mixin(TenantModel, [
ModelSetting,
]) {
/**
* Table name
*/
static get tableName() {
return 'inventory_adjustments';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['formattedType', 'inventoryDirection', 'isPublished'];
}
/**
* Retrieve formatted adjustment type.
*/
get formattedType() {
return InventoryAdjustment.getFormattedType(this.type);
}
/**
* Retrieve formatted reference type.
*/
get inventoryDirection() {
return InventoryAdjustment.getInventoryDirection(this.type);
}
/**
* Detarmines whether the adjustment is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
static getInventoryDirection(type) {
const directions = {
increment: 'IN',
decrement: 'OUT',
};
return directions[type] || '';
}
/**
* Retrieve the formatted adjustment type of the given type.
* @param {string} type
* @returns {string}
*/
static getFormattedType(type) {
const types = {
increment: 'inventory_adjustment.type.increment',
decrement: 'inventory_adjustment.type.decrement',
};
return types[type];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry');
const Account = require('models/Account');
return {
/**
* Adjustment entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: InventoryAdjustmentEntry.default,
join: {
from: 'inventory_adjustments.id',
to: 'inventory_adjustments_entries.adjustmentId',
},
},
/**
* Inventory adjustment account.
*/
adjustmentAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'inventory_adjustments.adjustmentAccountId',
to: 'accounts.id',
},
},
};
}
/**
* Model settings.
*/
static get meta() {
return InventoryAdjustmentSettings;
}
}

View File

@@ -0,0 +1,42 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class InventoryAdjustmentEntry extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'inventory_adjustments_entries';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const InventoryAdjustment = require('models/InventoryAdjustment');
const Item = require('models/Item');
return {
inventoryAdjustment: {
relation: Model.BelongsToOneRelation,
modelClass: InventoryAdjustment.default,
join: {
from: 'inventory_adjustments_entries.adjustmentId',
to: 'inventory_adjustments.id',
},
},
/**
* Entry item.
*/
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item.default,
join: {
from: 'inventory_adjustments_entries.itemId',
to: 'items.id',
},
},
};
}
}

View File

@@ -0,0 +1,112 @@
import { Model } from 'objection';
import { castArray, isEmpty } from 'lodash';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
export default class InventoryCostLotTracker extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'inventory_cost_lot_tracker';
}
/**
* Model timestamps.
*/
static get timestamps() {
return [];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
groupedEntriesCost(query) {
query.select(['date', 'item_id', 'transaction_id', 'transaction_type']);
query.sum('cost as cost');
query.groupBy('transaction_id');
query.groupBy('transaction_type');
query.groupBy('date');
query.groupBy('item_id');
},
filterDateRange(query, startDate, endDate, type = 'day') {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const fromDate = moment(startDate).startOf(type).format(dateFormat);
const toDate = moment(endDate).endOf(type).format(dateFormat);
if (startDate) {
query.where('date', '>=', fromDate);
}
if (endDate) {
query.where('date', '<=', toDate);
}
},
/**
* Filters transactions by the given branches.
*/
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
/**
* Filters transactions by the given warehosues.
*/
filterByWarehouses(query, branchesIds) {
const formattedWarehousesIds = castArray(branchesIds);
query.whereIn('warehouseId', formattedWarehousesIds);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Item = require('models/Item');
const SaleInvoice = require('models/SaleInvoice');
const ItemEntry = require('models/ItemEntry');
const SaleReceipt = require('models/SaleReceipt');
return {
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item.default,
join: {
from: 'inventory_cost_lot_tracker.itemId',
to: 'items.id',
},
},
invoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'inventory_cost_lot_tracker.transactionId',
to: 'sales_invoices.id',
},
},
itemEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'inventory_cost_lot_tracker.entryId',
to: 'items_entries.id',
},
},
receipt: {
relation: Model.BelongsToOneRelation,
modelClass: SaleReceipt.default,
join: {
from: 'inventory_cost_lot_tracker.transactionId',
to: 'sales_receipts.id',
},
},
};
}
}

View File

@@ -0,0 +1,168 @@
import { Model, raw } from 'objection';
import { castArray, isEmpty } from 'lodash';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
export default class InventoryTransaction extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'inventory_transactions';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Retrieve formatted reference type.
* @return {string}
*/
get transcationTypeFormatted() {
return InventoryTransaction.getReferenceTypeFormatted(this.transactionType);
}
/**
* Reference type formatted.
*/
static getReferenceTypeFormatted(referenceType) {
const mapped = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment receive',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
LandedCost: 'transaction_type.landed_cost',
};
return mapped[referenceType] || '';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterDateRange(query, startDate, endDate, type = 'day') {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const fromDate = moment(startDate).startOf(type).format(dateFormat);
const toDate = moment(endDate).endOf(type).format(dateFormat);
if (startDate) {
query.where('date', '>=', fromDate);
}
if (endDate) {
query.where('date', '<=', toDate);
}
},
itemsTotals(builder) {
builder.select('itemId');
builder.sum('rate as rate');
builder.sum('quantity as quantity');
builder.select(raw('SUM(`QUANTITY` * `RATE`) as COST'));
builder.groupBy('itemId');
},
INDirection(builder) {
builder.where('direction', 'IN');
},
OUTDirection(builder) {
builder.where('direction', 'OUT');
},
/**
* Filters transactions by the given branches.
*/
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branch_id', formattedBranchesIds);
},
/**
* Filters transactions by the given warehosues.
*/
filterByWarehouses(query, warehousesIds) {
const formattedWarehousesIds = castArray(warehousesIds);
query.whereIn('warehouse_id', formattedWarehousesIds);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Item = require('models/Item');
const ItemEntry = require('models/ItemEntry');
const InventoryTransactionMeta = require('models/InventoryTransactionMeta');
const InventoryCostLots = require('models/InventoryCostLotTracker');
return {
// Transaction meta.
meta: {
relation: Model.HasOneRelation,
modelClass: InventoryTransactionMeta.default,
join: {
from: 'inventory_transactions.id',
to: 'inventory_transaction_meta.inventoryTransactionId',
},
},
// Item cost aggregated.
itemCostAggregated: {
relation: Model.HasOneRelation,
modelClass: InventoryCostLots.default,
join: {
from: 'inventory_transactions.itemId',
to: 'inventory_cost_lot_tracker.itemId',
},
filter(query) {
query.select('itemId');
query.sum('cost as cost');
query.sum('quantity as quantity');
query.groupBy('itemId');
},
},
costLotAggregated: {
relation: Model.HasOneRelation,
modelClass: InventoryCostLots.default,
join: {
from: 'inventory_transactions.id',
to: 'inventory_cost_lot_tracker.inventoryTransactionId',
},
filter(query) {
query.sum('cost as cost');
query.sum('quantity as quantity');
query.groupBy('inventoryTransactionId');
},
},
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item.default,
join: {
from: 'inventory_transactions.itemId',
to: 'items.id',
},
},
itemEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'inventory_transactions.entryId',
to: 'items_entries.id',
},
},
};
}
}

View File

@@ -0,0 +1,29 @@
import { Model, raw } from 'objection';
import TenantModel from 'models/TenantModel';
export default class InventoryTransactionMeta extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'inventory_transaction_meta';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const InventoryTransactions = require('models/InventoryTransaction');
return {
inventoryTransaction: {
relation: Model.BelongsToOneRelation,
modelClass: InventoryTransactions.default,
join: {
from: 'inventory_transaction_meta.inventoryTransactionId',
to: 'inventory_transactions.inventoryTransactionId'
}
}
};
}
}

View File

@@ -0,0 +1,123 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
'type': {
name: 'item.field.type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'inventory', label: 'item.field.type.inventory', },
{ key: 'service', label: 'item.field.type.service' },
{ key: 'non-inventory', label: 'item.field.type.non-inventory', },
],
},
'name': {
name: 'item.field.name',
column: 'name',
fieldType: 'text',
},
'code': {
name: 'item.field.code',
column: 'code',
fieldType: 'text',
},
'sellable': {
name: 'item.field.sellable',
column: 'sellable',
fieldType: 'boolean',
},
'purchasable': {
name: 'item.field.purchasable',
column: 'purchasable',
fieldType: 'boolean',
},
'sell_price': {
name: 'item.field.cost_price',
column: 'sell_price',
fieldType: 'number',
},
'cost_price': {
name: 'item.field.cost_account',
column: 'cost_price',
fieldType: 'number',
},
'cost_account': {
name: 'item.field.sell_account',
column: 'cost_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'costAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'sell_account': {
name: 'item.field.sell_description',
column: 'sell_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'sellAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'inventory_account': {
name: 'item.field.inventory_account',
column: 'inventory_account_id',
relationType: 'enumeration',
relationKey: 'inventoryAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'sell_description': {
name: 'Sell description',
column: 'sell_description',
fieldType: 'text',
},
'purchase_description': {
name: 'Purchase description',
column: 'purchase_description',
fieldType: 'text',
},
'quantity_on_hand': {
name: 'item.field.quantity_on_hand',
column: 'quantity_on_hand',
fieldType: 'number',
},
'note': {
name: 'item.field.note',
column: 'note',
fieldType: 'text',
},
'category': {
name: 'item.field.category',
column: 'category_id',
relationType: 'enumeration',
relationKey: 'category',
relationEntityLabel: 'name',
relationEntityKey: 'id',
},
'active': {
name: 'item.field.active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
'created_at': {
name: 'item.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,218 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { buildFilterQuery } from '@/lib/ViewRolesBuilder';
import ItemSettings from './Item.Settings';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Items/constants';
import ModelSearchable from './ModelSearchable';
export default class Item extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'items';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
sortBy(query, columnSort, sortDirection) {
query.orderBy(columnSort, sortDirection);
},
viewRolesBuilder(query, conditions, logicExpression) {
buildFilterQuery(Item.tableName, conditions, logicExpression)(query);
},
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('items.active', !active);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Media = require('models/Media');
const Account = require('models/Account');
const ItemCategory = require('models/ItemCategory');
const ItemWarehouseQuantity = require('models/ItemWarehouseQuantity');
const ItemEntry = require('models/ItemEntry');
const WarehouseTransferEntry = require('models/WarehouseTransferEntry');
const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry');
return {
/**
* Item may belongs to cateogory model.
*/
category: {
relation: Model.BelongsToOneRelation,
modelClass: ItemCategory.default,
join: {
from: 'items.categoryId',
to: 'items_categories.id',
},
},
/**
* Item may belongs to cost account.
*/
costAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'items.costAccountId',
to: 'accounts.id',
},
},
/**
* Item may belongs to sell account.
*/
sellAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'items.sellAccountId',
to: 'accounts.id',
},
},
/**
* Item may belongs to inventory account.
*/
inventoryAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'items.inventoryAccountId',
to: 'accounts.id',
},
},
/**
* Item has many warehouses quantities.
*/
itemWarehouses: {
relation: Model.HasManyRelation,
modelClass: ItemWarehouseQuantity.default,
join: {
from: 'items.id',
to: 'items_warehouses_quantity.itemId',
},
},
/**
* Item may has many item entries.
*/
itemEntries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'items.id',
to: 'items_entries.itemId',
},
},
/**
* Item may has many warehouses transfers entries.
*/
warehousesTransfersEntries: {
relation: Model.HasManyRelation,
modelClass: WarehouseTransferEntry.default,
join: {
from: 'items.id',
to: 'warehouses_transfers_entries.itemId',
},
},
/**
* Item has many inventory adjustment entries.
*/
inventoryAdjustmentsEntries: {
relation: Model.HasManyRelation,
modelClass: InventoryAdjustmentEntry.default,
join: {
from: 'items.id',
to: 'inventory_adjustments_entries.itemId',
},
},
/**
*
*/
media: {
relation: Model.ManyToManyRelation,
modelClass: Media.default,
join: {
from: 'items.id',
through: {
from: 'media_links.model_id',
to: 'media_links.media_id',
},
to: 'media.id',
},
},
};
}
/**
*
*/
static get secureDeleteRelations() {
return [
'itemEntries',
'inventoryAdjustmentsEntries',
'warehousesTransfersEntries',
];
}
/**
* Model settings.
*/
static get meta() {
return ItemSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
}

View File

@@ -0,0 +1,30 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
name: {
name: 'item_category.field.name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'item_category.field.description',
column: 'description',
fieldType: 'text',
},
count: {
name: 'item_category.field.count',
column: 'count',
fieldType: 'number',
virtualColumn: true,
},
created_at: {
name: 'item_category.field.created_at',
column: 'created_at',
columnType: 'date',
},
},
};

View File

@@ -0,0 +1,62 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import ItemCategorySettings from './ItemCategory.Settings';
export default class ItemCategory extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
static get tableName() {
return 'items_categories';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Item = require('models/Item');
return {
/**
* Item category may has many items.
*/
items: {
relation: Model.HasManyRelation,
modelClass: Item.default,
join: {
from: 'items_categories.id',
to: 'items.categoryId',
},
},
};
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
sortByCount(query, order = 'asc') {
query.orderBy('count', order);
},
};
}
/**
* Model meta.
*/
static get meta() {
return ItemCategorySettings;
}
}

View File

@@ -0,0 +1,135 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ItemEntry extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'items_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
static get virtualAttributes() {
return ['amount'];
}
get amount() {
return ItemEntry.calcAmount(this);
}
static calcAmount(itemEntry) {
const { discount, quantity, rate } = itemEntry;
const total = quantity * rate;
return discount ? total - total * discount * 0.01 : total;
}
static get relationMappings() {
const Item = require('models/Item');
const BillLandedCostEntry = require('models/BillLandedCostEntry');
const SaleInvoice = require('models/SaleInvoice');
const Bill = require('models/Bill');
const SaleReceipt = require('models/SaleReceipt');
const SaleEstimate = require('models/SaleEstimate');
const ProjectTask = require('models/Task');
const Expense = require('models/Expense');
return {
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item.default,
join: {
from: 'items_entries.itemId',
to: 'items.id',
},
},
allocatedCostEntries: {
relation: Model.HasManyRelation,
modelClass: BillLandedCostEntry.default,
join: {
from: 'items_entries.referenceId',
to: 'bill_located_cost_entries.entryId',
},
},
invoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'items_entries.referenceId',
to: 'sales_invoices.id',
},
},
bill: {
relation: Model.BelongsToOneRelation,
modelClass: Bill.default,
join: {
from: 'items_entries.referenceId',
to: 'bills.id',
},
},
estimate: {
relation: Model.BelongsToOneRelation,
modelClass: SaleEstimate.default,
join: {
from: 'items_entries.referenceId',
to: 'sales_estimates.id',
},
},
receipt: {
relation: Model.BelongsToOneRelation,
modelClass: SaleReceipt.default,
join: {
from: 'items_entries.referenceId',
to: 'sales_receipts.id',
},
},
/**
*
*/
projectTaskRef: {
relation: Model.HasManyRelation,
modelClass: ProjectTask.default,
join: {
from: 'items_entries.projectRefId',
to: 'tasks.id',
},
},
/**
*
*/
projectExpenseRef: {
relation: Model.HasManyRelation,
modelClass: Expense.default,
join: {
from: 'items_entries.projectRefId',
to: 'expenses_transactions.id',
},
},
/**
*
*/
projectBillRef: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'items_entries.projectRefId',
to: 'bills.id',
},
},
};
}
}

View File

@@ -0,0 +1,35 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ItemWarehouseQuantity extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'items_warehouses_quantity';
}
static get relationMappings() {
const Item = require('models/Item');
const Warehouse = require('models/Warehouse');
return {
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item.default,
join: {
from: 'items_warehouses_quantity.itemId',
to: 'items.id',
},
},
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'items_warehouses_quantity.warehouseId',
to: 'warehouses.id',
},
},
};
}
}

View File

@@ -0,0 +1,69 @@
export default {
defaultFilterField: 'date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
'date': {
name: 'manual_journal.field.date',
column: 'date',
fieldType: 'date',
},
'journal_number': {
name: 'manual_journal.field.journal_number',
column: 'journal_number',
fieldType: 'text',
},
'reference': {
name: 'manual_journal.field.reference',
column: 'reference',
fieldType: 'text',
},
'journal_type': {
name: 'manual_journal.field.journal_type',
column: 'journal_type',
fieldType: 'text',
},
'amount': {
name: 'manual_journal.field.amount',
column: 'amount',
fieldType: 'number',
},
'description': {
name: 'manual_journal.field.description',
column: 'description',
fieldType: 'text',
},
'status': {
name: 'manual_journal.field.status',
column: 'status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'published' }
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'manual_journal.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field sorting custom query.
*/
function StatusFieldSortQuery(query, role) {
return query.modify('sortByStatus', role.order);
}
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}

View File

@@ -0,0 +1,170 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { formatNumber } from 'utils';
import ModelSetting from './ModelSetting';
import ManualJournalSettings from './ManualJournal.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/ManualJournals/constants';
import ModelSearchable from './ModelSearchable';
export default class ManualJournal extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'manual_journals';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isPublished', 'amountFormatted'];
}
/**
* Retrieve the amount formatted value.
*/
get amountFormatted() {
return formatNumber(this.amount, { currencyCode: this.currencyCode });
}
/**
* Detarmines whether the invoice is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Sort by status query.
*/
sortByStatus(query, order) {
query.orderByRaw(`PUBLISHED_AT IS NULL ${order}`);
},
/**
* Filter by draft status.
*/
filterByDraft(query) {
query.whereNull('publishedAt');
},
/**
* Filter by published status.
*/
filterByPublished(query) {
query.whereNotNull('publishedAt');
},
/**
* Filter by the given status.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
default:
query.modify('filterByPublished');
break;
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Media = require('models/Media');
const AccountTransaction = require('models/AccountTransaction');
const ManualJournalEntry = require('models/ManualJournalEntry');
return {
entries: {
relation: Model.HasManyRelation,
modelClass: ManualJournalEntry.default,
join: {
from: 'manual_journals.id',
to: 'manual_journals_entries.manualJournalId',
},
filter(query) {
query.orderBy('index', 'ASC');
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'manual_journals.id',
to: 'accounts_transactions.referenceId',
},
filter: (query) => {
query.where('referenceType', 'Journal');
},
},
media: {
relation: Model.ManyToManyRelation,
modelClass: Media.default,
join: {
from: 'manual_journals.id',
through: {
from: 'media_links.model_id',
to: 'media_links.media_id',
},
to: 'media.id',
},
filter(query) {
query.where('model_name', 'ManualJournal');
},
},
};
}
static get meta() {
return ManualJournalSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'journal_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,54 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ManualJournalEntry extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'manual_journals_entries';
}
/**
* Model timestamps.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('models/Account');
const Contact = require('models/Contact');
const Branch = require('models/Branch');
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'manual_journals_entries.accountId',
to: 'accounts.id',
},
},
contact: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
join: {
from: 'manual_journals_entries.contactId',
to: 'contacts.id',
},
},
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'manual_journals_entries.branchId',
to: 'branches.id',
},
},
};
}
}

View File

@@ -0,0 +1,36 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Media extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'media';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const MediaLink = require('models/MediaLink');
return {
links: {
relation: Model.HasManyRelation,
modelClass: MediaLink.default,
join: {
from: 'media.id',
to: 'media_links.media_id',
},
},
};
}
}

View File

@@ -0,0 +1,10 @@
import TenantModel from 'models/TenantModel';
export default class MediaLink extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'media_links';
}
}

View File

@@ -0,0 +1,281 @@
import knex from '@/database/knex';
// import cache from 'memory-cache';
// Metadata
export default {
METADATA_GROUP: 'default',
KEY_COLUMN: 'key',
VALUE_COLUMN: 'value',
TYPE_COLUMN: 'type',
extraColumns: [],
metadata: [],
shouldReload: true,
extraMetadataQuery: () => {},
/**
* Set the value column key to query from.
* @param {String} name -
*/
setKeyColumnName(name) {
this.KEY_COLUMN = name;
},
/**
* Set the key column name to query from.
* @param {String} name -
*/
setValueColumnName(name) {
this.VALUE_COLUMN = name;
},
/**
* Set extra columns to be added to the rows.
* @param {Array} columns -
*/
setExtraColumns(columns) {
this.extraColumns = columns;
},
/**
* Metadata database query.
* @param {Object} query -
* @param {String} groupName -
*/
whereQuery(query, key) {
const groupName = this.METADATA_GROUP;
if (groupName) {
query.where('group', groupName);
}
if (key) {
if (Array.isArray(key)) {
query.whereIn('key', key);
} else {
query.where('key', key);
}
}
},
/**
* Loads the metadata from the storage.
* @param {String|Array} key -
* @param {Boolean} force -
*/
async load(force = false) {
if (this.shouldReload || force) {
const metadataCollection = await this.query((query) => {
this.whereQuery(query);
this.extraMetadataQuery(query);
}).fetchAll();
this.shouldReload = false;
this.metadata = [];
const metadataArray = this.mapMetadataCollection(metadataCollection);
metadataArray.forEach((metadata) => { this.metadata.push(metadata); });
}
},
/**
* Fetches all the metadata that associate with the current group.
*/
async allMeta(force = false) {
await this.load(force);
return this.metadata;
},
/**
* Find the given metadata key.
* @param {String} key -
* @return {object} - Metadata object.
*/
findMeta(key) {
return this.metadata.find((meta) => meta.key === key);
},
/**
* Fetch the metadata of the current group.
* @param {*} key -
*/
async getMeta(key, defaultValue, force = false) {
await this.load(force);
const metadata = this.findMeta(key);
return metadata ? metadata.value : defaultValue || false;
},
/**
* Markes the metadata to should be deleted.
* @param {String} key -
*/
async removeMeta(key) {
await this.load();
const metadata = this.findMeta(key);
if (metadata) {
metadata.markAsDeleted = true;
}
this.shouldReload = true;
},
/**
* Remove all meta data of the given group.
* @param {*} group
*/
removeAllMeta(group = 'default') {
this.metdata.map((meta) => ({
...(meta.group !== group) ? { markAsDeleted: true } : {},
...meta,
}));
this.shouldReload = true;
},
/**
* Set the meta data to the stack.
* @param {String} key -
* @param {String} value -
*/
async setMeta(key, value, payload) {
if (Array.isArray(key)) {
const metadata = key;
metadata.forEach((meta) => {
this.setMeta(meta.key, meta.value);
});
return;
}
await this.load();
const metadata = this.findMeta(key);
if (metadata) {
metadata.value = value;
metadata.markAsUpdated = true;
} else {
this.metadata.push({
value, key, ...payload, markAsInserted: true,
});
}
},
/**
* Saved the modified metadata.
*/
async saveMeta() {
const inserted = this.metadata.filter((m) => (m.markAsInserted === true));
const updated = this.metadata.filter((m) => (m.markAsUpdated === true));
const deleted = this.metadata.filter((m) => (m.markAsDeleted === true));
const metadataDeletedKeys = deleted.map((m) => m.key);
const metadataInserted = inserted.map((m) => this.mapMetadata(m, 'format'));
const metadataUpdated = updated.map((m) => this.mapMetadata(m, 'format'));
const batchUpdate = (collection) => knex.transaction((trx) => {
const queries = collection.map((tuple) => {
const query = knex(this.tableName);
this.whereQuery(query, tuple.key);
this.extraMetadataQuery(query);
return query.update(tuple).transacting(trx);
});
return Promise.all(queries).then(trx.commit).catch(trx.rollback);
});
await Promise.all([
knex.insert(metadataInserted).into(this.tableName),
batchUpdate(metadataUpdated),
metadataDeletedKeys.length > 0
? this.query('whereIn', this.KEY_COLUMN, metadataDeletedKeys).destroy({
require: true,
}) : null,
]);
this.shouldReload = true;
},
/**
* Purge all the cached metadata in the memory.
*/
purgeMetadata() {
this.metadata = [];
this.shouldReload = true;
},
/**
* Parses the metadata value.
* @param {String} value -
* @param {String} valueType -
*/
parseMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'integer':
parsedValue = parseInt(value, 10);
break;
case 'float':
parsedValue = parseFloat(value);
break;
case 'boolean':
parsedValue = Boolean(value);
break;
case 'json':
parsedValue = JSON.parse(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
},
/**
* Format the metadata before saving to the database.
* @param {String|Number|Boolean} value -
* @param {String} valueType -
* @return {String|Number|Boolean} -
*/
formatMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
},
mapMetadata(attr, parseType = 'parse') {
return {
key: attr[this.KEY_COLUMN],
value: (parseType === 'parse')
? this.parseMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
)
: this.formatMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
),
...this.extraColumns.map((extraCol) => ({
[extraCol]: attr[extraCol] || null,
})),
};
},
/**
* Parse the metadata collection.
* @param {Array} collection -
*/
mapMetadataCollection(collection, parseType = 'parse') {
return collection.map((model) => this.mapMetadata(model.attributes, parseType));
},
};

View File

@@ -0,0 +1,48 @@
import { Model, mixin } from 'objection';
import { snakeCase, transform } from 'lodash';
import { mapKeysDeep } from 'utils';
import PaginationQueryBuilder from 'models/Pagination';
import DateSession from 'models/DateSession';
export default class ModelBase extends mixin(Model, [DateSession]) {
get timestamps() {
return [];
}
static get knexBinded() {
return this.knexBindInstance;
}
static set knexBinded(knex) {
this.knexBindInstance = knex;
}
static get collection() {
return Array;
}
static query(...args) {
return super.query(...args).runAfter((result) => {
if (Array.isArray(result)) {
return this.collection.from(result);
}
return result;
});
}
static get QueryBuilder() {
return PaginationQueryBuilder;
}
static relationBindKnex(model) {
return this.knexBinded ? model.bindKnex(this.knexBinded) : model;
}
static changeAmount(whereAttributes, attribute, amount, trx) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query(trx)
.where(whereAttributes)
[changeMethod](attribute, Math.abs(amount));
}
}

View File

@@ -0,0 +1,18 @@
import { IModelMeta, ISearchRole } from '@/interfaces';
export default (Model) =>
class ModelSearchable extends Model {
/**
* Searchable model.
*/
static get searchable(): IModelMeta {
throw true;
}
/**
* Search roles.
*/
static get searchRoles(): ISearchRole[] {
return [];
}
};

View File

@@ -0,0 +1,56 @@
import { get } from 'lodash';
import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from '@/interfaces';
export default (Model) =>
class ModelSettings extends Model {
/**
*
*/
static get meta(): IModelMeta {
throw new Error('');
}
/**
* Retrieve specific model field meta of the given field key.
* @param {string} key
* @returns {IModelMetaField}
*/
public static getField(key: string, attribute?:string): IModelMetaField {
const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field;
}
/**
* Retrieve the specific model meta.
* @param {string} key
* @returns
*/
public static getMeta(key?: string) {
return key ? get(this.meta, key): this.meta;
}
/**
* Retrieve the model meta fields.
* @return {{ [key: string]: IModelMetaField }}
*/
public static get fields(): { [key: string]: IModelMetaField } {
return this.getMeta('fields');
}
/**
* Retrieve the model default sort settings.
* @return {IModelMetaDefaultSort}
*/
public static get defaultSort(): IModelMetaDefaultSort {
return this.getMeta('defaultSort');
}
/**
* Retrieve the default filter field key.
* @return {string}
*/
public static get defaultFilterField(): string {
return this.getMeta('defaultFilterField');
}
};

View File

@@ -0,0 +1,30 @@
import TenantModel from 'models/TenantModel';
import definedOptions from '@/data/options';
export default class Option extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'options';
}
/**
* Validates the given options is defined or either not.
* @param {Array} options
* @return {Boolean}
*/
static validateDefined(options) {
const notDefined = [];
options.forEach((option) => {
if (!definedOptions[option.group]) {
notDefined.push(option);
} else if (!definedOptions[option.group].some((o) => o.key === option.key)) {
notDefined.push(option);
}
});
return notDefined;
}
}

View File

@@ -0,0 +1,48 @@
import { Model } from 'objection';
import { isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
export default class PaginationQueryBuilder extends Model.QueryBuilder {
pagination(page, pageSize) {
return super.page(page, pageSize).runAfter(({ results, total }) => {
return {
results,
pagination: {
total,
page: page + 1,
pageSize,
},
};
});
}
queryAndThrowIfHasRelations = ({ type, message }) => {
const model = this.modelClass();
const modelRelations = Object.keys(model.relationMappings).filter(
(relation) =>
[Model.HasManyRelation, Model.HasOneRelation].indexOf(
model.relationMappings[relation]?.relation
) !== -1
);
const relations = model.secureDeleteRelations || modelRelations;
this.runAfter((model, query) => {
const nonEmptyRelations = relations.filter(
(relation) => !isEmpty(model[relation])
);
if (nonEmptyRelations.length > 0) {
throw new ServiceError(type || 'MODEL_HAS_RELATIONS', { message });
}
return model;
});
return this.onBuild((query) => {
relations.forEach((relation) => {
query.withGraphFetched(`${relation}(selectId)`).modifiers({
selectId(builder) {
builder.select('id');
},
});
});
});
};
}

View File

@@ -0,0 +1,57 @@
export default {
fields: {
customer: {
name: 'payment_receive.field.customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
payment_date: {
name: 'payment_receive.field.payment_date',
column: 'payment_date',
fieldType: 'date',
},
amount: {
name: 'payment_receive.field.amount',
column: 'amount',
fieldType: 'number',
},
reference_no: {
name: 'payment_receive.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
deposit_account: {
name: 'payment_receive.field.deposit_account',
column: 'deposit_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'depositAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
payment_receive_no: {
name: 'payment_receive.field.payment_receive_no',
column: 'payment_receive_no',
fieldType: 'text',
},
statement: {
name: 'payment_receive.field.statement',
column: 'statement',
fieldType: 'text',
},
created_at: {
name: 'payment_receive.field.created_at',
column: 'created_at',
fieldDate: 'date',
},
},
};

View File

@@ -0,0 +1,148 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import PaymentReceiveSettings from './PaymentReceive.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
import ModelSearchable from './ModelSearchable';
export default class PaymentReceive extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'payment_receives';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
}
/**
* Payment receive amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Resourcable model.
*/
static get resourceable() {
return true;
}
/*
* Relationship mapping.
*/
static get relationMappings() {
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const AccountTransaction = require('models/AccountTransaction');
const Customer = require('models/Customer');
const Account = require('models/Account');
const Branch = require('models/Branch');
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
join: {
from: 'payment_receives.customerId',
to: 'contacts.id',
},
filter: (query) => {
query.where('contact_service', 'customer');
},
},
depositAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'payment_receives.depositAccountId',
to: 'accounts.id',
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: PaymentReceiveEntry.default,
join: {
from: 'payment_receives.id',
to: 'payment_receives_entries.paymentReceiveId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'payment_receives.id',
to: 'accounts_transactions.referenceId',
},
filter: (builder) => {
builder.where('reference_type', 'PaymentReceive');
},
},
/**
* Payment receive may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'payment_receives.branchId',
to: 'branches.id',
},
},
};
}
/**
*
*/
static get meta() {
return PaymentReceiveSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'payment_receive_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,51 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
export default class PaymentReceiveEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'payment_receives_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const PaymentReceive = require('models/PaymentReceive');
const SaleInvoice = require('models/SaleInvoice');
return {
/**
*/
payment: {
relation: Model.BelongsToOneRelation,
modelClass: PaymentReceive.default,
join: {
from: 'payment_receives_entries.paymentReceiveId',
to: 'payment_receives.id',
},
},
/**
* The payment receive entry have have sale invoice.
*/
invoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'payment_receives_entries.invoiceId',
to: 'sales_invoices.id',
},
},
};
}
}

View File

@@ -0,0 +1,157 @@
import { mixin, Model } from 'objection';
import TenantModel from 'models/TenantModel';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSetting from './ModelSetting';
import ModelSearchable from './ModelSearchable';
export default class Project extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
costEstimate!: number;
deadline!: Date;
/**
* Table name
*/
static get tableName() {
return 'projects';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
totalExpensesDetails(builder) {
builder
.withGraphFetched('expenses')
.modifyGraph('expenses', (builder) => {
builder.select(['projectId']);
builder.groupBy('projectId');
builder.sum('totalAmount as totalExpenses');
builder.sum('invoicedAmount as totalInvoicedExpenses');
});
},
totalBillsDetails(builder) {
builder.withGraphFetched('tasks').modifyGraph('tasks', (builder) => {
builder.select(['projectId']);
builder.groupBy('projectId');
builder.modify('sumTotalActualHours');
builder.modify('sumTotalEstimateHours');
builder.modify('sumTotalInvoicedHours');
builder.modify('sumTotalActualAmount');
builder.modify('sumTotalInvoicedAmount');
builder.modify('sumTotalEstimateAmount');
});
},
totalTasksDetails(builder) {
builder.withGraphFetched('tasks').modifyGraph('tasks', (builder) => {
builder.select(['projectId']);
builder.groupBy('projectId');
builder.modify('sumTotalActualHours');
builder.modify('sumTotalEstimateHours');
builder.modify('sumTotalInvoicedHours');
builder.modify('sumTotalActualAmount');
builder.modify('sumTotalInvoicedAmount');
builder.modify('sumTotalEstimateAmount');
});
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Contact = require('models/Contact');
const Task = require('models/Task');
const Time = require('models/Time');
const Expense = require('models/Expense');
const Bill = require('models/Bill');
return {
/**
* Belongs to customer model.
*/
contact: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
join: {
from: 'projects.contactId',
to: 'contacts.id',
},
},
/**
* Project may has many associated tasks.
*/
tasks: {
relation: Model.HasManyRelation,
modelClass: Task.default,
join: {
from: 'projects.id',
to: 'tasks.projectId',
},
},
/**
* Project may has many associated times.
*/
times: {
relation: Model.HasManyRelation,
modelClass: Time.default,
join: {
from: 'projects.id',
to: 'times.projectId',
},
},
/**
* Project may has many associated expenses.
*/
expenses: {
relation: Model.HasManyRelation,
modelClass: Expense.default,
join: {
from: 'projects.id',
to: 'expenses_transactions.projectId',
},
},
/**
* Project may has many associated bills.
*/
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'projects.id',
to: 'bills.projectId',
},
},
};
}
}

View File

@@ -0,0 +1,47 @@
import { mixin, Model } from 'objection';
import TenantModel from 'models/TenantModel';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSetting from './ModelSetting';
import ModelSearchable from './ModelSearchable';
export default class ProjectItemEntryRef extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'projects_item_entries_links';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
static get relationMappings() {
const ItemEntry = require('models/ItemEntry');
return {
itemEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry.default,
join: {
from: 'projects_item_entries_links.itemEntryId',
to: 'items_entries.id',
},
},
};
}
}

View File

@@ -0,0 +1,52 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSearchable from './ModelSearchable';
export default class RefundCreditNote extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'refund_credit_note_transactions';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/*
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('models/Account');
const CreditNote = require('models/CreditNote');
return {
fromAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'refund_credit_note_transactions.fromAccountId',
to: 'accounts.id',
},
},
creditNote: {
relation: Model.BelongsToOneRelation,
modelClass: CreditNote.default,
join: {
from: 'refund_credit_note_transactions.creditNoteId',
to: 'credit_notes.id',
},
},
};
}
}

View File

@@ -0,0 +1,52 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSearchable from './ModelSearchable';
export default class RefundVendorCredit extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'refund_vendor_credit_transactions';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/*
* Relationship mapping.
*/
static get relationMappings() {
const VendorCredit = require('models/VendorCredit');
const Account = require('models/Account');
return {
depositAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'refund_vendor_credit_transactions.depositAccountId',
to: 'accounts.id',
},
},
vendorCredit: {
relation: Model.BelongsToOneRelation,
modelClass: VendorCredit.default,
join: {
from: 'refund_vendor_credit_transactions.vendorCreditId',
to: 'vendor_credits.id',
},
},
};
}
}

View File

@@ -0,0 +1,8 @@
export default class ResourceableModel {
static get resourceable() {
return true;
}
}

View File

@@ -0,0 +1,33 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Role extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'roles';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const RolePermission = require('models/RolePermission');
return {
/**
*
*/
permissions: {
relation: Model.HasManyRelation,
modelClass: RolePermission.default,
join: {
from: 'roles.id',
to: 'role_permissions.roleId',
},
},
};
}
}

View File

@@ -0,0 +1,39 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSearchable from './ModelSearchable';
export default class RolePermission extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'role_permissions';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Role = require('models/Role');
return {
/**
*
*/
role: {
relation: Model.BelongsToOneRelation,
modelClass: Role.default,
join: {
from: 'role_permissions.roleId',
to: 'roles.id',
},
},
};
}
}

View File

@@ -0,0 +1,80 @@
export default {
defaultFilterField: 'estimate_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'estimate_date',
},
fields: {
'amount': {
name: 'estimate.field.amount',
column: 'amount',
fieldType: 'number',
},
'estimate_number': {
name: 'estimate.field.estimate_number',
column: 'estimate_number',
fieldType: 'text',
},
'customer': {
name: 'estimate.field.customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'estimate_date': {
name: 'estimate.field.estimate_date',
column: 'estimate_date',
fieldType: 'date',
},
'expiration_date': {
name: 'estimate.field.expiration_date',
column: 'expiration_date',
fieldType: 'date',
},
'reference_no': {
name: 'estimate.field.reference_no',
column: 'reference',
fieldType: 'text',
},
'note': {
name: 'estimate.field.note',
column: 'note',
fieldType: 'text',
},
'terms_conditions': {
name: 'estimate.field.terms_conditions',
column: 'terms_conditions',
fieldType: 'text',
},
'status': {
name: 'estimate.field.status',
fieldType: 'enumeration',
options: [
{ label: 'estimate.field.status.delivered', key: 'delivered' },
{ label: 'estimate.field.status.rejected', key: 'rejected' },
{ label: 'estimate.field.status.approved', key: 'approved' },
{ label: 'estimate.field.status.draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'estimate.field.created_at',
column: 'created_at',
columnType: 'date',
},
},
};
function StatusFieldSortQuery(query, role) {
query.modify('orderByStatus', role.order);
}
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}

View File

@@ -0,0 +1,256 @@
import moment from 'moment';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils';
import SaleEstimateSettings from './SaleEstimate.Settings';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants';
import ModelSearchable from './ModelSearchable';
export default class SaleEstimate extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'sales_estimates';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'localAmount',
'isDelivered',
'isExpired',
'isConvertedToInvoice',
'isApproved',
'isRejected',
];
}
/**
* Estimate amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Detarmines whether the sale estimate converted to sale invoice.
* @return {boolean}
*/
get isConvertedToInvoice() {
return !!(this.convertedToInvoiceId && this.convertedToInvoiceAt);
}
/**
* Detarmines whether the estimate is delivered.
* @return {boolean}
*/
get isDelivered() {
return !!this.deliveredAt;
}
/**
* Detarmines whether the estimate is expired.
* @return {boolean}
*/
get isExpired() {
return defaultToTransform(
this.expirationDate,
moment().isAfter(this.expirationDate, 'day'),
false
);
}
/**
* Detarmines whether the estimate is approved.
* @return {boolean}
*/
get isApproved() {
return !!this.approvedAt;
}
/**
* Detarmines whether the estimate is reject.
* @return {boolean}
*/
get isRejected() {
return !!this.rejectedAt;
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the drafted estimates transactions.
*/
draft(query) {
query.where('delivered_at', null);
},
/**
* Filters the delivered estimates transactions.
*/
delivered(query) {
query.whereNot('delivered_at', null);
},
/**
* Filters the expired estimates transactions.
*/
expired(query) {
query.where('expiration_date', '<', moment().format('YYYY-MM-DD'));
},
/**
* Filters the rejected estimates transactions.
*/
rejected(query) {
query.whereNot('rejected_at', null);
},
/**
* Filters the invoiced estimates transactions.
*/
invoiced(query) {
query.whereNot('converted_to_invoice_at', null);
},
/**
* Filters the approved estimates transactions.
*/
approved(query) {
query.whereNot('approved_at', null);
},
/**
* Sorting the estimates orders by delivery status.
*/
orderByStatus(query, order) {
query.orderByRaw(`delivered_at is null ${order}`);
},
/**
* Filtering the estimates oreders by status field.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'approved':
query.modify('approved');
break;
case 'rejected':
query.modify('rejected');
break;
case 'invoiced':
query.modify('invoiced');
break;
case 'expired':
query.modify('expired');
break;
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer');
const Branch = require('models/Branch');
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
join: {
from: 'sales_estimates.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'customer');
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'sales_estimates.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleEstimate');
builder.orderBy('index', 'ASC');
},
},
/**
* Sale estimate may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'sales_estimates.branchId',
to: 'branches.id',
},
},
};
}
/**
* Model settings.
*/
static get meta() {
return SaleEstimateSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ fieldKey: 'amount', comparator: 'equals' },
{ condition: 'or', fieldKey: 'estimate_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,30 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class SaleEstimateEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'sales_estimate_entries';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleEstimate = require('models/SaleEstimate');
return {
estimate: {
relation: Model.BelongsToOneRelation,
modelClass: SaleEstimate.default,
join: {
from: 'sales_estimates.id',
to: 'sales_estimate_entries.estimate_id',
},
},
};
}
}

View File

@@ -0,0 +1,100 @@
export default {
defaultFilterField: 'customer',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
customer: {
name: 'invoice.field.customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
invoice_date: {
name: 'invoice.field.invoice_date',
column: 'invoice_date',
fieldType: 'date',
},
due_date: {
name: 'invoice.field.due_date',
column: 'due_date',
fieldType: 'date',
},
invoice_no: {
name: 'invoice.field.invoice_no',
column: 'invoice_no',
fieldType: 'text',
},
reference_no: {
name: 'invoice.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
invoice_message: {
name: 'invoice.field.invoice_message',
column: 'invoice_message',
fieldType: 'text',
},
terms_conditions: {
name: 'invoice.field.terms_conditions',
column: 'terms_conditions',
fieldType: 'text',
},
amount: {
name: 'invoice.field.amount',
column: 'balance',
fieldType: 'number',
},
payment_amount: {
name: 'invoice.field.payment_amount',
column: 'payment_amount',
fieldType: 'number',
},
due_amount: {
// calculated.
name: 'invoice.field.due_amount',
column: 'due_amount',
fieldType: 'number',
virtualColumn: true,
},
status: {
name: 'invoice.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'invoice.field.status.draft' },
{ key: 'delivered', label: 'invoice.field.status.delivered' },
{ key: 'unpaid', label: 'invoice.field.status.unpaid' },
{ key: 'overdue', label: 'invoice.field.status.overdue' },
{ key: 'partially-paid', label: 'invoice.field.status.partially-paid' },
{ key: 'paid', label: 'invoice.field.status.paid' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
created_at: {
name: 'invoice.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('statusFilter', role.value);
}
/**
* Status field sort custom query.
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -0,0 +1,489 @@
import { mixin, Model, raw } from 'objection';
import { castArray } from 'lodash';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import SaleInvoiceMeta from './SaleInvoice.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/constants';
import ModelSearchable from './ModelSearchable';
export default class SaleInvoice extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'sales_invoices';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
get pluralName() {
return 'asdfsdf';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'localAmount',
'dueAmount',
'balanceAmount',
'isDelivered',
'isOverdue',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'isWrittenoff',
'remainingDays',
'overdueDays',
'filterByBranches',
];
}
/**
* Invoice amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.balance * this.exchangeRate;
}
/**
* Invoice local written-off amount.
* @returns {number}
*/
get localWrittenoffAmount() {
return this.writtenoffAmount * this.exchangeRate;
}
/**
* Detarmines whether the invoice is delivered.
* @return {boolean}
*/
get isDelivered() {
return !!this.deliveredAt;
}
/**
* Detarmines the due date is over.
* @return {boolean}
*/
get isOverdue() {
return this.overdueDays > 0;
}
/**
* Retrieve the sale invoice balance.
* @return {number}
*/
get balanceAmount() {
return this.paymentAmount + this.writtenoffAmount + this.creditedAmount;
}
/**
* Retrieve the invoice due amount.
* Equation (Invoice amount - payment amount = Due amount)
* @return {boolean}
*/
get dueAmount() {
return Math.max(this.balance - this.balanceAmount, 0);
}
/**
* Detarmine whether the invoice paid partially.
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.balance && this.dueAmount > 0;
}
/**
* Deetarmine whether the invoice paid fully.
* @return {boolean}
*/
get isFullyPaid() {
return this.dueAmount === 0;
}
/**
* Detarmines whether the invoice paid fully or partially.
* @return {boolean}
*/
get isPaid() {
return this.isPartiallyPaid || this.isFullyPaid;
}
/**
* Detarmines whether the sale invoice is written-off.
* @return {boolean}
*/
get isWrittenoff() {
return Boolean(this.writtenoffAt);
}
/**
* Retrieve the remaining days in number
* @return {number|null}
*/
get remainingDays() {
const dateMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(dueDateMoment.diff(dateMoment, 'days'), 0);
}
/**
* Retrieve the overdue days in number.
* @return {number|null}
*/
get overdueDays() {
const dateMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(dateMoment.diff(dueDateMoment, 'days'), 0);
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the due invoices.
*/
dueInvoices(query) {
query.where(
raw(`
COALESCE(BALANCE, 0) -
COALESCE(PAYMENT_AMOUNT, 0) -
COALESCE(WRITTENOFF_AMOUNT, 0) -
COALESCE(CREDITED_AMOUNT, 0) > 0
`)
);
},
/**
* Filters the invoices between the given date range.
*/
filterDateRange(query, startDate, endDate, type = 'day') {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const fromDate = moment(startDate).startOf(type).format(dateFormat);
const toDate = moment(endDate).endOf(type).format(dateFormat);
if (startDate) {
query.where('invoice_date', '>=', fromDate);
}
if (endDate) {
query.where('invoice_date', '<=', toDate);
}
},
/**
* Filters the invoices in draft status.
*/
draft(query) {
query.where('delivered_at', null);
},
/**
* Filters the published invoices.
*/
published(query) {
query.whereNot('delivered_at', null);
},
/**
* Filters the delivered invoices.
*/
delivered(query) {
query.whereNot('delivered_at', null);
},
/**
* Filters the unpaid invoices.
*/
unpaid(query) {
query.where(raw('PAYMENT_AMOUNT = 0'));
},
/**
* Filters the overdue invoices.
*/
overdue(query, asDate = moment().format('YYYY-MM-DD')) {
query.where('due_date', '<', asDate);
},
/**
* Filters the not overdue invoices.
*/
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
query.where('due_date', '>=', asDate);
},
/**
* Filters the partially invoices.
*/
partiallyPaid(query) {
query.whereNot('payment_amount', 0);
query.whereNot(raw('`PAYMENT_AMOUNT` = `BALANCE`'));
},
/**
* Filters the paid invoices.
*/
paid(query) {
query.where(raw('PAYMENT_AMOUNT = BALANCE'));
},
/**
* Filters the sale invoices from the given date.
*/
fromDate(query, fromDate) {
query.where('invoice_date', '<=', fromDate);
},
/**
* Sort the sale invoices by full-payment invoices.
*/
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = BALANCE ${order}`);
},
/**
* Sort the sale invoices by the due amount.
*/
sortByDueAmount(query, order) {
query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`);
},
/**
* Retrieve the max invoice
*/
maxInvoiceNo(query, prefix, number) {
query
.select(raw(`REPLACE(INVOICE_NO, "${prefix}", "") AS INV_NUMBER`))
.havingRaw('CHAR_LENGTH(INV_NUMBER) = ??', [number.length])
.orderBy('invNumber', 'DESC')
.limit(1)
.first();
},
byPrefixAndNumber(query, prefix, number) {
query.where('invoice_no', `${prefix}${number}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
/**
* Filters by branches.
*/
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
dueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueInvoices');
query.modify('notOverdue', asDate);
query.modify('fromDate', asDate);
},
overdueInvoicesFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueInvoices');
query.modify('overdue', asDate);
query.modify('fromDate', asDate);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const Branch = require('models/Branch');
const Account = require('models/Account');
return {
/**
* Sale invoice associated entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'sales_invoices.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleInvoice');
builder.orderBy('index', 'ASC');
},
},
/**
* Belongs to customer model.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
join: {
from: 'sales_invoices.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'Customer');
},
},
/**
* Invoice has associated account transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'sales_invoices.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleInvoice');
},
},
/**
*
*/
costTransactions: {
relation: Model.HasManyRelation,
modelClass: InventoryCostLotTracker.default,
join: {
from: 'sales_invoices.id',
to: 'inventory_cost_lot_tracker.transactionId',
},
filter(builder) {
builder.where('transaction_type', 'SaleInvoice');
},
},
/**
*
*/
paymentEntries: {
relation: Model.HasManyRelation,
modelClass: PaymentReceiveEntry.default,
join: {
from: 'sales_invoices.id',
to: 'payment_receives_entries.invoiceId',
},
},
/**
* Invoice may has associated branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'sales_invoices.branchId',
to: 'branches.id',
},
},
writtenoffExpenseAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'sales_invoices.writtenoffExpenseAccountId',
to: 'accounts.id',
},
},
};
}
/**
* Change payment amount.
* @param {Integer} invoiceId
* @param {Numeric} amount
*/
static async changePaymentAmount(invoiceId, amount, trx) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
await this.query(trx)
.where('id', invoiceId)
[changeMethod]('payment_amount', Math.abs(amount));
}
/**
* Sale invoice meta.
*/
static get meta() {
return SaleInvoiceMeta;
}
static dueAmountFieldSortQuery(query, role) {
query.modify('sortByDueAmount', role.order);
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model searchable.
*/
static get searchable() {
return true;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'invoice_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,29 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class SaleInvoiceEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'sales_invoices_entries';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleInvoice = require('models/SaleInvoice');
return {
saleInvoice: {
relation: Model.BelongsToOneRelation,
modelClass: SaleInvoice.default,
join: {
from: 'sales_invoices_entries.sale_invoice_id',
to: 'sales_invoices.id',
},
},
};
}
}

View File

@@ -0,0 +1,85 @@
export default {
defaultFilterField: 'receipt_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
'amount': {
name: 'receipt.field.amount',
column: 'amount',
fieldType: 'number',
},
'deposit_account': {
column: 'deposit_account_id',
name: 'receipt.field.deposit_account',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'depositAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'customer': {
name: 'receipt.field.customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'receipt_date': {
name: 'receipt.field.receipt_date',
column: 'receipt_date',
fieldType: 'date',
},
'receipt_number': {
name: 'receipt.field.receipt_number',
column: 'receipt_number',
fieldType: 'text',
},
'reference_no': {
name: 'receipt.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
'receipt_message': {
name: 'receipt.field.receipt_message',
column: 'receipt_message',
fieldType: 'text',
},
'statement': {
name: 'receipt.field.statement',
column: 'statement',
fieldType: 'text',
},
'created_at': {
name: 'receipt.field.created_at',
column: 'created_at',
fieldType: 'date',
},
'status': {
name: 'receipt.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'receipt.field.status.draft' },
{ key: 'closed', label: 'receipt.field.status.closed' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
},
};
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -0,0 +1,204 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import SaleReceiptSettings from './SaleReceipt.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants';
import ModelSearchable from './ModelSearchable';
export default class SaleReceipt extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'sales_receipts';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount', 'isClosed', 'isDraft'];
}
/**
* Estimate amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Detarmine whether the sale receipt closed.
* @return {boolean}
*/
get isClosed() {
return !!this.closedAt;
}
/**
* Detarmines whether the sale receipt drafted.
* @return {boolean}
*/
get isDraft() {
return !this.closedAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the closed receipts.
*/
closed(query) {
query.whereNot('closed_at', null);
},
/**
* Filters the invoices in draft status.
*/
draft(query) {
query.where('closed_at', null);
},
/**
* Sorting the receipts order by status.
*/
sortByStatus(query, order) {
query.orderByRaw(`CLOSED_AT IS NULL ${order}`);
},
/**
* Filtering the receipts orders by status.
*/
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('draft');
break;
case 'closed':
default:
query.modify('closed');
break;
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Customer = require('models/Customer');
const Account = require('models/Account');
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
const Branch = require('models/Branch');
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
join: {
from: 'sales_receipts.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'customer');
},
},
depositAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'sales_receipts.depositAccountId',
to: 'accounts.id',
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'sales_receipts.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleReceipt');
builder.orderBy('index', 'ASC');
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'sales_receipts.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleReceipt');
},
},
/**
* Sale receipt may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'sales_receipts.branchId',
to: 'branches.id',
},
},
};
}
/**
* Sale invoice meta.
*/
static get meta() {
return SaleReceiptSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'receipt_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,29 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class SaleReceiptEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'sales_receipt_entries';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleReceipt = require('models/SaleReceipt');
return {
saleReceipt: {
relation: Model.BelongsToOneRelation,
modelClass: SaleReceipt.default,
join: {
from: 'sales_receipt_entries.sale_receipt_id',
to: 'sales_receipts.id',
},
},
};
}
}

View File

@@ -0,0 +1,21 @@
import TenantModel from 'models/TenantModel';
import Auth from './Auth';
export default class Setting extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'settings';
}
/**
* Extra metadata query to query with the current authenticate user.
* @param {Object} query
*/
static extraMetadataQuery(query) {
if (Auth.isLogged()) {
query.where('user_id', Auth.userId());
}
}
}

View File

@@ -0,0 +1,173 @@
import { mixin, Model, raw } from 'objection';
import TenantModel from 'models/TenantModel';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSetting from './ModelSetting';
import ModelSearchable from './ModelSearchable';
import { ProjectTaskChargeType } from '@/services/Projects/Tasks/constants';
import { number } from 'mathjs';
export default class Task extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
type!: string;
rate!: number;
actualHours!: number;
invoicedHours!: number;
estimateHours!: number;
/**
* Table name
*/
static get tableName() {
return 'tasks';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'actualAmount',
'invoicedAmount',
'estimateAmount',
'billableAmount',
'billableHours',
];
}
/**
* Retrieves the actual amount.
*/
get actualAmount(): number {
return this.rate * this.actualHours;
}
/**
* Retrieves the invoiced amount.
*/
get invoicedAmount(): number {
return this.rate * this.invoicedHours;
}
/**
* Retrieves the estimate amount.
*/
get estimateAmount(): number {
return this.rate * this.estimateHours;
}
/**
* Retrieves the billable amount.
*/
get billableAmount() {
return this.actualAmount - this.invoicedAmount;
}
/**
* Retrieves the billable hours.
*/
get billableHours() {
return this.actualHours - this.invoicedHours;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Sumation of total actual hours.
* @param builder
*/
sumTotalActualHours(builder) {
builder.sum('actualHours as totalActualHours');
},
/**
* Sumation total estimate hours.
* @param builder
*/
sumTotalEstimateHours(builder) {
builder.sum('estimateHours as totalEstimateHours');
},
/**
* Sumation of total invoiced hours.
* @param builder
*/
sumTotalInvoicedHours(builder) {
builder.sum('invoicedHours as totalInvoicedHours');
},
/**
* Sumation of total actual amount.
* @param builder
*/
sumTotalActualAmount(builder) {
builder.groupBy('totalActualAmount');
builder.select(raw('ACTUAL_HOURS * RATE').as('totalActualAmount'));
},
/**
* Sumation of total invoiced amount.
* @param builder
*/
sumTotalInvoicedAmount(builder) {
this.groupBy('totalInvoicedAmount');
builder.select(raw('INVOICED_HOURS * RATE').as('totalInvoicedAmount'));
},
/**
* Sumation of total estimate amount.
* @param builder
*/
sumTotalEstimateAmount(builder) {
builder.groupBy('totalEstimateAmount');
builder.select(raw('ESTIMATE_HOURS * RATE').as('totalEstimateAmount'));
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Time = require('models/Time');
const Project = require('models/Project');
return {
/**
* Project may has many associated tasks.
*/
times: {
relation: Model.HasManyRelation,
modelClass: Time.default,
join: {
from: 'tasks.id',
to: 'times.taskId',
},
},
/**
* Project may has many associated times.
*/
project: {
relation: Model.BelongsToOneRelation,
modelClass: Project.default,
join: {
from: 'tasks.projectId',
to: 'projects.id',
},
},
};
}
}

View File

@@ -0,0 +1,22 @@
import { Container } from 'typedi';
import BaseModel from 'models/Model';
export default class TenantModel extends BaseModel {
/**
* Logging all tenant databases queries.
* @param {...any} args
*/
static query(...args) {
const Logger = Container.get('logger');
return super.query(...args).onBuildKnex((knexQueryBuilder) => {
const { userParams: { tenantId } } = knexQueryBuilder.client.config;
knexQueryBuilder.on('query', queryData => {
Logger.info(`[query][tenant] ${queryData.sql}`, {
bindings: queryData.bindings, tenantId
});
});
});
}
}

View File

@@ -0,0 +1,66 @@
import { mixin, Model } from 'objection';
import TenantModel from 'models/TenantModel';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSetting from './ModelSetting';
import ModelSearchable from './ModelSearchable';
export default class Time extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'times';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Task = require('models/Task');
const Project = require('models/Project');
return {
/**
* Project may has many associated tasks.
*/
task: {
relation: Model.BelongsToOneRelation,
modelClass: Task.default,
join: {
from: 'times.taskId',
to: 'tasks.id',
},
},
/**
* Project may has many associated times.
*/
project: {
relation: Model.BelongsToOneRelation,
modelClass: Project.default,
join: {
from: 'times.projectId',
to: 'projects.id',
},
},
};
}
}

View File

@@ -0,0 +1,60 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class User extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'users';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isInviteAccepted', 'fullName'];
}
/**
* Detarmines whether the user ivnite is accept.
*/
get isInviteAccepted() {
return !!this.inviteAcceptedAt;
}
/**
* Full name attribute.
*/
get fullName() {
return `${this.firstName} ${this.lastName}`.trim();
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Role = require('models/Role');
return {
/**
* User belongs to user.
*/
role: {
relation: Model.BelongsToOneRelation,
modelClass: Role.default,
join: {
from: 'users.roleId',
to: 'roles.id',
},
},
};
}
}

View File

@@ -0,0 +1,92 @@
export default {
defaultFilterField: 'display_name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
first_name: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
},
last_name: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
},
display_name: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
},
work_phone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
},
personal_phone: {
name: 'vendor.field.personal_pone',
column: 'personal_phone',
fieldType: 'text',
},
company_name: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
},
created_at: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'vendor.field.balance',
column: 'balance',
fieldType: 'number',
},
opening_balance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
},
opening_balance_at: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
},
currency_code: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
},
status: {
name: 'vendor.field.status',
type: 'enumeration',
options: [
{ key: 'overdue', label: 'vendor.field.status.overdue' },
{ key: 'unpaid', label: 'vendor.field.status.unpaid' },
],
filterCustomQuery: (query, role) => {
switch (role.value) {
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
},
};

View File

@@ -0,0 +1,179 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination';
import ModelSetting from './ModelSetting';
import VendorSettings from './Vendor.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Contacts/Vendors/constants';
import ModelSearchable from './ModelSearchable';
class VendorQueryBuilder extends PaginationQueryBuilder {
constructor(...args) {
super(...args);
this.onBuild((builder) => {
if (builder.isFind() || builder.isDelete() || builder.isUpdate()) {
builder.where('contact_service', 'vendor');
}
});
}
}
export default class Vendor extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Query builder.
*/
static get QueryBuilder() {
return VendorQueryBuilder;
}
/**
* Table name
*/
static get tableName() {
return 'contacts';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Defined virtual attributes.
*/
static get virtualAttributes() {
return ['closingBalance', 'contactNormal', 'localOpeningBalance'];
}
/**
* Closing balance attribute.
*/
get closingBalance() {
return this.balance;
}
/**
* Retrieves the local opening balance.
* @returns {number}
*/
get localOpeningBalance() {
return this.openingBalance
? this.openingBalance * this.openingBalanceExchangeRate
: 0;
}
/**
* Retrieve the contact noraml;
*/
get contactNormal() {
return 'debit';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/**
* Filters the active customers.
*/
active(query) {
query.where('active', 1);
},
/**
* Filters the inactive customers.
*/
inactive(query) {
query.where('active', 0);
},
/**
* Filters the vendors that have overdue invoices.
*/
overdue(query) {
query.select(
'*',
Vendor.relatedQuery('overdueBills', query.knex())
.count()
.as('countOverdue')
);
query.having('countOverdue', '>', 0);
},
/**
* Filters the unpaid customers.
*/
unpaid(query) {
query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0');
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Bill = require('models/Bill');
return {
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'contacts.id',
to: 'bills.vendorId',
},
},
overdueBills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'contacts.id',
to: 'bills.vendorId',
},
filter: (query) => {
query.modify('overdue');
},
},
};
}
static get meta() {
return VendorSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'display_name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'first_name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'last_name', comparator: 'equals' },
{ condition: 'or', fieldKey: 'company_name', comparator: 'equals' },
{ condition: 'or', fieldKey: 'email', comparator: 'equals' },
{ condition: 'or', fieldKey: 'work_phone', comparator: 'equals' },
{ condition: 'or', fieldKey: 'personal_phone', comparator: 'equals' },
{ condition: 'or', fieldKey: 'website', comparator: 'equals' },
];
}
}

View File

@@ -0,0 +1,75 @@
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
vendor: {
name: 'vendor_credit.field.vendor',
column: 'vendor_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
amount: {
name: 'vendor_credit.field.amount',
column: 'amount',
fieldType: 'number',
},
currency_code: {
name: 'vendor_credit.field.currency_code',
column: 'currency_code',
fieldType: 'string',
},
credit_date: {
name: 'vendor_credit.field.credit_date',
column: 'vendor_credit_date',
fieldType: 'date',
},
reference_no: {
name: 'vendor_credit.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
credit_number: {
name: 'vendor_credit.field.credit_number',
column: 'vendor_credit_number',
fieldType: 'text',
},
note: {
name: 'vendor_credit.field.note',
column: 'note',
fieldType: 'text',
},
status: {
name: 'vendor_credit.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'vendor_credit.field.status.draft' },
{ key: 'published', label: 'vendor_credit.field.status.published' },
{ key: 'open', label: 'vendor_credit.field.status.open' },
{ key: 'closed', label: 'vendor_credit.field.status.closed' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
created_at: {
name: 'vendor_credit.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,253 @@
import { Model, raw, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/VendorCredits/constants';
import ModelSearchable from './ModelSearchable';
import VendorCreditMeta from './VendorCredit.Meta';
export default class VendorCredit extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'vendor_credits';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
}
/**
* Vendor credit amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the credit notes in draft status.
*/
draft(query) {
query.where('opened_at', null);
},
/**
* Filters the published vendor credits.
*/
published(query) {
query.whereNot('opened_at', null);
},
/**
* Filters the open vendor credits.
*/
open(query) {
query
.where(
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICED_AMOUNT) <
COALESCE(AMOUNT)`)
)
.modify('published');
},
/**
* Filters the closed vendor credits.
*/
closed(query) {
query
.where(
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICED_AMOUNT) =
COALESCE(AMOUNT)`)
)
.modify('published');
},
/**
* Status filter.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'published':
query.modify('published');
break;
case 'open':
default:
query.modify('open');
break;
case 'closed':
query.modify('closed');
break;
}
},
/**
*
*/
sortByStatus(query, order) {
query.orderByRaw(
`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICED_AMOUNT) = COALESCE(AMOUNT) ${order}`
);
},
};
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isDraft', 'isPublished', 'isOpen', 'isClosed', 'creditsRemaining'];
}
/**
* Detarmines whether the vendor credit is draft.
* @returns {boolean}
*/
get isDraft() {
return !this.openedAt;
}
/**
* Detarmines whether vendor credit is published.
* @returns {boolean}
*/
get isPublished() {
return !!this.openedAt;
}
/**
* Detarmines whether the credit note is open.
* @return {boolean}
*/
get isOpen() {
return !!this.openedAt && this.creditsRemaining > 0;
}
/**
* Detarmines whether the credit note is closed.
* @return {boolean}
*/
get isClosed() {
return this.openedAt && this.creditsRemaining === 0;
}
/**
* Retrieve the credits remaining.
* @returns {number}
*/
get creditsRemaining() {
return Math.max(this.amount - this.refundedAmount - this.invoicedAmount, 0);
}
/**
* Bill model settings.
*/
static get meta() {
return BillSettings;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Vendor = require('models/Vendor');
const ItemEntry = require('models/ItemEntry');
const Branch = require('models/Branch');
return {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor.default,
join: {
from: 'vendor_credits.vendorId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'vendor');
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
join: {
from: 'vendor_credits.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'VendorCredit');
builder.orderBy('index', 'ASC');
},
},
/**
* Vendor credit may belongs to branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch.default,
join: {
from: 'vendor_credits.branchId',
to: 'branches.id',
},
},
};
}
/**
*
*/
static get meta() {
return VendorCreditMeta;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'credit_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
* @returns {boolean}
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,53 @@
import { mixin, Model } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSearchable from './ModelSearchable';
export default class VendorCreditAppliedBill extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name
*/
static get tableName() {
return 'vendor_credit_applied_bill';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Bill = require('models/Bill');
const VendorCredit = require('models/VendorCredit');
return {
bill: {
relation: Model.BelongsToOneRelation,
modelClass: Bill.default,
join: {
from: 'vendor_credit_applied_bill.billId',
to: 'bills.id',
},
},
vendorCredit: {
relation: Model.BelongsToOneRelation,
modelClass: VendorCredit.default,
join: {
from: 'vendor_credit_applied_bill.vendorCreditId',
to: 'vendor_credits.id',
},
},
};
}
}

View File

@@ -0,0 +1,72 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
export default class View extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'views';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
static get modifiers() {
const TABLE_NAME = View.tableName;
return {
allMetadata(query) {
query.withGraphFetched('roles.field');
query.withGraphFetched('columns');
},
specificOrFavourite(query, viewId) {
if (viewId) {
query.where('id', viewId)
} else {
query.where('favourite', true);
}
return query;
}
}
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const ViewColumn = require('models/ViewColumn');
const ViewRole = require('models/ViewRole');
return {
/**
* View model may has many columns.
*/
columns: {
relation: Model.HasManyRelation,
modelClass: ViewColumn.default,
join: {
from: 'views.id',
to: 'view_has_columns.viewId',
},
},
/**
* View model may has many view roles.
*/
roles: {
relation: Model.HasManyRelation,
modelClass: ViewRole.default,
join: {
from: 'views.id',
to: 'view_roles.viewId',
},
},
};
}
}

View File

@@ -0,0 +1,21 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ViewColumn extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'view_has_columns';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {
};
}
}

View File

@@ -0,0 +1,46 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ViewRole extends TenantModel {
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['comparators'];
}
static get comparators() {
return [
'equals', 'not_equal', 'contains', 'not_contain',
];
}
/**
* Table name.
*/
static get tableName() {
return 'view_roles';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const View = require('models/View');
return {
/**
* View role model may belongs to view model.
*/
view: {
relation: Model.BelongsToOneRelation,
modelClass: View.default,
join: {
from: 'view_roles.viewId',
to: 'views.id',
},
},
};
}
}

View File

@@ -0,0 +1,162 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Warehouse extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'warehouses';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters accounts by the given ids.
* @param {Query} query
* @param {number[]} accountsIds
*/
isPrimary(query) {
query.where('primary', true);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const SaleInvoice = require('models/SaleInvoice');
const SaleEstimate = require('models/SaleEstimate');
const SaleReceipt = require('models/SaleReceipt');
const Bill = require('models/Bill');
const VendorCredit = require('models/VendorCredit');
const CreditNote = require('models/CreditNote');
const InventoryTransaction = require('models/InventoryTransaction');
const WarehouseTransfer = require('models/WarehouseTransfer');
const InventoryAdjustment = require('models/InventoryAdjustment');
return {
/**
* Warehouse may belongs to associated sale invoices.
*/
invoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
join: {
from: 'warehouses.id',
to: 'sales_invoices.warehouseId',
},
},
/**
* Warehouse may belongs to associated sale estimates.
*/
estimates: {
relation: Model.HasManyRelation,
modelClass: SaleEstimate.default,
join: {
from: 'warehouses.id',
to: 'sales_estimates.warehouseId',
},
},
/**
* Warehouse may belongs to associated sale receipts.
*/
receipts: {
relation: Model.HasManyRelation,
modelClass: SaleReceipt.default,
join: {
from: 'warehouses.id',
to: 'sales_receipts.warehouseId',
},
},
/**
* Warehouse may belongs to assocaited bills.
*/
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
join: {
from: 'warehouses.id',
to: 'bills.warehouseId',
},
},
/**
* Warehouse may belongs to associated credit notes.
*/
creditNotes: {
relation: Model.HasManyRelation,
modelClass: CreditNote.default,
join: {
from: 'warehouses.id',
to: 'credit_notes.warehouseId',
},
},
/**
* Warehouse may belongs to associated to vendor credits.
*/
vendorCredit: {
relation: Model.HasManyRelation,
modelClass: VendorCredit.default,
join: {
from: 'warehouses.id',
to: 'vendor_credits.warehouseId',
},
},
/**
* Warehouse may belongs to associated to inventory transactions.
*/
inventoryTransactions: {
relation: Model.HasManyRelation,
modelClass: InventoryTransaction.default,
join: {
from: 'warehouses.id',
to: 'inventory_transactions.warehouseId',
},
},
warehouseTransferTo: {
relation: Model.HasManyRelation,
modelClass: WarehouseTransfer.default,
join: {
from: 'warehouses.id',
to: 'warehouses_transfers.toWarehouseId',
},
},
warehouseTransferFrom: {
relation: Model.HasManyRelation,
modelClass: WarehouseTransfer.default,
join: {
from: 'warehouses.id',
to: 'warehouses_transfers.fromWarehouseId',
},
},
inventoryAdjustment: {
relation: Model.HasManyRelation,
modelClass: InventoryAdjustment.default,
join: {
from: 'warehouses.id',
to: 'inventory_adjustments.warehouseId',
},
},
};
}
}

View File

@@ -0,0 +1,41 @@
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
date: {
name: 'warehouse_transfer.field.date',
column: 'date',
columnType: 'date',
fieldType: 'date',
},
transaction_number: {
name: 'warehouse_transfer.field.transaction_number',
column: 'transaction_number',
fieldType: 'text',
},
status: {
name: 'warehouse_transfer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'in-transit', label: 'In Transit' },
{ key: 'transferred', label: 'Transferred' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortable: false,
},
created_at: {
name: 'warehouse_transfer.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
};

View File

@@ -0,0 +1,148 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import WarehouseTransferSettings from './WarehouseTransfer.Settings';
import ModelSearchable from './ModelSearchable';
import CustomViewBaseModel from './CustomViewBaseModel';
import ModelSetting from './ModelSetting';
import { DEFAULT_VIEWS } from '../services/Warehouses/WarehousesTransfers/constants';
export default class WarehouseTransfer extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
/**
* Table name.
*/
static get tableName() {
return 'warehouses_transfers';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isInitiated', 'isTransferred'];
}
/**
* Detarmines whether the warehouse transfer initiated.
* @retruns {boolean}
*/
get isInitiated() {
return !!this.transferInitiatedAt;
}
/**
* Detarmines whether the warehouse transfer transferred.
* @returns {boolean}
*/
get isTransferred() {
return !!this.transferDeliveredAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterByDraft(query) {
query.whereNull('transferInitiatedAt');
query.whereNull('transferDeliveredAt');
},
filterByInTransit(query) {
query.whereNotNull('transferInitiatedAt');
query.whereNull('transferDeliveredAt');
},
filterByTransferred(query) {
query.whereNotNull('transferInitiatedAt');
query.whereNotNull('transferDeliveredAt');
},
filterByStatus(query, status) {
switch (status) {
case 'draft':
default:
return query.modify('filterByDraft');
case 'in-transit':
return query.modify('filterByInTransit');
case 'transferred':
return query.modify('filterByTransferred');
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const WarehouseTransferEntry = require('models/WarehouseTransferEntry');
const Warehouse = require('models/Warehouse');
return {
/**
* View model may has many columns.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: WarehouseTransferEntry.default,
join: {
from: 'warehouses_transfers.id',
to: 'warehouses_transfers_entries.warehouseTransferId',
},
},
/**
*
*/
fromWarehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'warehouses_transfers.fromWarehouseId',
to: 'warehouses.id',
},
},
toWarehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'warehouses_transfers.toWarehouseId',
to: 'warehouses.id',
},
},
};
}
/**
* Model settings.
*/
static get meta() {
return WarehouseTransferSettings;
}
/**
* Retrieve the default custom views, roles and columns.
*/
static get defaultViews() {
return DEFAULT_VIEWS;
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
// { fieldKey: 'name', comparator: 'contains' },
// { condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
}

View File

@@ -0,0 +1,44 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Warehouse extends TenantModel {
/**
* Table name.
*/
static get tableName() {
return 'warehouses_transfers_entries';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['total'];
}
/**
* Invoice amount in local currency.
* @returns {number}
*/
get total() {
return this.cost * this.quantity;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Item = require('models/Item');
return {
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item.default,
join: {
from: 'warehouses_transfers_entries.itemId',
to: 'items.id',
},
},
};
}
}

View File

@@ -0,0 +1,55 @@
import Option from './Option';
import Setting from './Setting';
import SaleEstimate from './SaleEstimate';
import SaleEstimateEntry from './SaleEstimateEntry';
import SaleReceipt from './SaleReceipt';
import SaleReceiptEntry from './SaleReceiptEntry';
import Item from './Item';
import Account from './Account';
import AccountTransaction from './AccountTransaction';
import SaleInvoice from './SaleInvoice';
import SaleInvoiceEntry from './SaleInvoiceEntry';
import PaymentReceive from './PaymentReceive';
import PaymentReceiveEntry from './PaymentReceiveEntry';
import Bill from './Bill';
import BillPayment from './BillPayment';
import BillPaymentEntry from './BillPaymentEntry';
import View from './View';
import ItemEntry from './ItemEntry';
import InventoryTransaction from './InventoryTransaction';
import InventoryLotCostTracker from './InventoryCostLotTracker';
import Customer from './Customer';
import Contact from './Contact';
import Vendor from './Vendor';
import ExpenseCategory from './ExpenseCategory';
import Expense from './Expense';
import ManualJournal from './ManualJournal';
export {
SaleEstimate,
SaleEstimateEntry,
SaleReceipt,
SaleReceiptEntry,
SaleInvoice,
SaleInvoiceEntry,
Item,
Account,
AccountTransaction,
PaymentReceive,
PaymentReceiveEntry,
Bill,
BillPayment,
BillPaymentEntry,
View,
ItemEntry,
InventoryTransaction,
InventoryLotCostTracker,
Option,
Contact,
ExpenseCategory,
Expense,
ManualJournal,
Customer,
Vendor,
Setting
};