mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
Merge branch 'master' of https://github.com/abouolia/Bigcapital
This commit is contained in:
@@ -40,6 +40,24 @@ export default class SalesEstimatesController extends BaseController {
|
||||
asyncMiddleware(this.deliverSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
);
|
||||
router.post(
|
||||
'/:id/approve',
|
||||
[
|
||||
this.validateSpecificEstimateSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.approveSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/reject',
|
||||
[
|
||||
this.validateSpecificEstimateSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.rejectSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
)
|
||||
router.post(
|
||||
'/:id', [
|
||||
...this.validateSpecificEstimateSchema,
|
||||
@@ -202,8 +220,55 @@ export default class SalesEstimatesController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the sale estimate as approved.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async approveSaleEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.saleEstimateService.approveSaleEstimate(tenantId, estimateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been approved successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the sale estimate as rejected.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.saleEstimateService.rejectSaleEstimate(tenantId, estimateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: estimateId,
|
||||
message: 'The sale estimate has been rejected successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given estimate with associated entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getEstimate(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: estimateId } = req.params;
|
||||
@@ -302,11 +367,31 @@ export default class SalesEstimatesController extends BaseController {
|
||||
errors: [{ type: 'NOT_SELL_ABLE_ITEMS', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
if (error.errorType === 'SALE_ESTIMATE_ALREADY_APPROVED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_ALREADY_APPROVED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_NOT_DELIVERED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_NOT_DELIVERED', code: 1100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_ESTIMATE_ALREADY_REJECTED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_ESTIMATE_ALREADY_REJECTED', code: 1200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_NOT_FOUND', code: 1300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -264,7 +264,11 @@ export default class SaleInvoicesController extends BaseController{
|
||||
}
|
||||
|
||||
try {
|
||||
const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList(
|
||||
const {
|
||||
salesInvoices,
|
||||
filterMeta,
|
||||
pagination,
|
||||
} = await this.saleInvoiceService.salesInvoicesList(
|
||||
tenantId, filter,
|
||||
);
|
||||
return res.status(200).send({
|
||||
|
||||
@@ -22,6 +22,16 @@ export default class SalesReceiptsController extends BaseController{
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id/close',
|
||||
[
|
||||
...this.specificReceiptValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.closeSaleReceipt.bind(this)),
|
||||
this.handleServiceErrors,
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:id', [
|
||||
...this.specificReceiptValidationSchema,
|
||||
@@ -75,6 +85,7 @@ export default class SalesReceiptsController extends BaseController{
|
||||
check('receipt_date').exists().isISO8601(),
|
||||
check('receipt_number').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('closed').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
|
||||
@@ -188,6 +199,31 @@ export default class SalesReceiptsController extends BaseController{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given the sale receipt as closed.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async closeSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
try {
|
||||
// Update the given sale receipt details.
|
||||
await this.saleReceiptService.closeSaleReceipt(
|
||||
tenantId,
|
||||
saleReceiptId,
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: saleReceiptId,
|
||||
message: 'Sale receipt has been closed successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing sales receipts.
|
||||
* @param {Request} req
|
||||
@@ -296,6 +332,11 @@ export default class SalesReceiptsController extends BaseController{
|
||||
errors: [{ type: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SALE_RECEIPT_IS_ALREADY_CLOSED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE_RECEIPT_IS_ALREADY_CLOSED', code: 1000 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ exports.up = function (knex) {
|
||||
return knex.schema.createTable('views', (table) => {
|
||||
table.increments();
|
||||
table.string('name').index();
|
||||
table.string('slug').index();
|
||||
table.boolean('predefined');
|
||||
table.string('resource_model').index();
|
||||
table.boolean('favourite');
|
||||
|
||||
@@ -14,6 +14,9 @@ exports.up = function(knex) {
|
||||
table.text('send_to_email');
|
||||
|
||||
table.date('delivered_at').index();
|
||||
table.date('approved_at').index();
|
||||
table.date('rejected_at').index();
|
||||
|
||||
table.integer('user_id').unsigned().index();
|
||||
|
||||
table.integer('converted_to_invoice_id').unsigned();
|
||||
|
||||
@@ -6,11 +6,12 @@ exports.up = function(knex) {
|
||||
table.integer('deposit_account_id').unsigned().index().references('id').inTable('accounts');
|
||||
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
|
||||
table.date('receipt_date').index();
|
||||
table.string('receipt_number');
|
||||
table.string('reference_no');
|
||||
table.string('receipt_number').index();
|
||||
table.string('reference_no').index();
|
||||
table.string('send_to_email');
|
||||
table.text('receipt_message');
|
||||
table.text('statement');
|
||||
table.date('closed_at').index();
|
||||
table.timestamps();
|
||||
})
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ exports.up = (knex) => {
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('views').insert([
|
||||
// Accounts
|
||||
// Accounts.
|
||||
{ id: 15, name: i18n.__('Inactive'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
|
||||
{ id: 1, name: i18n.__('Assets'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
|
||||
{ id: 2, name: i18n.__('Liabilities'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
|
||||
@@ -18,7 +18,7 @@ exports.up = (knex) => {
|
||||
{ id: 4, name: i18n.__('Income'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
|
||||
{ id: 5, name: i18n.__('Expenses'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
|
||||
|
||||
// Items
|
||||
// Items.
|
||||
{ id: 6, name: i18n.__('Services'), roles_logic_expression: '1', resource_model: 'Item', predefined: true },
|
||||
{ id: 7, name: i18n.__('Inventory'), roles_logic_expression: '1', resource_model: 'Item', predefined: true },
|
||||
{ id: 8, name: i18n.__('Non-Inventory'), roles_logic_expression: '1', resource_model: 'Item', predefined: true },
|
||||
@@ -28,10 +28,34 @@ exports.up = (knex) => {
|
||||
{ id: 10, name: i18n.__('Credit'), roles_logic_expression: '1', resource_model: 'ManualJournal', predefined: true },
|
||||
{ id: 11, name: i18n.__('Reconciliation'), roles_logic_expression: '1', resource_model: 'ManualJournal', predefined: true },
|
||||
|
||||
// Expenses
|
||||
// Expenses.
|
||||
{ id: 12, name: i18n.__('Interest'), roles_logic_expression: '1', resource_model: 'Expense', predefined: false, },
|
||||
{ id: 13, name: i18n.__('Depreciation'), roles_logic_expression: '1', resource_model: 'Expense', predefined: false, },
|
||||
{ id: 14, name: i18n.__('Payroll'), roles_logic_expression: '1', resource_model: 'Expense', predefined: false },
|
||||
|
||||
// Sales invoices.
|
||||
{ id: 16, name: 'Draft', slug: 'draft', roles_logic_expression: '1', resource_model: 'SaleInvoice', predefined: true, },
|
||||
{ id: 17, name: 'Delivered', slug: 'delivered', roles_logic_expression: '1', resource_model: 'SaleInvoice', predefined: true },
|
||||
{ id: 18, name: 'Unpaid', slug: 'unpaid', roles_logic_expression: '1', resource_model: 'SaleInvoice', predefined: true },
|
||||
{ id: 19, name: 'Overdue', slug: 'overdue', roles_logic_expression: '1', resource_model: 'SaleInvoice', predefined: true },
|
||||
{ id: 20, name: 'Partially paid', slug: 'partially-paid', roles_logic_expression: '1', resource_model: 'SaleInvoice', predefined: true },
|
||||
{ id: 21, name: 'Paid', slug: 'paid', roles_logic_expression: '1', resource_model: 'SaleInvoice', predefined: true },
|
||||
|
||||
// Bills.
|
||||
{ id: 22, name: 'Draft', slug: 'draft', roles_logic_expression: '1', resource_model: 'Bill', predefined: true, },
|
||||
{ id: 23, name: 'Opened', slug: 'opened', roles_logic_expression: '1', resource_model: 'Bill', predefined: true },
|
||||
{ id: 24, name: 'Unpaid', slug: 'unpaid', roles_logic_expression: '1', resource_model: 'Bill', predefined: true },
|
||||
{ id: 25, name: 'Overdue', slug: 'overdue', roles_logic_expression: '1', resource_model: 'Bill', predefined: true },
|
||||
{ id: 26, name: 'Partially paid', slug: 'partially-paid', roles_logic_expression: '1', resource_model: 'Bill', predefined: true },
|
||||
{ id: 27, name: 'Paid', slug: 'paid', roles_logic_expression: '1', resource_model: 'Bill', predefined: true },
|
||||
|
||||
// Sale estimate.
|
||||
{ id: 28, name: 'Draft', slug: 'draft', roles_logic_expression: '1', resource_model: 'SaleEstimate', predefined: true },
|
||||
{ id: 29, name: 'Delivered', slug: 'delivered', roles_logic_expression: '1', resource_model: 'SaleEstimate', predefined: true },
|
||||
{ id: 30, name: 'Approved', slug: 'approved', roles_logic_expression: '1', resource_model: 'SaleEstimate', predefined: true },
|
||||
{ id: 31, name: 'Rejected', slug: 'rejected', roles_logic_expression: '1', resource_model: 'SaleEstimate', predefined: true },
|
||||
{ id: 32, name: 'Invoiced', slug: 'invoiced', roles_logic_expression: '1', resource_model: 'SaleEstimate', predefined: true },
|
||||
{ id: 33, name: 'Expired', slug: 'expired', roles_logic_expression: '1', resource_model: 'SaleEstimate', predefined: true },
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,22 +6,46 @@ exports.up = (knex) => {
|
||||
// Inserts seed entries
|
||||
return knex('view_roles').insert([
|
||||
// Accounts
|
||||
{ id: 1, field_key: 'type', index: 1, comparator: 'equals', value: 'asset', view_id: 1 },
|
||||
{ id: 2, field_key: 'type', index: 1, comparator: 'equals', value: 'liability', view_id: 2 },
|
||||
{ id: 3, field_key: 'type', index: 1, comparator: 'equals', value: 'equity', view_id: 3 },
|
||||
{ id: 4, field_key: 'type', index: 1, comparator: 'equals', value: 'income', view_id: 4 },
|
||||
{ id: 5, field_key: 'type', index: 1, comparator: 'equals', value: 'expense', view_id: 5 },
|
||||
{ id: 12, field_key: 'active', index: 1, comparator: 'is', value: 1, view_id: 15 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'asset', view_id: 1 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'liability', view_id: 2 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'equity', view_id: 3 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'income', view_id: 4 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'expense', view_id: 5 },
|
||||
{ field_key: 'active', index: 1, comparator: 'is', value: 1, view_id: 15 },
|
||||
|
||||
// Items.
|
||||
{ id: 6, field_key: 'type', index: 1, comparator: 'equals', value: 'service', view_id: 6 },
|
||||
{ id: 7, field_key: 'type', index: 1, comparator: 'equals', value: 'inventory', view_id: 7 },
|
||||
{ id: 8, field_key: 'type', index: 1, comparator: 'equals', value: 'non-inventory', view_id: 8 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'service', view_id: 6 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'inventory', view_id: 7 },
|
||||
{ field_key: 'type', index: 1, comparator: 'equals', value: 'non-inventory', view_id: 8 },
|
||||
|
||||
// Manual Journals.
|
||||
{ id: 9, field_key: 'journal_type', index: 1, comparator: 'equals', value: 'Journal', view_id: 9 },
|
||||
{ id: 10, field_key: 'journal_type', index: 1, comparator: 'equals', value: 'CreditNote', view_id: 10 },
|
||||
{ id: 11, field_key: 'journal_type', index: 1, comparator: 'equals', value: 'Reconciliation', view_id: 11 },
|
||||
{ field_key: 'journal_type', index: 1, comparator: 'equals', value: 'Journal', view_id: 9 },
|
||||
{ field_key: 'journal_type', index: 1, comparator: 'equals', value: 'CreditNote', view_id: 10 },
|
||||
{ field_key: 'journal_type', index: 1, comparator: 'equals', value: 'Reconciliation', view_id: 11 },
|
||||
|
||||
// Sale invoice.
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'draft', view_id: 16 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'delivered', view_id: 17 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'unpaid', view_id: 18 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'overdue', view_id: 19 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'partially-paid', view_id: 20 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'paid', view_id: 21 },
|
||||
|
||||
// Bills
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'draft', view_id: 22 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'opened', view_id: 23 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'unpaid', view_id: 24 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'overdue', view_id: 25 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'partially-paid', view_id: 26 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'paid', view_id: 27 },
|
||||
|
||||
// Sale estimates
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'draft', view_id: 28 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'delivered', view_id: 29 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'approved', view_id: 30 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'rejected', view_id: 31 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'invoiced', view_id: 32 },
|
||||
{ field_key: 'status', index: 1, comparator: 'is', value: 'expired', view_id: 33 },
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ISaleReceipt {
|
||||
receiptMessage: string,
|
||||
receiptNumber: string,
|
||||
statement: string,
|
||||
closedAt: Date|string,
|
||||
entries: any[],
|
||||
};
|
||||
|
||||
@@ -26,6 +27,7 @@ export interface ISaleReceiptDTO {
|
||||
referenceNo: string,
|
||||
receiptMessage: string,
|
||||
statement: string,
|
||||
closed: boolean,
|
||||
entries: any[],
|
||||
};
|
||||
|
||||
|
||||
@@ -49,8 +49,9 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
|
||||
this.responseMeta = {
|
||||
view: {
|
||||
logicExpression: this.logicExpression,
|
||||
filterRoles: this.filterRoles
|
||||
.map((filterRole) => ({ ...omit(filterRole, ['id', 'viewId']) })),
|
||||
filterRoles: this.filterRoles.map((filterRole) =>
|
||||
({ ...omit(filterRole, ['id', 'viewId']) })
|
||||
),
|
||||
customViewId: this.viewId,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,6 +124,12 @@ export function buildRoleQuery(model: IModel, role: IFilterRole) {
|
||||
const fieldRelation = getRoleFieldColumn(model, role.fieldKey);
|
||||
const comparatorColumn = fieldRelation.relationColumn || `${model.tableName}.${fieldRelation.column}`;
|
||||
|
||||
//
|
||||
if (typeof fieldRelation.query !== 'undefined') {
|
||||
return (builder) => {
|
||||
fieldRelation.query(builder, role);
|
||||
};
|
||||
}
|
||||
switch (fieldRelation.columnType) {
|
||||
case 'number':
|
||||
return numberRoleQueryBuilder(role, comparatorColumn);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Model, raw } from 'objection';
|
||||
import moment from 'moment';
|
||||
import { difference } from 'lodash';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { query } from 'winston';
|
||||
|
||||
export default class Bill extends TenantModel {
|
||||
/**
|
||||
@@ -17,9 +18,49 @@ export default class Bill extends TenantModel {
|
||||
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filters the bills in draft status.
|
||||
*/
|
||||
draft(query) {
|
||||
query.where('opened_at', 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('AMOUNT - PAYMENT_AMOUNT > 0'));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Filters the overdue bills.
|
||||
*/
|
||||
overdue(query) {
|
||||
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
|
||||
},
|
||||
/**
|
||||
* 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`'));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +75,16 @@ export default class Bill extends TenantModel {
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['dueAmount', 'isOpen', 'isPartiallyPaid', 'isFullyPaid', 'isPaid', 'remainingDays', 'overdueDays', 'isOverdue'];
|
||||
return [
|
||||
'dueAmount',
|
||||
'isOpen',
|
||||
'isPartiallyPaid',
|
||||
'isFullyPaid',
|
||||
'isPaid',
|
||||
'remainingDays',
|
||||
'overdueDays',
|
||||
'isOverdue',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,11 +231,6 @@ export default class Bill extends TenantModel {
|
||||
|
||||
static get fields() {
|
||||
return {
|
||||
created_at: {
|
||||
label: 'Created at',
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
},
|
||||
vendor: {
|
||||
label: 'Vendor',
|
||||
column: 'vendor_id',
|
||||
@@ -216,7 +261,23 @@ export default class Bill extends TenantModel {
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
column: 'status',
|
||||
options: [],
|
||||
query: (query, role) => {
|
||||
switch(role.value) {
|
||||
case 'draft':
|
||||
query.modify('draft'); break;
|
||||
case 'opened':
|
||||
query.modify('opened'); break;
|
||||
case 'unpaid':
|
||||
query.modify('unpaid'); break;
|
||||
case 'overdue':
|
||||
query.modify('overdue'); break;
|
||||
case 'partially-paid':
|
||||
query.modify('partiallyPaid'); break;
|
||||
case 'paid':
|
||||
query.modify('paid'); break;
|
||||
}
|
||||
},
|
||||
},
|
||||
amount: {
|
||||
label: 'Amount',
|
||||
@@ -234,6 +295,14 @@ export default class Bill extends TenantModel {
|
||||
label: 'Note',
|
||||
column: 'note',
|
||||
},
|
||||
user: {
|
||||
|
||||
},
|
||||
created_at: {
|
||||
label: 'Created at',
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export default class PaymentReceive extends TenantModel {
|
||||
query.where('contact_service', 'customer');
|
||||
}
|
||||
},
|
||||
|
||||
depositAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account.default,
|
||||
@@ -50,7 +49,6 @@ export default class PaymentReceive extends TenantModel {
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PaymentReceiveEntry.default,
|
||||
@@ -59,7 +57,6 @@ export default class PaymentReceive extends TenantModel {
|
||||
to: 'payment_receives_entries.paymentReceiveId',
|
||||
},
|
||||
},
|
||||
|
||||
transactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: AccountTransaction.default,
|
||||
@@ -79,11 +76,58 @@ export default class PaymentReceive extends TenantModel {
|
||||
*/
|
||||
static get fields() {
|
||||
return {
|
||||
customer: {
|
||||
label: 'Customer',
|
||||
column: 'customer_id',
|
||||
fieldType: 'options',
|
||||
optionsResource: 'customers',
|
||||
optionsKey: 'id',
|
||||
optionsLable: 'displayName',
|
||||
},
|
||||
payment_date: {
|
||||
label: 'Payment date',
|
||||
column: 'payment_date',
|
||||
columnType: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
amount: {
|
||||
label: 'Amount',
|
||||
column: 'amount',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
reference_no: {
|
||||
label: 'Reference No.',
|
||||
column: 'reference_no',
|
||||
columnType: 'string',
|
||||
fieldType: 'text',
|
||||
},
|
||||
deposit_acount: {
|
||||
column: 'deposit_account_id',
|
||||
lable: 'Deposit account',
|
||||
relation: "accounts.id",
|
||||
optionsResource: "account",
|
||||
},
|
||||
payment_receive_no: {
|
||||
label: 'Payment receive No.',
|
||||
column: 'payment_receive_no',
|
||||
columnType: 'string',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
label: 'description',
|
||||
column: 'description',
|
||||
columnType: 'string',
|
||||
fieldType: 'text',
|
||||
},
|
||||
created_at: {
|
||||
label: 'Created at',
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
},
|
||||
user: {
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { defaultToTransform } from 'utils';
|
||||
import HasItemEntries from 'services/Sales/HasItemsEntries';
|
||||
import { query } from 'winston';
|
||||
|
||||
export default class SaleEstimate extends TenantModel {
|
||||
/**
|
||||
@@ -22,7 +24,13 @@ export default class SaleEstimate extends TenantModel {
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isDelivered', 'isExpired', 'isConvertedToInvoice'];
|
||||
return [
|
||||
'isDelivered',
|
||||
'isExpired',
|
||||
'isConvertedToInvoice',
|
||||
'isApproved',
|
||||
'isRejected'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +61,22 @@ export default class SaleEstimate extends TenantModel {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -60,6 +84,50 @@ export default class SaleEstimate extends TenantModel {
|
||||
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)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
@@ -79,7 +147,6 @@ export default class SaleEstimate extends TenantModel {
|
||||
query.where('contact_service', 'customer');
|
||||
}
|
||||
},
|
||||
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ItemEntry.default,
|
||||
@@ -99,6 +166,64 @@ export default class SaleEstimate extends TenantModel {
|
||||
*/
|
||||
static get fields() {
|
||||
return {
|
||||
amount: {
|
||||
label: 'Amount',
|
||||
column: 'amount',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
customer: {
|
||||
label: 'Customer',
|
||||
column: 'customer_id',
|
||||
fieldType: 'options',
|
||||
optionsResource: 'customers',
|
||||
optionsKey: 'id',
|
||||
optionsLable: 'displayName',
|
||||
},
|
||||
estimate_date: {
|
||||
label: 'Estimate date',
|
||||
column: 'estimate_date',
|
||||
columnType: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
expiration_date: {
|
||||
label: 'Expiration date',
|
||||
column: 'expiration_date',
|
||||
columnType: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
note: {
|
||||
label: 'Note',
|
||||
column: 'note',
|
||||
columnType: 'text',
|
||||
fieldType: 'text',
|
||||
},
|
||||
terms_conditions: {
|
||||
label: 'Terms & conditions',
|
||||
column: 'terms_conditions',
|
||||
columnType: 'text',
|
||||
fieldType: 'text',
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
query: (query, role) => {
|
||||
switch(role.value) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
label: 'Created at',
|
||||
column: 'created_at',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Model, raw } from 'objection';
|
||||
import moment from 'moment';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { defaultToTransform } from 'utils';
|
||||
import { QueryBuilder } from 'knex';
|
||||
|
||||
export default class SaleInvoice extends TenantModel {
|
||||
/**
|
||||
@@ -29,7 +30,16 @@ export default class SaleInvoice extends TenantModel {
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['dueAmount', 'isDelivered', 'isOverdue', 'isPartiallyPaid', 'isFullyPaid', 'isPaid', 'remainingDays', 'overdueDays'];
|
||||
return [
|
||||
'dueAmount',
|
||||
'isDelivered',
|
||||
'isOverdue',
|
||||
'isPartiallyPaid',
|
||||
'isFullyPaid',
|
||||
'isPaid',
|
||||
'remainingDays',
|
||||
'overdueDays',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,10 +128,15 @@ export default class SaleInvoice extends TenantModel {
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filters the due invoices.
|
||||
*/
|
||||
dueInvoices(query) {
|
||||
query.where(raw('BALANCE - PAYMENT_AMOUNT > 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);
|
||||
@@ -134,16 +149,46 @@ export default class SaleInvoice extends TenantModel {
|
||||
query.where('invoice_date', '<=', toDate);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Filters the invoices in draft status.
|
||||
*/
|
||||
draft(query) {
|
||||
query.where('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) {
|
||||
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
|
||||
},
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Due amount of the given.
|
||||
*/
|
||||
get dueAmount() {
|
||||
return Math.max(this.balance - this.paymentAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
@@ -232,11 +277,106 @@ export default class SaleInvoice extends TenantModel {
|
||||
*/
|
||||
static get fields() {
|
||||
return {
|
||||
customer: {
|
||||
label: 'Customer',
|
||||
column: 'customer_id',
|
||||
fieldType: 'options',
|
||||
optionsResource: 'customers',
|
||||
optionsKey: 'id',
|
||||
optionsLable: 'displayName',
|
||||
},
|
||||
invoice_date: {
|
||||
label: 'Invoice date',
|
||||
column: 'invoice_date',
|
||||
columnType: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
due_date: {
|
||||
label: 'Due date',
|
||||
column: 'due_date',
|
||||
columnType: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
invoice_no: {
|
||||
label: 'Invoice No.',
|
||||
column: 'invoice_no',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
refernece_no: {
|
||||
label: 'Reference No.',
|
||||
column: 'reference_no',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
invoice_message: {
|
||||
label: 'Invoice message',
|
||||
column: 'invoice_message',
|
||||
columnType: 'text',
|
||||
fieldType: 'text',
|
||||
},
|
||||
terms_conditions: {
|
||||
label: 'Terms & conditions',
|
||||
column: 'terms_conditions',
|
||||
columnType: 'text',
|
||||
fieldType: 'text',
|
||||
},
|
||||
invoice_amount: {
|
||||
label: 'Invoice amount',
|
||||
column: 'invoice_amount',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
payment_amount: {
|
||||
label: 'Payment amount',
|
||||
column: 'payment_amount',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
due_amount: {
|
||||
label: 'Due amount',
|
||||
column: 'due_amount',
|
||||
columnType: 'number',
|
||||
fieldType: 'number',
|
||||
},
|
||||
created_at: {
|
||||
label: 'Created at',
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
options: [
|
||||
{ key: 'draft', label: 'Draft', },
|
||||
{ key: 'delivered', label: 'Delivered' },
|
||||
{ key: 'unpaid', label: 'Unpaid' },
|
||||
{ key: 'overdue', label: 'Overdue' },
|
||||
{ key: 'partially-paid', label: 'Partially paid' },
|
||||
{ key: 'paid', label: 'Paid' },
|
||||
],
|
||||
query: (query, role) => {
|
||||
switch(role.value) {
|
||||
case 'draft':
|
||||
query.modify('draft');
|
||||
break;
|
||||
case 'delivered':
|
||||
query.modify('delivered');
|
||||
break;
|
||||
case 'unpaid':
|
||||
query.modify('unpaid');
|
||||
break;
|
||||
case 'overdue':
|
||||
query.modify('overdue');
|
||||
break;
|
||||
case 'partially-paid':
|
||||
query.modify('partiallyPaid');
|
||||
break;
|
||||
case 'paid':
|
||||
query.modify('paid');
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,32 @@ export default class SaleReceipt extends TenantModel {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'isClosed',
|
||||
'isDraft',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,6 @@ export default class ViewRepository extends TenantRepository {
|
||||
* Retrieve all views of the given resource id.
|
||||
*/
|
||||
allByResource(resourceModel: string, withRelations?) {
|
||||
return super.find({ resource_mode: resourceModel }, withRelations);
|
||||
return super.find({ resource_model: resourceModel }, withRelations);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export default class DynamicListService implements IDynamicListService {
|
||||
*/
|
||||
private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
|
||||
const { viewRepository } = this.tenancy.repositories(tenantId);
|
||||
const view = await viewRepository.findOneById(viewId);
|
||||
const view = await viewRepository.findOneById(viewId, 'roles');
|
||||
|
||||
if (!view || view.resourceModel !== model.name) {
|
||||
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
|
||||
|
||||
@@ -25,7 +25,6 @@ import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { entriesAmountDiff, formatDateFields } from 'utils';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { Bill } from 'models';
|
||||
|
||||
const ERRORS = {
|
||||
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
|
||||
|
||||
@@ -21,7 +21,10 @@ const ERRORS = {
|
||||
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
|
||||
ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS',
|
||||
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
|
||||
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE'
|
||||
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE',
|
||||
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
|
||||
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
|
||||
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED'
|
||||
};
|
||||
/**
|
||||
* Sale estimate service.
|
||||
@@ -352,4 +355,61 @@ export default class SaleEstimateService {
|
||||
deliveredAt: moment().toMySqlDateTime()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the sale estimate as approved from the customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
*/
|
||||
public async approveSaleEstimate(
|
||||
tenantId: number,
|
||||
saleEstimateId: number,
|
||||
): Promise<void> {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve details of the given sale estimate id.
|
||||
const saleEstimate = await this.getSaleEstimateOrThrowError(tenantId, saleEstimateId);
|
||||
|
||||
// Throws error in case the sale estimate still not delivered to customer.
|
||||
if (!saleEstimate.isDelivered) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
|
||||
}
|
||||
// Throws error in case the sale estimate already approved.
|
||||
if (saleEstimate.isApproved) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED);
|
||||
}
|
||||
await SaleEstimate.query().where('id', saleEstimateId).patch({
|
||||
approvedAt: moment().toMySqlDateTime(),
|
||||
rejectedAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the sale estimate as rejected from the customer.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleEstimateId
|
||||
*/
|
||||
public async rejectSaleEstimate(
|
||||
tenantId: number,
|
||||
saleEstimateId: number,
|
||||
): Promise<void> {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve details of the given sale estimate id.
|
||||
const saleEstimate = await this.getSaleEstimateOrThrowError(tenantId, saleEstimateId);
|
||||
|
||||
// Throws error in case the sale estimate still not delivered to customer.
|
||||
if (!saleEstimate.isDelivered) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
|
||||
}
|
||||
// Throws error in case the sale estimate already rejected.
|
||||
if (saleEstimate.isRejected) {
|
||||
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED);
|
||||
}
|
||||
// Mark the sale estimate as reject on the storage.
|
||||
await SaleEstimate.query().where('id', saleEstimateId).patch({
|
||||
rejectedAt: moment().toMySqlDateTime(),
|
||||
approvedAt: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -449,12 +449,19 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
|
||||
public async salesInvoicesList(
|
||||
tenantId: number,
|
||||
salesInvoicesFilter: ISalesInvoicesFilter
|
||||
): Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
|
||||
): Promise<{
|
||||
salesInvoices: ISaleInvoice[],
|
||||
pagination: IPaginationMeta,
|
||||
filterMeta: IFilterMeta
|
||||
}> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter);
|
||||
|
||||
this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter });
|
||||
const { results, pagination } = await SaleInvoice.query().onBuild((builder) => {
|
||||
const {
|
||||
results,
|
||||
pagination,
|
||||
} = await SaleInvoice.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
builder.withGraphFetched('customer');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import events from 'subscribers/events';
|
||||
import { ISaleReceipt } from 'interfaces';
|
||||
import { ISaleReceipt, ISaleReceiptDTO } from 'interfaces';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatDateFields } from 'utils';
|
||||
@@ -13,13 +14,15 @@ import { IFilterMeta, IPaginationMeta } from 'interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
|
||||
import { ItemEntry } from 'models';
|
||||
|
||||
|
||||
const ERRORS = {
|
||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
|
||||
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
|
||||
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE'
|
||||
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
|
||||
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED'
|
||||
};
|
||||
@Service()
|
||||
export default class SalesReceiptService {
|
||||
@@ -102,6 +105,35 @@ export default class SalesReceiptService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform DTO object to model object.
|
||||
* @param {ISaleReceiptDTO} saleReceiptDTO -
|
||||
* @param {ISaleReceipt} oldSaleReceipt -
|
||||
* @returns {ISaleReceipt}
|
||||
*/
|
||||
transformObjectDTOToModel(
|
||||
saleReceiptDTO: ISaleReceiptDTO,
|
||||
oldSaleReceipt?: ISaleReceipt
|
||||
): ISaleReceipt {
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
|
||||
return {
|
||||
amount,
|
||||
...formatDateFields(
|
||||
omit(saleReceiptDTO, ['closed', 'entries']),
|
||||
['receiptDate']
|
||||
),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
...(saleReceiptDTO.closed && (!oldSaleReceipt?.closedAt)) && ({
|
||||
closedAt: moment().toMySqlDateTime(),
|
||||
}),
|
||||
entries: saleReceiptDTO.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
...omit(entry, ['id', 'amount']),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @async
|
||||
@@ -109,13 +141,10 @@ export default class SalesReceiptService {
|
||||
* @return {Object}
|
||||
*/
|
||||
public async createSaleReceipt(tenantId: number, saleReceiptDTO: any): Promise<ISaleReceipt> {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
const saleReceiptObj = {
|
||||
amount,
|
||||
...formatDateFields(saleReceiptDTO, ['receiptDate'])
|
||||
};
|
||||
// Transform sale receipt DTO to model.
|
||||
const saleReceiptObj = this.transformObjectDTOToModel(saleReceiptDTO);
|
||||
|
||||
// Validate receipt deposit account existance and type.
|
||||
await this.validateReceiptDepositAccountExistance(tenantId, saleReceiptDTO.depositAccountId);
|
||||
@@ -131,15 +160,8 @@ export default class SalesReceiptService {
|
||||
await this.validateReceiptNumberUnique(tenantId, saleReceiptDTO.receiptNumber);
|
||||
}
|
||||
this.logger.info('[sale_receipt] trying to insert sale receipt graph.', { tenantId, saleReceiptDTO });
|
||||
const saleReceipt = await SaleReceipt.query()
|
||||
.insertGraphAndFetch({
|
||||
...omit(saleReceiptObj, ['entries']),
|
||||
const saleReceipt = await SaleReceipt.query().insertGraphAndFetch({ ...saleReceiptObj });
|
||||
|
||||
entries: saleReceiptObj.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
...omit(entry, ['id', 'amount']),
|
||||
}))
|
||||
});
|
||||
await this.eventDispatcher.dispatch(events.saleReceipt.onCreated, { tenantId, saleReceipt });
|
||||
|
||||
this.logger.info('[sale_receipt] sale receipt inserted successfully.', { tenantId });
|
||||
@@ -156,13 +178,11 @@ export default class SalesReceiptService {
|
||||
public async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) {
|
||||
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
|
||||
const saleReceiptObj = {
|
||||
amount,
|
||||
...formatDateFields(saleReceiptDTO, ['receiptDate'])
|
||||
};
|
||||
// Retrieve sale receipt or throw not found service error.
|
||||
const oldSaleReceipt = await this.getSaleReceiptOrThrowError(tenantId, saleReceiptId);
|
||||
|
||||
// Transform sale receipt DTO to model.
|
||||
const saleReceiptObj = this.transformObjectDTOToModel(saleReceiptDTO, oldSaleReceipt);
|
||||
|
||||
// Validate receipt deposit account existance and type.
|
||||
await this.validateReceiptDepositAccountExistance(tenantId, saleReceiptDTO.depositAccountId);
|
||||
@@ -178,16 +198,10 @@ export default class SalesReceiptService {
|
||||
await this.validateReceiptNumberUnique(tenantId, saleReceiptDTO.receiptNumber, saleReceiptId);
|
||||
}
|
||||
|
||||
const saleReceipt = await SaleReceipt.query()
|
||||
.upsertGraphAndFetch({
|
||||
id: saleReceiptId,
|
||||
...omit(saleReceiptObj, ['entries']),
|
||||
|
||||
entries: saleReceiptObj.entries.map((entry) => ({
|
||||
reference_type: 'SaleReceipt',
|
||||
...omit(entry, ['amount']),
|
||||
}))
|
||||
});
|
||||
const saleReceipt = await SaleReceipt.query().upsertGraphAndFetch({
|
||||
id: saleReceiptId,
|
||||
...saleReceiptObj
|
||||
});
|
||||
|
||||
this.logger.info('[sale_receipt] edited successfully.', { tenantId, saleReceiptId });
|
||||
await this.eventDispatcher.dispatch(events.saleReceipt.onEdited, {
|
||||
@@ -265,4 +279,29 @@ export default class SalesReceiptService {
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given sale receipt as closed.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleReceiptId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async closeSaleReceipt(
|
||||
tenantId: number,
|
||||
saleReceiptId: number
|
||||
): Promise<void> {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve sale receipt or throw not found service error.
|
||||
const oldSaleReceipt = await this.getSaleReceiptOrThrowError(tenantId, saleReceiptId);
|
||||
|
||||
// Throw service error if the sale receipt already closed.
|
||||
if (oldSaleReceipt.isClosed) {
|
||||
throw new ServiceError(ERRORS.SALE_RECEIPT_IS_ALREADY_CLOSED);
|
||||
}
|
||||
// Mark the sale receipt as closed on the storage.
|
||||
await SaleReceipt.query().findById(saleReceiptId).patch({
|
||||
closedAt: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,17 @@ export default class ViewsService implements IViewsService {
|
||||
* @param {number} tenantId -
|
||||
* @param {string} resourceModel -
|
||||
*/
|
||||
public async listResourceViews(tenantId: number, resourceModelName: string): Promise<IView[]> {
|
||||
public async listResourceViews(
|
||||
tenantId: number,
|
||||
resourceModelName: string,
|
||||
): Promise<IView[]> {
|
||||
this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModelName });
|
||||
|
||||
// Validate the resource model name is valid.
|
||||
const resourceModel = this.getResourceModelOrThrowError(tenantId, resourceModelName);
|
||||
|
||||
const { viewRepository } = this.tenancy.repositories(tenantId);
|
||||
return viewRepository.allByResource(resourceModel.name, ['columns', 'roles']);
|
||||
return viewRepository.allByResource(resourceModel.name, 'roles');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +59,10 @@ export default class ViewsService implements IViewsService {
|
||||
* @param {string} resourceName
|
||||
* @param {IViewRoleDTO[]} viewRoles
|
||||
*/
|
||||
private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) {
|
||||
private validateResourceRolesFieldsExistance(
|
||||
ResourceModel: IModel,
|
||||
viewRoles: IViewRoleDTO[],
|
||||
) {
|
||||
const resourceFieldsKeys = getModelFieldsKeys(ResourceModel);
|
||||
|
||||
const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey);
|
||||
@@ -73,7 +79,10 @@ export default class ViewsService implements IViewsService {
|
||||
* @param {string} resourceName
|
||||
* @param {IViewColumnDTO[]} viewColumns
|
||||
*/
|
||||
private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) {
|
||||
private validateResourceColumnsExistance(
|
||||
ResourceModel: IModel,
|
||||
viewColumns: IViewColumnDTO[],
|
||||
) {
|
||||
const resourceFieldsKeys = getModelFieldsKeys(ResourceModel);
|
||||
|
||||
const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey);
|
||||
@@ -118,7 +127,10 @@ export default class ViewsService implements IViewsService {
|
||||
* @param {number} tenantId
|
||||
* @param {number} resourceModel
|
||||
*/
|
||||
private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel {
|
||||
private getResourceModelOrThrowError(
|
||||
tenantId: number,
|
||||
resourceModel: string,
|
||||
): IModel {
|
||||
return this.resourceService.getResourceModel(tenantId, resourceModel);
|
||||
}
|
||||
|
||||
@@ -137,7 +149,9 @@ export default class ViewsService implements IViewsService {
|
||||
): void {
|
||||
const { View } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[views] trying to validate view name uniqiness.', { tenantId, resourceModel, viewName });
|
||||
this.logger.info('[views] trying to validate view name uniqiness.', {
|
||||
tenantId, resourceModel, viewName,
|
||||
});
|
||||
const foundViews = await View.query()
|
||||
.where('resource_model', resourceModel)
|
||||
.where('name', viewName)
|
||||
|
||||
Reference in New Issue
Block a user