mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
346 lines
10 KiB
JavaScript
346 lines
10 KiB
JavaScript
import express from 'express';
|
|
import { check, param, query } from 'express-validator';
|
|
import { ItemEntry } from '@/models';
|
|
import BaseController from '@/http/controllers/BaseController'
|
|
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
|
import CustomersService from '@/services/Customers/CustomersService';
|
|
import SaleEstimateService from '@/services/Sales/SalesEstimate';
|
|
import ItemsService from '@/services/Items/ItemsService';
|
|
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
|
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
|
|
|
export default class SalesEstimatesController extends BaseController {
|
|
/**
|
|
* Router constructor.
|
|
*/
|
|
static router() {
|
|
const router = express.Router();
|
|
|
|
router.post(
|
|
'/',
|
|
this.estimateValidationSchema,
|
|
validateMiddleware,
|
|
asyncMiddleware(this.validateEstimateCustomerExistance),
|
|
asyncMiddleware(this.validateEstimateNumberExistance),
|
|
asyncMiddleware(this.validateEstimateEntriesItemsExistance),
|
|
asyncMiddleware(this.newEstimate)
|
|
);
|
|
router.post(
|
|
'/:id', [
|
|
...this.validateSpecificEstimateSchema,
|
|
...this.estimateValidationSchema,
|
|
],
|
|
validateMiddleware,
|
|
asyncMiddleware(this.validateEstimateIdExistance),
|
|
asyncMiddleware(this.validateEstimateCustomerExistance),
|
|
asyncMiddleware(this.validateEstimateNumberExistance),
|
|
asyncMiddleware(this.validateEstimateEntriesItemsExistance),
|
|
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance),
|
|
asyncMiddleware(this.editEstimate)
|
|
);
|
|
router.delete(
|
|
'/:id', [
|
|
this.validateSpecificEstimateSchema,
|
|
],
|
|
validateMiddleware,
|
|
asyncMiddleware(this.validateEstimateIdExistance),
|
|
asyncMiddleware(this.deleteEstimate)
|
|
);
|
|
router.get(
|
|
'/:id',
|
|
this.validateSpecificEstimateSchema,
|
|
validateMiddleware,
|
|
asyncMiddleware(this.validateEstimateIdExistance),
|
|
asyncMiddleware(this.getEstimate)
|
|
);
|
|
router.get(
|
|
'/',
|
|
this.validateEstimateListSchema,
|
|
validateMiddleware,
|
|
asyncMiddleware(this.getEstimates)
|
|
);
|
|
return router;
|
|
}
|
|
|
|
/**
|
|
* Estimate validation schema.
|
|
*/
|
|
static get estimateValidationSchema() {
|
|
return [
|
|
check('customer_id').exists().isNumeric().toInt(),
|
|
check('estimate_date').exists().isISO8601(),
|
|
check('expiration_date').optional().isISO8601(),
|
|
check('reference').optional(),
|
|
check('estimate_number').exists().trim().escape(),
|
|
|
|
check('entries').exists().isArray({ min: 1 }),
|
|
check('entries.*.index').exists().isNumeric().toInt(),
|
|
check('entries.*.item_id').exists().isNumeric().toInt(),
|
|
check('entries.*.description').optional().trim().escape(),
|
|
check('entries.*.quantity').exists().isNumeric().toInt(),
|
|
check('entries.*.rate').exists().isNumeric().toFloat(),
|
|
check('entries.*.discount').optional().isNumeric().toFloat(),
|
|
|
|
check('note').optional().trim().escape(),
|
|
check('terms_conditions').optional().trim().escape(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Specific sale estimate validation schema.
|
|
*/
|
|
static get validateSpecificEstimateSchema() {
|
|
return [
|
|
param('id').exists().isNumeric().toInt(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sales estimates list validation schema.
|
|
*/
|
|
static get validateEstimateListSchema() {
|
|
return [
|
|
query('custom_view_id').optional().isNumeric().toInt(),
|
|
query('stringified_filter_roles').optional().isJSON(),
|
|
query('column_sort_by').optional(),
|
|
query('sort_order').optional().isIn(['desc', 'asc']),
|
|
query('page').optional().isNumeric().toInt(),
|
|
query('page_size').optional().isNumeric().toInt(),
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Validate whether the estimate customer exists on the storage.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {Function} next
|
|
*/
|
|
static async validateEstimateCustomerExistance(req, res, next) {
|
|
const estimate = { ...req.body };
|
|
const isCustomerExists = await CustomersService.isCustomerExists(
|
|
estimate.customer_id
|
|
);
|
|
if (!isCustomerExists) {
|
|
return res.status(404).send({
|
|
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
|
|
});
|
|
}
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Validate the estimate number unique on the storage.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {Function} next
|
|
*/
|
|
static async validateEstimateNumberExistance(req, res, next) {
|
|
const estimate = { ...req.body };
|
|
|
|
const isEstNumberUnqiue = await SaleEstimateService.isEstimateNumberUnique(
|
|
estimate.estimate_number,
|
|
req.params.id,
|
|
);
|
|
if (isEstNumberUnqiue) {
|
|
return res.boom.badRequest(null, {
|
|
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
|
|
});
|
|
}
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Validate the estimate entries items ids existance on the storage.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {Function} next
|
|
*/
|
|
static async validateEstimateEntriesItemsExistance(req, res, next) {
|
|
const estimate = { ...req.body };
|
|
const estimateItemsIds = estimate.entries.map(e => e.item_id);
|
|
|
|
// Validate items ids in estimate entries exists.
|
|
const notFoundItemsIds = await ItemsService.isItemsIdsExists(estimateItemsIds);
|
|
|
|
if (notFoundItemsIds.length > 0) {
|
|
return res.boom.badRequest(null, {
|
|
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
|
|
});
|
|
}
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Validate whether the sale estimate id exists on the storage.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {Function} next
|
|
*/
|
|
static async validateEstimateIdExistance(req, res, next) {
|
|
const { id: estimateId } = req.params;
|
|
const storedEstimate = await SaleEstimateService.getEstimate(estimateId);
|
|
|
|
if (!storedEstimate) {
|
|
return res.status(404).send({
|
|
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
|
});
|
|
}
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Validate sale invoice entries ids existance on the storage.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
* @param {Function} next
|
|
*/
|
|
static async valdiateInvoiceEntriesIdsExistance(req, res, next) {
|
|
const { id: saleInvoiceId } = req.params;
|
|
const saleInvoice = { ...req.body };
|
|
const entriesIds = saleInvoice.entries
|
|
.filter(e => e.id)
|
|
.map((e) => e.id);
|
|
|
|
const foundEntries = await ItemEntry.tenant().query()
|
|
.whereIn('id', entriesIds)
|
|
.where('reference_type', 'SaleInvoice')
|
|
.where('reference_id', saleInvoiceId);
|
|
|
|
if (foundEntries.length > 0) {
|
|
return res.status(400).send({
|
|
errors: [{ type: 'ENTRIES.IDS.NOT.EXISTS', code: 300 }],
|
|
});
|
|
}
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Handle create a new estimate with associated entries.
|
|
* @param {Request} req -
|
|
* @param {Response} res -
|
|
* @return {Response} res -
|
|
*/
|
|
static async newEstimate(req, res) {
|
|
const estimate = {
|
|
...req.body,
|
|
entries: req.body.entries.map((entry) => ({
|
|
...entry,
|
|
amount: ItemEntry.calcAmount(entry),
|
|
})),
|
|
};
|
|
const storedEstimate = await SaleEstimateService.createEstimate(estimate);
|
|
|
|
return res.status(200).send({ id: storedEstimate.id });
|
|
}
|
|
|
|
/**
|
|
* Handle update estimate details with associated entries.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
static async editEstimate(req, res) {
|
|
const { id: estimateId } = req.params;
|
|
const estimate = { ...req.body };
|
|
|
|
// Update estimate with associated estimate entries.
|
|
await SaleEstimateService.editEstimate(estimateId, estimate);
|
|
|
|
return res.status(200).send({ id: estimateId });
|
|
}
|
|
|
|
/**
|
|
* Deletes the given estimate with associated entries.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
static async deleteEstimate(req, res) {
|
|
const { id: estimateId } = req.params;
|
|
await SaleEstimateService.deleteEstimate(estimateId);
|
|
|
|
return res.status(200).send({ id: estimateId });
|
|
}
|
|
|
|
/**
|
|
* Retrieve the given estimate with associated entries.
|
|
*/
|
|
static async getEstimate(req, res) {
|
|
const { id: estimateId } = req.params;
|
|
const estimate = await SaleEstimateService.getEstimateWithEntries(estimateId);
|
|
|
|
return res.status(200).send({ estimate });
|
|
}
|
|
|
|
/**
|
|
* Retrieve estimates with pagination metadata.
|
|
* @param {Request} req
|
|
* @param {Response} res
|
|
*/
|
|
static async getEstimates(req, res) {
|
|
const filter = {
|
|
filter_roles: [],
|
|
sort_order: 'asc',
|
|
page: 1,
|
|
page_size: 10,
|
|
...req.query,
|
|
};
|
|
if (filter.stringified_filter_roles) {
|
|
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
|
}
|
|
const { SaleEstimate, Resource, View } = req.models;
|
|
const resource = await Resource.tenant().query()
|
|
.remember()
|
|
.where('name', 'sales_estimates')
|
|
.withGraphFetched('fields')
|
|
.first();
|
|
|
|
if (!resource) {
|
|
return res.status(400).send({
|
|
errors: [{ type: 'RESOURCE.NOT.FOUND', code: 200, }],
|
|
});
|
|
}
|
|
const viewMeta = await View.query()
|
|
.modify('allMetadata')
|
|
.modify('specificOrFavourite', filter.custom_view_id)
|
|
.where('resource_id', resource.id)
|
|
.first();
|
|
|
|
const listingBuilder = new DynamicListingBuilder();
|
|
const errorReasons = [];
|
|
|
|
listingBuilder.addView(viewMeta);
|
|
listingBuilder.addModelClass(SaleEstimate);
|
|
listingBuilder.addCustomViewId(filter.custom_view_id);
|
|
listingBuilder.addFilterRoles(filter.filter_roles);
|
|
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
|
|
|
|
const dynamicListing = new DynamicListing(listingBuilder);
|
|
|
|
if (dynamicListing instanceof Error) {
|
|
const errors = dynamicListingErrorsToResponse(dynamicListing);
|
|
errorReasons.push(...errors);
|
|
}
|
|
if (errorReasons.length > 0) {
|
|
return res.status(400).send({ errors: errorReasons });
|
|
}
|
|
|
|
const salesEstimates = await SaleEstimate.query().onBuild((builder) => {
|
|
dynamicListing.buildQuery()(builder);
|
|
builder.withGraphFetched('customer');
|
|
return builder;
|
|
}).pagination(filter.page - 1, filter.page_size);
|
|
|
|
return res.status(200).send({
|
|
sales_estimates: {
|
|
...salesEstimates,
|
|
...(viewMeta ? {
|
|
viewMeta: {
|
|
custom_view_id: viewMeta.id,
|
|
},
|
|
} : {}),
|
|
},
|
|
|
|
});
|
|
}
|
|
};
|