mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
- feat: Sales estimates.
- feat: Sales invoices. - feat: Sales payment receives. - feat: Purchases bills. - feat: Purchases bills payments that made to the vendors.
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@types/lodash": "^4.14.158",
|
||||
"babel-loader": "^8.0.6",
|
||||
"chai": "^4.2.0",
|
||||
"chai-http": "^4.3.0",
|
||||
@@ -81,6 +82,8 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^14.1.1",
|
||||
"sinon": "^7.4.2",
|
||||
"ts-loader": "^8.0.1",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-cli": "^3.3.7",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
|
||||
@@ -4,7 +4,7 @@ exports.up = function(knex) {
|
||||
table.increments();
|
||||
|
||||
table.string('customer_type');
|
||||
table.decimal('balance', 13, 3);
|
||||
table.decimal('balance', 13, 3).defaultTo(0);
|
||||
|
||||
table.string('first_name').nullable();
|
||||
table.string('last_name').nullable();
|
||||
@@ -36,6 +36,7 @@ exports.up = function(knex) {
|
||||
|
||||
table.text('note');
|
||||
table.boolean('active').defaultTo(true);
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ exports.up = function(knex) {
|
||||
table.increments();
|
||||
|
||||
table.string('customer_type');
|
||||
table.decimal('balance', 13, 3);
|
||||
table.decimal('balance', 13, 3).defaultTo(0);
|
||||
|
||||
table.string('first_name').nullable();
|
||||
table.string('last_name').nullable();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('sales_estimates', (table) => {
|
||||
table.increments();
|
||||
table.decimal('amount', 13, 3);
|
||||
table.integer('customer_id').unsigned();
|
||||
table.date('estimate_date');
|
||||
table.date('expiration_date');
|
||||
@@ -9,6 +10,8 @@ exports.up = function(knex) {
|
||||
table.string('estimate_number');
|
||||
table.text('note');
|
||||
table.text('terms_conditions');
|
||||
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('sales_estimate_entries', table => {
|
||||
table.increments();
|
||||
table.integer('estimate_id').unsigned();
|
||||
table.integer('item_id').unsigned();
|
||||
table.text('description');
|
||||
table.integer('discount').unsigned();
|
||||
table.integer('quantity').unsigned();
|
||||
table.integer('rate').unsigned();
|
||||
})
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('sales_estimate_entries');
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('sales_receipts', table => {
|
||||
table.increments();
|
||||
table.decimal('amount', 13, 3);
|
||||
table.integer('deposit_account_id').unsigned();
|
||||
table.integer('customer_id').unsigned();
|
||||
table.date('receipt_date');
|
||||
|
||||
@@ -13,6 +13,8 @@ exports.up = function(knex) {
|
||||
table.text('terms_conditions');
|
||||
|
||||
table.decimal('balance', 13, 3);
|
||||
table.decimal('payment_amount', 13, 3);
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ exports.up = function(knex) {
|
||||
table.increments();
|
||||
table.integer('customer_id').unsigned();
|
||||
table.date('payment_date');
|
||||
table.decimal('amount', 13, 3).defaultTo(0);
|
||||
table.string('reference_no');
|
||||
table.integer('deposit_account_id').unsigned();
|
||||
table.string('payment_receive_no');
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('sales_invoices_entries', table => {
|
||||
table.increments();
|
||||
table.integer('sale_invoice_id').unsigned();
|
||||
table.integer('item_id').unsigned();
|
||||
table.integer('index').unsigned();
|
||||
table.text('description');
|
||||
table.integer('discount').unsigned();
|
||||
table.integer('quantity').unsigned();
|
||||
table.integer('rate').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('sales_invoices_entries');
|
||||
};
|
||||
@@ -2,11 +2,17 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('bills', (table) => {
|
||||
table.increments();
|
||||
table.integer('vendor_id').unsigned();
|
||||
table.string('bill_number');
|
||||
table.date('bill_date');
|
||||
table.date('due_date');
|
||||
table.integer('vendor_id').unsigned();
|
||||
table.string('reference_no');
|
||||
table.string('status');
|
||||
table.text('note');
|
||||
|
||||
table.decimal('amount', 13, 3).defaultTo(0);
|
||||
table.decimal('payment_amount', 13, 3).defaultTo(0);
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ exports.up = function(knex) {
|
||||
table.string('payment_number');
|
||||
table.date('payment_date');
|
||||
table.string('payment_method');
|
||||
table.string('reference');
|
||||
table.integer('user_id').unsigned();
|
||||
table.text('description');
|
||||
table.timestamps();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('inventory_transactions', table => {
|
||||
table.increments('id');
|
||||
table.date('date');
|
||||
table.string('direction');
|
||||
table.integer('item_id');
|
||||
table.integer('quantity');
|
||||
table.decimal('rate', 13, 3);
|
||||
table.integer('remaining');
|
||||
|
||||
table.string('transaction_type');
|
||||
table.integer('transaction_id');
|
||||
|
||||
table.integer('inventory_transaction_id');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
|
||||
};
|
||||
@@ -1,17 +1,20 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('sales_receipt_entries', table => {
|
||||
return knex.schema.createTable('items_entries', (table) => {
|
||||
table.increments();
|
||||
table.integer('sale_receipt_id').unsigned();
|
||||
table.string('reference_type');
|
||||
table.string('reference_id');
|
||||
|
||||
table.integer('index').unsigned();
|
||||
table.integer('item_id');
|
||||
table.text('description');
|
||||
table.integer('discount').unsigned();
|
||||
table.integer('quantity').unsigned();
|
||||
table.integer('rate').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('sales_receipt_entries') ;
|
||||
return knex.schema.dropTableIfExists('items_entries');
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('bills_payments_entries', table => {
|
||||
table.increments();
|
||||
|
||||
table.integer('bill_payment_id').unsigned();
|
||||
table.integer('bill_id').unsigned();
|
||||
table.decimal('payment_amount', 13, 3).unsigned();
|
||||
})
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('bills_payments_entries');
|
||||
};
|
||||
@@ -119,8 +119,8 @@ exports.seed = (knex) => {
|
||||
name: 'Cost of Goods Sold (COGS)',
|
||||
key: 'cost_of_goods_sold',
|
||||
normal: 'debit',
|
||||
root_type: 'asset',
|
||||
child_type: 'current_asset',
|
||||
root_type: 'expenses',
|
||||
child_type: 'expenses',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
|
||||
@@ -126,6 +126,16 @@ exports.seed = (knex) => {
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Cost of Goods Sold (COGS)',
|
||||
account_type_id: 12,
|
||||
predefined: 1,
|
||||
parent_account_id: null,
|
||||
index: 1,
|
||||
active: 1,
|
||||
description: 1,
|
||||
}
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ exports.seed = (knex) => {
|
||||
{ id: 10, name: 'sales_receipts' },
|
||||
{ id: 11, name: 'sales_invoices' },
|
||||
{ id: 12, name: 'sales_payment_receives' },
|
||||
{ id: 13, name: 'bills' },
|
||||
{ id: 14, name: 'bill_payments' },
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -271,6 +271,154 @@ exports.seed = (knex) => {
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
|
||||
|
||||
// Sales Estimates
|
||||
{
|
||||
label_name: 'Customer name',
|
||||
key: 'customer_name',
|
||||
},
|
||||
{
|
||||
label_name: 'Amount',
|
||||
key: 'amount',
|
||||
},
|
||||
{
|
||||
label_name: 'Estimate number',
|
||||
key: 'estimate_number',
|
||||
},
|
||||
{
|
||||
label_name: 'Estimate date',
|
||||
key: 'estimate_date',
|
||||
},
|
||||
{
|
||||
label_name: 'Expiration date',
|
||||
key: 'expiration_date',
|
||||
},
|
||||
{
|
||||
label_name: 'Reference',
|
||||
key: 'reference',
|
||||
},
|
||||
{
|
||||
label_name: 'Terms and conditions',
|
||||
key: 'terms_conditions',
|
||||
},
|
||||
{
|
||||
label_name: 'Note',
|
||||
key: 'note',
|
||||
},
|
||||
|
||||
// Sales invoices
|
||||
// {
|
||||
// label_name: 'Customer name',
|
||||
// ley: 'customer_name',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Amount',
|
||||
// ley: 'amount',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Invoice number',
|
||||
// ley: 'invoice_no',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Invoice date',
|
||||
// ley: 'invoice_date',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Reference',
|
||||
// ley: 'reference',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Payment amount',
|
||||
// ley: 'payment_amount',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Invoice message',
|
||||
// ley: 'invoice_no',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Terms and conditions',
|
||||
// key: 'terms_conditions',
|
||||
// },
|
||||
|
||||
// // Sales receipts
|
||||
// {
|
||||
// label_name: 'Deposit account',
|
||||
// key: 'deposit_account',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Customer name',
|
||||
// key: 'customer_name',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Receipt date',
|
||||
// key: 'receipt_date',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Reference No',
|
||||
// key: 'reference',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Receipt message',
|
||||
// key: 'receipt_message',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Sent to email',
|
||||
// key: 'email_send_to',
|
||||
// },
|
||||
|
||||
// // Payment Receives
|
||||
// {
|
||||
// label_name: 'Customer name',
|
||||
// key: 'customer_name',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Payment date',
|
||||
// key: 'payment_date',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Amount',
|
||||
// key: 'amount',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Reference No',
|
||||
// key: 'reference',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Deposit account',
|
||||
// key: 'deposit_account',
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Payment receive no.',
|
||||
// key: 'payment_receive_no',
|
||||
// },
|
||||
|
||||
// // Purchases bills.
|
||||
// {
|
||||
// label_name: 'Bill number',
|
||||
// key: 'bill_number'
|
||||
// },
|
||||
|
||||
// {
|
||||
// label_name: 'Bill date',
|
||||
// key: 'bill_date'
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Amount',
|
||||
// key: 'amount'
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Vendor name',
|
||||
// key: 'vendor_name'
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Due date',
|
||||
// key: 'due_date'
|
||||
// },
|
||||
// {
|
||||
// label_name: 'Note',
|
||||
// key: 'note'
|
||||
// },
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -225,6 +225,7 @@ export default {
|
||||
const form = { ...req.body };
|
||||
|
||||
const customer = await Customer.query().insertAndFetch({
|
||||
balance: 0,
|
||||
...pick(form, [
|
||||
'customer_type',
|
||||
'first_name',
|
||||
|
||||
@@ -29,22 +29,18 @@ export default {
|
||||
this.editItem.validation,
|
||||
asyncMiddleware(this.editItem.handler)
|
||||
);
|
||||
|
||||
router.post('/',
|
||||
this.newItem.validation,
|
||||
asyncMiddleware(this.newItem.handler)
|
||||
);
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteItem.validation,
|
||||
asyncMiddleware(this.deleteItem.handler)
|
||||
);
|
||||
|
||||
router.delete('/',
|
||||
this.bulkDeleteItems.validation,
|
||||
asyncMiddleware(this.bulkDeleteItems.handler)
|
||||
);
|
||||
|
||||
router.get('/',
|
||||
this.listItems.validation,
|
||||
asyncMiddleware(this.listItems.handler)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import express from "express";
|
||||
import { check, param } from 'express-validator';
|
||||
import express from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import BillsService from "@/services/Purchases/Bills";
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import BillsService from '@/services/Purchases/Bills';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import VendorsServices from '@/services/Vendors/VendorsService';
|
||||
import ItemsService from '@/services/Items/ItemsService';
|
||||
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
||||
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
||||
import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/HasDynamicListing';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
export default class BillsController extends BaseController {
|
||||
/**
|
||||
@@ -13,31 +18,44 @@ export default class BillsController extends BaseController {
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', [
|
||||
...this.validationSchema,
|
||||
],
|
||||
router.post(
|
||||
'/',
|
||||
[...this.billValidationSchema],
|
||||
validateMiddleware,
|
||||
this.validateVendorExistance,
|
||||
this.validateItemsIds,
|
||||
this.validateBillNumberExists,
|
||||
this.newBill,
|
||||
asyncMiddleware(this.validateVendorExistance),
|
||||
asyncMiddleware(this.validateItemsIds),
|
||||
asyncMiddleware(this.validateBillNumberExists),
|
||||
asyncMiddleware(this.newBill)
|
||||
);
|
||||
// router.post('/:id', [
|
||||
// ...this.billValidationSchema,
|
||||
// ...this.validationSchema,
|
||||
// ],
|
||||
// validateMiddleware,
|
||||
// this.validateBillExistance,
|
||||
// this.validateVendorExistance,
|
||||
// this.validateItemsIds,
|
||||
// this.editBill,
|
||||
// );
|
||||
router.delete('/:id', [
|
||||
...this.billValidationSchema,
|
||||
],
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.billValidationSchema, ...this.specificBillValidationSchema],
|
||||
validateMiddleware,
|
||||
this.validateBillExistance,
|
||||
this.deleteBill
|
||||
asyncMiddleware(this.validateBillExistance),
|
||||
asyncMiddleware(this.validateVendorExistance),
|
||||
asyncMiddleware(this.validateItemsIds),
|
||||
asyncMiddleware(this.validateEntriesIdsExistance),
|
||||
asyncMiddleware(this.editBill)
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[...this.specificBillValidationSchema],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateBillExistance),
|
||||
asyncMiddleware(this.getBill)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[...this.billsListingValidationSchema],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.listingBills)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.specificBillValidationSchema],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateBillExistance),
|
||||
asyncMiddleware(this.deleteBill)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -45,7 +63,7 @@ export default class BillsController extends BaseController {
|
||||
/**
|
||||
* Common validation schema.
|
||||
*/
|
||||
static get validationSchema() {
|
||||
static get billValidationSchema() {
|
||||
return [
|
||||
check('bill_number').exists().trim().escape(),
|
||||
check('bill_date').exists().isISO8601(),
|
||||
@@ -53,24 +71,50 @@ export default class BillsController extends BaseController {
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.id').optional().isNumeric().toInt(),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount').optional().isNumeric().toFloat(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
]
|
||||
}
|
||||
|
||||
static get billValidationSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill validation schema.
|
||||
*/
|
||||
static get specificBillValidationSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bills list validation schema.
|
||||
*/
|
||||
static get billsListingValidationSchema() {
|
||||
return [
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether the vendor is exist.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateVendorExistance(req, res, next) {
|
||||
const isVendorExists = await VendorsServices.isVendorExists(req.body.vendor_id);
|
||||
const isVendorExists = await VendorsServices.isVendorExists(
|
||||
req.body.vendor_id
|
||||
);
|
||||
if (!isVendorExists) {
|
||||
return res.status(400).send({
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDOR.ID.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
@@ -79,9 +123,9 @@ export default class BillsController extends BaseController {
|
||||
|
||||
/**
|
||||
* Validates the given bill existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateBillExistance(req, res, next) {
|
||||
const isBillExists = await BillsService.isBillExists(req.params.id);
|
||||
@@ -94,16 +138,14 @@ export default class BillsController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries items ids.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* Validates the entries items ids.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateItemsIds(req, res, next) {
|
||||
const itemsIds = req.body.entries.map((e) => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(
|
||||
itemsIds
|
||||
);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(itemsIds);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
|
||||
@@ -114,12 +156,14 @@ export default class BillsController extends BaseController {
|
||||
|
||||
/**
|
||||
* Validates the bill number existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateBillNumberExists(req, res, next) {
|
||||
const isBillNoExists = await BillsService.isBillNoExists(req.body.bill_number);
|
||||
const isBillNoExists = await BillsService.isBillNoExists(
|
||||
req.body.bill_number
|
||||
);
|
||||
|
||||
if (isBillNoExists) {
|
||||
return res.status(400).send({
|
||||
@@ -130,16 +174,85 @@ export default class BillsController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bill and records journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* Validates the entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateEntriesIdsExistance(req, res, next) {
|
||||
const { id: billId } = req.params;
|
||||
const bill = { ...req.body };
|
||||
const { ItemEntry } = req.models;
|
||||
|
||||
const entriesIds = bill.entries.filter((e) => e.id).map((e) => e.id);
|
||||
|
||||
const storedEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.whereIn('reference_id', [billId])
|
||||
.whereIn('reference_type', ['Bill']);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.ENTRIES.IDS.NOT.FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bill and records journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async newBill(req, res, next) {
|
||||
const bill = { ...req.body };
|
||||
const { ItemEntry } = req.models;
|
||||
|
||||
const bill = {
|
||||
...req.body,
|
||||
entries: req.body.entries.map((entry) => ({
|
||||
...entry,
|
||||
amount: ItemEntry.calcAmount(entry),
|
||||
})),
|
||||
};
|
||||
const storedBill = await BillsService.createBill(bill);
|
||||
|
||||
return res.status(200).send({ id: storedBill });
|
||||
return res.status(200).send({ id: storedBill.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit bill details with associated entries and rewrites journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async editBill(req, res) {
|
||||
const { ItemEntry } = req.models;
|
||||
const { id: billId } = req.params;
|
||||
const bill = {
|
||||
...req.body,
|
||||
entries: req.body.entries.map((entry) => ({
|
||||
...entry,
|
||||
amount: ItemEntry.calcAmount(entry),
|
||||
})),
|
||||
};
|
||||
const editedBill = await BillsService.editBill(billId, bill);
|
||||
|
||||
return res.status(200).send({ id: billId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given bill details with associated item entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
static async getBill(req, res) {
|
||||
const { id: billId } = req.params;
|
||||
const bill = await BillsService.getBill(billId);
|
||||
|
||||
return res.status(200).send({ bill });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,4 +267,78 @@ export default class BillsController extends BaseController {
|
||||
|
||||
return res.status(200).send({ id: billId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing bills with pagination meta.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
static async listingBills(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 { Bill, View, Resource } = req.models;
|
||||
const resource = await Resource.query()
|
||||
.remember()
|
||||
.where('name', 'bills')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILLS_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.addModelClass(Bill);
|
||||
listingBuilder.addCustomViewId(filter.custom_view_id);
|
||||
listingBuilder.addFilterRoles(filter.filter_roles);
|
||||
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
|
||||
listingBuilder.addView(viewMeta);
|
||||
|
||||
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 bills = await Bill.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicListing.buildQuery()(builder);
|
||||
return builder;
|
||||
})
|
||||
.pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
bills: {
|
||||
...bills,
|
||||
...(viewMeta
|
||||
? {
|
||||
view_meta: {
|
||||
customViewId: viewMeta.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
|
||||
import express from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import BillPaymentsService from '@/services/Purchases/BillPayments';
|
||||
|
||||
export default class BillsPayments extends BaseController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', [
|
||||
...this.billPaymentSchemaValidation,
|
||||
],
|
||||
this.validatePaymentAccount,
|
||||
this.validatePaymentNumber,
|
||||
this.validateItemsIds,
|
||||
this.createBillPayment,
|
||||
);
|
||||
router.delete('/:id',
|
||||
this.validateBillPaymentExistance,
|
||||
this.deleteBillPayment,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill payments schema validation.
|
||||
*/
|
||||
static get billPaymentSchemaValidation() {
|
||||
return [
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_number').exists().trim().escape(),
|
||||
check('payment_date').exists(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount').optional().isNumeric().toFloat(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill payment existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateBillPaymentExistance(req, res, next) {
|
||||
const foundBillPayment = await BillPaymentsService.isBillPaymentExists(req.params.id);
|
||||
|
||||
if (!foundBillPayment) {
|
||||
return res.status(404).sned({
|
||||
errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
next(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validatePaymentAccount(req, res, next) {
|
||||
const isAccountExists = AccountsService.isAccountExists(req.body.payment_account_id);
|
||||
|
||||
if (!isAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment number uniqness.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} res
|
||||
*/
|
||||
static async validatePaymentNumber(req, res, next) {
|
||||
const isNumberExists = await BillPaymentsService.isBillNoExists(req.body.payment_number);
|
||||
|
||||
if (!isNumberExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
next(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* validate entries items ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateItemsIds(req, res, next) {
|
||||
const itemsIds = req.body.entries.map((e) => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(
|
||||
itemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bill payment.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async createBillPayment(req, res) {
|
||||
const billPayment = { ...req.body };
|
||||
const storedPayment = await BillPaymentsService.createBillPayment(billPayment);
|
||||
|
||||
return res.status(200).send({ id: storedPayment.id });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response} res
|
||||
*/
|
||||
static async deleteBillPayment(req, res) {
|
||||
|
||||
}
|
||||
}
|
||||
338
server/src/http/controllers/Purchases/BillsPayments.ts
Normal file
338
server/src/http/controllers/Purchases/BillsPayments.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
|
||||
import { Router } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import BillPaymentsService from '@/services/Purchases/BillPayments';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import ItemsService from '@/services/Items/ItemsService';
|
||||
import { IBillPaymentEntry, IBillPayment } from '@/interfaces/BillPayment';
|
||||
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
||||
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
||||
import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/hasDynamicListing';
|
||||
|
||||
export default class BillsPayments extends BaseController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/', [
|
||||
...this.billPaymentSchemaValidation,
|
||||
],
|
||||
asyncMiddleware(this.validateBillPaymentVendorExistance),
|
||||
asyncMiddleware(this.validatePaymentAccount),
|
||||
asyncMiddleware(this.validatePaymentNumber),
|
||||
asyncMiddleware(this.validateItemsIds),
|
||||
asyncMiddleware(this.createBillPayment),
|
||||
);
|
||||
router.post('/:id', [
|
||||
...this.billPaymentSchemaValidation,
|
||||
...this.specificBillPaymentValidateSchema,
|
||||
],
|
||||
asyncMiddleware(this.validateBillPaymentVendorExistance),
|
||||
asyncMiddleware(this.validatePaymentAccount),
|
||||
asyncMiddleware(this.validatePaymentNumber),
|
||||
asyncMiddleware(this.validateItemsIds),
|
||||
asyncMiddleware(this.validateEntriesIds),
|
||||
asyncMiddleware(this.editBillPayment),
|
||||
)
|
||||
router.delete('/:id',
|
||||
this.specificBillPaymentValidateSchema,
|
||||
asyncMiddleware(this.validateBillPaymentExistance),
|
||||
asyncMiddleware(this.deleteBillPayment),
|
||||
);
|
||||
router.get('/:id',
|
||||
this.specificBillPaymentValidateSchema,
|
||||
asyncMiddleware(this.validateBillPaymentExistance),
|
||||
asyncMiddleware(this.getBillPayment),
|
||||
);
|
||||
router.get('/',
|
||||
this.listingValidationSchema,
|
||||
asyncMiddleware(this.getBillsPayments)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill payments schema validation.
|
||||
*/
|
||||
static get billPaymentSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_number').exists().trim().escape(),
|
||||
check('payment_date').exists(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('reference').optional().trim().escape(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.id').optional().isNumeric().toInt(),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount').optional().isNumeric().toFloat(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific bill payment schema validation.
|
||||
*/
|
||||
static get specificBillPaymentValidateSchema(): ValidationChain[] {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the bill payment vendor exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateBillPaymentVendorExistance(req: Request, res: Response, next: any ) {
|
||||
const billPayment = req.body;
|
||||
const { Vendor } = req.models;
|
||||
const isVendorExists = await Vendor.query('id', billPayment.vendor_id).first();
|
||||
|
||||
if (!isVendorExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.PAYMENT.VENDOR.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill payment existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateBillPaymentExistance(req: Request, res: Response, next: any ) {
|
||||
const foundBillPayment = await BillPaymentsService.isBillPaymentExists(req.params.id);
|
||||
|
||||
if (!foundBillPayment) {
|
||||
return res.status(404).sned({
|
||||
errors: [{ type: 'BILL.PAYMENT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
next(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validatePaymentAccount(req: Request, res: Response, next: any) {
|
||||
const billPayment = { ...req.body };
|
||||
const isAccountExists = AccountsService.isAccountExists(billPayment);
|
||||
|
||||
if (!isAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment number uniqness.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} res
|
||||
*/
|
||||
static async validatePaymentNumber(req: Request, res: Response, next: any) {
|
||||
const billPayment = { ...req.body };
|
||||
const isNumberExists = await BillPaymentsService.isBillNoExists(billPayment);
|
||||
|
||||
if (!isNumberExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT.NUMBER.NOT.UNIQUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
next(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* validate entries items ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateItemsIds(req: Request, res: Response, next: Function) {
|
||||
const billPayment: any = { ...req.body };
|
||||
const itemsIds = billPayment.entries.map((e) => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(
|
||||
itemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries ids in edit bill payment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
static async validateEntriesIds(req: Request, res: Response, next: Function) {
|
||||
const { BillPaymentEntry } = req.models;
|
||||
const { id: billPaymentId } = req.params;
|
||||
const billPayment = { id: billPaymentId, ...req.body };
|
||||
|
||||
const entriesIds = billPayment.entries
|
||||
.filter((entry: IBillPaymentEntry) => entry.id)
|
||||
.map((entry: IBillPaymentEntry) => entry.id);
|
||||
|
||||
const storedEntries = await BillPaymentEntry.tenant().query()
|
||||
.where('bill_payment_id', billPaymentId);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry: IBillPaymentEntry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bill payment.
|
||||
* @async
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async createBillPayment(req: Request, res: Response) {
|
||||
const billPayment = { ...req.body };
|
||||
const storedPayment = await BillPaymentsService.createBillPayment(billPayment);
|
||||
|
||||
return res.status(200).send({ id: storedPayment.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given bill payment details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async editBillPayment(req: Request, res: Response) {
|
||||
const billPayment = { ...req.body };
|
||||
|
||||
return res.status(200).send({ id: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the bill payment and revert the journal
|
||||
* transactions with accounts balance.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response} res -
|
||||
*/
|
||||
static async deleteBillPayment(req: Request, res: Response) {
|
||||
const { id: billPaymentId } = req.params;
|
||||
const billPayment = req.body;
|
||||
|
||||
await BillPaymentsService.deleteBillPayment(billPaymentId);
|
||||
|
||||
return res.status(200).send({ id: billPaymentId });
|
||||
}
|
||||
|
||||
static async getBillPayment(req: Request, res: Response) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bills payment list validation schema.
|
||||
*/
|
||||
static get listingValidationSchema(): ValidationChain[] {
|
||||
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(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve bills payments listing with pagination metadata.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
static async getBillsPayments(req: Request, res: Response) {
|
||||
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 { BillPayment, View, Resource } = req.models;
|
||||
|
||||
const resource = await Resource.query()
|
||||
.where('name', 'bill_payments')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BILL.PAYMENTS.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.addModelClass(BillPayment);
|
||||
listingBuilder.addCustomViewId(filter.custom_view_id);
|
||||
listingBuilder.addFilterRoles(filter.filter_roles);
|
||||
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
|
||||
listingBuilder.addView(viewMeta);
|
||||
|
||||
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 billPayments = await BillPayment.query().onBuild((builder) => {
|
||||
dynamicListing.buildQuery()(builder);
|
||||
return builder;
|
||||
});
|
||||
return res.status(200).send({
|
||||
billPayments,
|
||||
...(viewMeta
|
||||
? {
|
||||
customViewId: viewMeta.id,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,78 @@
|
||||
import express from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import PaymentReceiveService from '@/services/Sales/PaymentReceive';
|
||||
import CustomersService from '@/services/Customers/CustomersService';
|
||||
import SaleInvoicesService from '@/services/Sales/SaleInvoice';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import { PaymentReceiveEntry } from '@/models';
|
||||
|
||||
export default class PaymentReceivesController {
|
||||
export default class PaymentReceivesController extends BaseController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.newPaymentReceiveValidation,
|
||||
validateMiddleware,
|
||||
this.validatePaymentReceiveNoExistance,
|
||||
this.validateCustomerExistance,
|
||||
this.validateDepositAccount,
|
||||
this.validateInvoicesIDs,
|
||||
asyncMiddleware(this.newPaymentReceive),
|
||||
);
|
||||
router.post('/:id',
|
||||
router.post(
|
||||
'/:id',
|
||||
this.editPaymentReceiveValidation,
|
||||
validateMiddleware,
|
||||
this.validatePaymentReceiveNoExistance,
|
||||
this.validateCustomerExistance,
|
||||
this.validateDepositAccount,
|
||||
this.validateInvoicesIDs,
|
||||
asyncMiddleware(this.editPaymentReceive),
|
||||
asyncMiddleware(this.validatePaymentReceiveExistance),
|
||||
asyncMiddleware(this.validatePaymentReceiveNoExistance),
|
||||
asyncMiddleware(this.validateCustomerExistance),
|
||||
asyncMiddleware(this.validateDepositAccount),
|
||||
asyncMiddleware(this.validateInvoicesIDs),
|
||||
asyncMiddleware(this.validateEntriesIdsExistance),
|
||||
asyncMiddleware(this.editPaymentReceive)
|
||||
);
|
||||
router.get('/:id',
|
||||
router.post(
|
||||
'/',
|
||||
this.newPaymentReceiveValidation,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validatePaymentReceiveNoExistance),
|
||||
asyncMiddleware(this.validateCustomerExistance),
|
||||
asyncMiddleware(this.validateDepositAccount),
|
||||
asyncMiddleware(this.validateInvoicesIDs),
|
||||
asyncMiddleware(this.validateInvoicesPaymentsAmount),
|
||||
asyncMiddleware(this.newPaymentReceive)
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
this.paymentReceiveValidation,
|
||||
validateMiddleware,
|
||||
this.validatePaymentReceiveExistance,
|
||||
asyncMiddleware(this.getPaymentReceive),
|
||||
asyncMiddleware(this.validatePaymentReceiveExistance),
|
||||
asyncMiddleware(this.getPaymentReceive)
|
||||
);
|
||||
router.delete('/:id',
|
||||
router.get(
|
||||
'/',
|
||||
this.validatePaymentReceiveList,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.getPaymentReceiveList),
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.paymentReceiveValidation,
|
||||
validateMiddleware,
|
||||
this.validatePaymentReceiveExistance,
|
||||
asyncMiddleware(this.deletePaymentReceive),
|
||||
asyncMiddleware(this.validatePaymentReceiveExistance),
|
||||
asyncMiddleware(this.deletePaymentReceive)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment receive number existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validatePaymentReceiveNoExistance(req, res, next) {
|
||||
const isPaymentNoExists = await PaymentReceiveService.isPaymentReceiveNoExists(
|
||||
req.body.payment_receive_no,
|
||||
req.params.id,
|
||||
);
|
||||
if (isPaymentNoExists) {
|
||||
return res.status(400).send({
|
||||
@@ -64,10 +84,13 @@ export default class PaymentReceivesController {
|
||||
|
||||
/**
|
||||
* Validates the payment receive existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validatePaymentReceiveExistance(req, res, next) {
|
||||
const isPaymentNoExists = await PaymentReceiveService.isPaymentReceiveExists(
|
||||
req.params.id,
|
||||
req.params.id
|
||||
);
|
||||
if (!isPaymentNoExists) {
|
||||
return res.status(400).send({
|
||||
@@ -79,10 +102,13 @@ export default class PaymentReceivesController {
|
||||
|
||||
/**
|
||||
* Validate the deposit account id existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateDepositAccount(req, res, next) {
|
||||
const isDepositAccExists = await AccountsService.isAccountExists(
|
||||
req.body.deposit_account_id,
|
||||
req.body.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccExists) {
|
||||
return res.status(400).send({
|
||||
@@ -94,13 +120,13 @@ export default class PaymentReceivesController {
|
||||
|
||||
/**
|
||||
* Validates the `customer_id` existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateCustomerExistance(req, res, next) {
|
||||
const isCustomerExists = await CustomersService.isCustomerExists(
|
||||
req.body.customer_id,
|
||||
req.body.customer_id
|
||||
);
|
||||
if (!isCustomerExists) {
|
||||
return res.status(400).send({
|
||||
@@ -112,11 +138,15 @@ export default class PaymentReceivesController {
|
||||
|
||||
/**
|
||||
* Validates the invoices IDs existance.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
static async validateInvoicesIDs(req, res, next) {
|
||||
const invoicesIds = req.body.entries.map((e) => e.invoice_id);
|
||||
const notFoundInvoicesIDs = await SaleInvoicesService.isInvoicesExist(invoicesIds);
|
||||
|
||||
const notFoundInvoicesIDs = await SaleInvoicesService.isInvoicesExist(
|
||||
invoicesIds
|
||||
);
|
||||
if (notFoundInvoicesIDs.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVOICES.IDS.NOT.FOUND', code: 500 }],
|
||||
@@ -125,6 +155,72 @@ export default class PaymentReceivesController {
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates entries invoice payment amount.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
static async validateInvoicesPaymentsAmount(req, res, next) {
|
||||
const { SaleInvoice } = req.models;
|
||||
const invoicesIds = req.body.entries.map((e) => e.invoice_id);
|
||||
const storedInvoices = await SaleInvoice.tenant()
|
||||
.query()
|
||||
.whereIn('id', invoicesIds);
|
||||
|
||||
const storedInvoicesMap = new Map(
|
||||
storedInvoices.map((invoice) => [invoice.id, invoice])
|
||||
);
|
||||
const hasWrongPaymentAmount = [];
|
||||
|
||||
req.body.entries.forEach((entry, index) => {
|
||||
const entryInvoice = storedInvoicesMap.get(entry.invoice_id);
|
||||
const { dueAmount } = entryInvoice;
|
||||
|
||||
if (dueAmount < entry.payment_amount) {
|
||||
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
|
||||
}
|
||||
});
|
||||
if (hasWrongPaymentAmount.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVOICE.PAYMENT.AMOUNT',
|
||||
code: 200,
|
||||
indexes: hasWrongPaymentAmount,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the payment receive entries IDs existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
static async validateEntriesIdsExistance(req, res, next) {
|
||||
const paymentReceive = { id: req.params.id, ...req.body };
|
||||
const entriesIds = paymentReceive.entries
|
||||
.filter(entry => entry.id)
|
||||
.map(entry => entry.id);
|
||||
|
||||
const storedEntries = await PaymentReceiveEntry.tenant().query()
|
||||
.where('payment_receive_id', paymentReceive.id);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ENTEIES.IDS.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive schema.
|
||||
* @return {Array}
|
||||
@@ -136,12 +232,19 @@ export default class PaymentReceivesController {
|
||||
check('reference_no').optional(),
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_receive_no').exists().trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.payment_amount').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* New payment receive validation schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
static get newPaymentReceiveValidation() {
|
||||
return [...this.paymentReceiveSchema];
|
||||
}
|
||||
@@ -151,8 +254,9 @@ export default class PaymentReceivesController {
|
||||
*/
|
||||
static async newPaymentReceive(req, res) {
|
||||
const paymentReceive = { ...req.body };
|
||||
const storedPaymentReceive = await PaymentReceiveService.createPaymentReceive(paymentReceive);
|
||||
|
||||
const storedPaymentReceive = await PaymentReceiveService.createPaymentReceive(
|
||||
paymentReceive
|
||||
);
|
||||
return res.status(200).send({ id: storedPaymentReceive.id });
|
||||
}
|
||||
|
||||
@@ -167,15 +271,27 @@ export default class PaymentReceivesController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given payment receive.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* Edit the given payment receive.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
static async editPaymentReceive(req, res) {
|
||||
const paymentReceive = { ...req.body };
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
await PaymentReceiveService.editPaymentReceive(paymentReceiveId, paymentReceive);
|
||||
const { PaymentReceive } = req.models;
|
||||
|
||||
// Retrieve the payment receive before updating.
|
||||
const oldPaymentReceive = await PaymentReceive.query()
|
||||
.where('id', paymentReceiveId)
|
||||
.withGraphFetched('entries')
|
||||
.first();
|
||||
|
||||
await PaymentReceiveService.editPaymentReceive(
|
||||
paymentReceiveId,
|
||||
paymentReceive,
|
||||
oldPaymentReceive,
|
||||
);
|
||||
return res.status(200).send({ id: paymentReceiveId });
|
||||
}
|
||||
|
||||
@@ -183,20 +299,27 @@ export default class PaymentReceivesController {
|
||||
* Validate payment receive parameters.
|
||||
*/
|
||||
static get paymentReceiveValidation() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
];
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delets the given payment receive id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async deletePaymentReceive(req, res) {
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
await PaymentReceiveService.deletePaymentReceive(paymentReceiveId);
|
||||
|
||||
const { PaymentReceive } = req.models;
|
||||
const storedPaymentReceive = await PaymentReceive.query()
|
||||
.where('id', paymentReceiveId)
|
||||
.withGraphFetched('entries')
|
||||
.first();
|
||||
|
||||
await PaymentReceiveService.deletePaymentReceive(
|
||||
paymentReceiveId,
|
||||
storedPaymentReceive
|
||||
);
|
||||
return res.status(200).send({ id: paymentReceiveId });
|
||||
}
|
||||
|
||||
@@ -208,8 +331,94 @@ export default class PaymentReceivesController {
|
||||
*/
|
||||
static async getPaymentReceive(req, res) {
|
||||
const { id: paymentReceiveId } = req.params;
|
||||
const paymentReceive = await PaymentReceiveService.getPaymentReceive(paymentReceiveId);
|
||||
|
||||
const paymentReceive = await PaymentReceiveService.getPaymentReceive(
|
||||
paymentReceiveId
|
||||
);
|
||||
return res.status(200).send({ paymentReceive });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment receive list validation schema.
|
||||
*/
|
||||
static async validatePaymentReceiveList() {
|
||||
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(),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment receive list with pagination metadata.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
static async getPaymentReceiveList(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 { Resource, PaymentReceive, View } = req.models;
|
||||
const resource = await Resource.query()
|
||||
.remember()
|
||||
.where('name', 'payment_receives')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PAYMENT_RECEIVES_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.addModelClass(Bill);
|
||||
listingBuilder.addCustomViewId(filter.custom_view_id);
|
||||
listingBuilder.addFilterRoles(filter.filter_roles);
|
||||
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
|
||||
listingBuilder.addView(viewMeta);
|
||||
|
||||
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 paymentReceives = await PaymentReceive.query().onBuild((builder) => {
|
||||
dynamicListing.buildQuery()(builder);
|
||||
return builder;
|
||||
});
|
||||
return res.status(200).send({
|
||||
payment_receives: {
|
||||
...paymentReceives,
|
||||
...(viewMeta
|
||||
? {
|
||||
viewMeta: {
|
||||
customViewId: viewMeta.id,
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
@@ -8,278 +10,334 @@ import ItemsService from '@/services/Items/ItemsService';
|
||||
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
||||
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
||||
|
||||
export default {
|
||||
router() {
|
||||
export default class SalesEstimatesController extends BaseController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
this.newEstimate.validation,
|
||||
this.estimateValidationSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.newEstimate.handler)
|
||||
asyncMiddleware(this.validateEstimateCustomerExistance),
|
||||
asyncMiddleware(this.validateEstimateNumberExistance),
|
||||
asyncMiddleware(this.validateEstimateEntriesItemsExistance),
|
||||
asyncMiddleware(this.newEstimate)
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
this.editEstimate.validation,
|
||||
'/:id', [
|
||||
...this.validateSpecificEstimateSchema,
|
||||
...this.estimateValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.editEstimate.handler)
|
||||
asyncMiddleware(this.validateEstimateIdExistance),
|
||||
asyncMiddleware(this.validateEstimateCustomerExistance),
|
||||
asyncMiddleware(this.validateEstimateNumberExistance),
|
||||
asyncMiddleware(this.validateEstimateEntriesItemsExistance),
|
||||
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance),
|
||||
asyncMiddleware(this.editEstimate)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteEstimate.validation,
|
||||
'/:id', [
|
||||
this.validateSpecificEstimateSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.deleteEstimate.handler)
|
||||
asyncMiddleware(this.validateEstimateIdExistance),
|
||||
asyncMiddleware(this.deleteEstimate)
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
this.getEstimate.validation,
|
||||
this.validateSpecificEstimateSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.getEstimate.handler)
|
||||
asyncMiddleware(this.validateEstimateIdExistance),
|
||||
asyncMiddleware(this.getEstimate)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
this.getEstimates.validation,
|
||||
this.validateEstimateListSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.getEstimates.handler)
|
||||
asyncMiddleware(this.getEstimates)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle create a new estimate with associated entries.
|
||||
* Estimate validation schema.
|
||||
*/
|
||||
newEstimate: {
|
||||
validation: [
|
||||
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(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
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 }],
|
||||
});
|
||||
}
|
||||
const isEstNumberUnqiue = await SaleEstimateService.isEstimateNumberUnique(
|
||||
estimate.estimate_number
|
||||
);
|
||||
if (isEstNumberUnqiue) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
// Validate items ids in estimate entries exists.
|
||||
const estimateItemsIds = estimate.entries.map(e => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(estimateItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
const storedEstimate = await SaleEstimateService.createEstimate(estimate);
|
||||
|
||||
return res.status(200).send({ id: storedEstimate.id });
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update estimate details with associated entries.
|
||||
* Specific sale estimate validation schema.
|
||||
*/
|
||||
editEstimate: {
|
||||
validation: [
|
||||
static get validateSpecificEstimateSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
|
||||
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.*.id').optional().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(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id: estimateId } = req.params;
|
||||
const estimate = { ...req.body };
|
||||
const storedEstimate = await SaleEstimateService.getEstimate(estimateId);
|
||||
|
||||
if (!storedEstimate) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
const isCustomerExists = await CustomersService.isCustomerExists(
|
||||
estimate.customer_id
|
||||
);
|
||||
if (!isCustomerExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
// Validate the estimate number is unique except on the current estimate id.
|
||||
const foundEstimateNumbers = await SaleEstimateService.isEstimateNumberUnique(
|
||||
estimate.estimate_number,
|
||||
storedEstimate.id, // Exclude the given estimate id.
|
||||
);
|
||||
if (foundEstimateNumbers) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE', code: 300 }],
|
||||
});
|
||||
}
|
||||
// Validate items ids in estimate entries exists.
|
||||
const estimateItemsIds = estimate.entries.map(e => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(estimateItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
// Validate the sale estimate entries IDs that not found.
|
||||
const notFoundEntriesIds = await SaleEstimateService.isEstimateEntriesIDsExists(
|
||||
storedEstimate.id,
|
||||
estimate
|
||||
);
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ESTIMATE.NOT.FOUND.ENTRIES.IDS', code: 500 }],
|
||||
});
|
||||
}
|
||||
// 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.
|
||||
* Sales estimates list validation schema.
|
||||
*/
|
||||
deleteEstimate: {
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id: estimateId } = req.params;
|
||||
const isEstimateExists = await SaleEstimateService.isEstimateExists(estimateId);
|
||||
|
||||
if (!isEstimateExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
await SaleEstimateService.deleteEstimate(estimateId);
|
||||
|
||||
return res.status(200).send({ id: estimateId });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the given estimate with associated entries.
|
||||
*/
|
||||
getEstimate: {
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id: estimateId } = req.params;
|
||||
const estimate = await SaleEstimateService.getEstimateWithEntries(estimateId);
|
||||
|
||||
if (!estimate) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.ESTIMATE.ID.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
return res.status(200).send({ estimate });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve estimates with pagination metadata.
|
||||
*/
|
||||
getEstimates: {
|
||||
validation: [
|
||||
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']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const filter = {
|
||||
filter_roles: [],
|
||||
sort_order: 'asc',
|
||||
...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);
|
||||
return builder;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
sales_estimates: salesEstimates,
|
||||
/**
|
||||
* 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.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((query) => {
|
||||
dynamicListing.buildQuery()(builder);
|
||||
return builder;
|
||||
}).pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
sales_estimates: {
|
||||
...salesEstimates,
|
||||
...(viewMeta ? {
|
||||
custom_view_id: viewMeta.id,
|
||||
viewMeta: {
|
||||
custom_view_id: viewMeta.id,
|
||||
},
|
||||
} : {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,52 +1,73 @@
|
||||
import express from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { ItemEntry } from '@/models';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import SaleInvoiceService from '@/services/Sales/SaleInvoice';
|
||||
import ItemsService from '@/services/Items/ItemsService';
|
||||
import CustomersService from '@/services/Customers/CustomersService';
|
||||
import { SaleInvoice } from '@/models';
|
||||
import DynamicListing, { DYNAMIC_LISTING_ERRORS } from '@/services/DynamicListing/DynamicListing';
|
||||
import DynamicListingBuilder from '../../../services/DynamicListing/DynamicListingBuilder';
|
||||
import {
|
||||
dynamicListingErrorsToResponse
|
||||
} from '@/services/DynamicListing/hasDynamicListing';
|
||||
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
||||
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
||||
import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/hasDynamicListing';
|
||||
import { SaleInvoice } from '../../../models';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
export default {
|
||||
router() {
|
||||
export default class SaleInvoicesController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
this.newSaleInvoice.validation,
|
||||
this.saleInvoiceValidationSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.newSaleInvoice.handler)
|
||||
asyncMiddleware(this.validateInvoiceNumberUnique),
|
||||
asyncMiddleware(this.validateInvoiceItemsIdsExistance),
|
||||
asyncMiddleware(this.newSaleInvoice)
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
this.editSaleInvoice.validation,
|
||||
[
|
||||
...this.saleInvoiceValidationSchema,
|
||||
...this.specificSaleInvoiceValidation,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.editSaleInvoice.handler)
|
||||
asyncMiddleware(this.validateInvoiceExistance),
|
||||
asyncMiddleware(this.validateInvoiceNumberUnique),
|
||||
asyncMiddleware(this.validateInvoiceItemsIdsExistance),
|
||||
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance),
|
||||
asyncMiddleware(this.validateEntriesIdsExistance),
|
||||
asyncMiddleware(this.editSaleInvoice)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteSaleInvoice.validation,
|
||||
this.specificSaleInvoiceValidation,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.deleteSaleInvoice.handler)
|
||||
asyncMiddleware(this.validateInvoiceExistance),
|
||||
asyncMiddleware(this.deleteSaleInvoice)
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
this.specificSaleInvoiceValidation,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateInvoiceExistance),
|
||||
asyncMiddleware(this.getSaleInvoice)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
this.getSalesInvoices.validation,
|
||||
asyncMiddleware(this.getSalesInvoices.handler)
|
||||
);
|
||||
this.saleInvoiceListValidationSchema,
|
||||
asyncMiddleware(this.getSalesInvoices)
|
||||
);
|
||||
return router;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice.
|
||||
* Sale invoice validation schema.
|
||||
*/
|
||||
newSaleInvoice: {
|
||||
validation: [
|
||||
static get saleInvoiceValidationSchema() {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('invoice_date').exists().isISO8601(),
|
||||
check('due_date').exists().isISO8601(),
|
||||
@@ -58,204 +79,295 @@ export default {
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount').optional().isNumeric().toFloat(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const errorReasons = [];
|
||||
const saleInvoice = { ...req.body };
|
||||
const isInvoiceNoExists = await SaleInvoiceService.isSaleInvoiceNumberExists(
|
||||
saleInvoice.invoice_no
|
||||
);
|
||||
if (isInvoiceNoExists) {
|
||||
errorReasons.push({ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 });
|
||||
}
|
||||
const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id);
|
||||
const isItemsIdsExists = await ItemsService.isItemsIdsExists(
|
||||
entriesItemsIds
|
||||
);
|
||||
if (isItemsIdsExists.length > 0) {
|
||||
errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 });
|
||||
}
|
||||
// Validate the customer id exists.
|
||||
const isCustomerIDExists = await CustomersService.isCustomerExists(
|
||||
saleInvoice.customer_id
|
||||
);
|
||||
if (!isCustomerIDExists) {
|
||||
errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Creates a new sale invoice with associated entries.
|
||||
const storedSaleInvoice = await SaleInvoiceService.createSaleInvoice(
|
||||
saleInvoice
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleInvoice.id });
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit sale invoice details.
|
||||
* Specific sale invoice validation schema.
|
||||
*/
|
||||
editSaleInvoice: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
static get specificSaleInvoiceValidation() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('invoice_date').exists(),
|
||||
check('due_date').exists(),
|
||||
check('invoice_no').exists().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('status').exists().trim().escape(),
|
||||
|
||||
check('invoice_message').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.quantity').exists().isNumeric().toFloat(),
|
||||
check('entries.*.discount').optional().isNumeric().toFloat(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoice = { ...req.body };
|
||||
const isSaleInvoiceExists = await SaleInvoiceService.isSaleInvoiceExists(
|
||||
saleInvoiceId
|
||||
);
|
||||
if (!isSaleInvoiceExists) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ type: 'SALE.INVOICE.NOT.FOUND', code: 200 });
|
||||
}
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate the invoice number uniqness.
|
||||
const isInvoiceNoExists = await SaleInvoiceService.isSaleInvoiceNumberExists(
|
||||
saleInvoice.invoice_no,
|
||||
saleInvoiceId
|
||||
);
|
||||
if (isInvoiceNoExists) {
|
||||
errorReasons.push({ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 });
|
||||
}
|
||||
// Validate sale invoice entries items IDs.
|
||||
const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id);
|
||||
const isItemsIdsExists = await ItemsService.isItemsIdsExists(
|
||||
entriesItemsIds
|
||||
);
|
||||
if (isItemsIdsExists.length > 0) {
|
||||
errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 });
|
||||
}
|
||||
// Validate the customer id exists.
|
||||
const isCustomerIDExists = await CustomersService.isCustomerExists(
|
||||
saleInvoice.customer_id
|
||||
);
|
||||
if (!isCustomerIDExists) {
|
||||
errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Update the given sale invoice details.
|
||||
await SaleInvoiceService.editSaleInvoice(saleInvoiceId, saleInvoice);
|
||||
|
||||
return res.status(200).send({ id: saleInvoice.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the sale invoice with associated entries and journal transactions.
|
||||
*/
|
||||
deleteSaleInvoice: {
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const isSaleInvoiceExists = await SaleInvoiceService.isSaleInvoiceExists(
|
||||
saleInvoiceId
|
||||
);
|
||||
if (!isSaleInvoiceExists) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ errors: [{ type: 'SALE.INVOICE.NOT.FOUND', code: 200 }] });
|
||||
}
|
||||
// Deletes the sale invoice with associated entries and journal transaction.
|
||||
await SaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve paginated sales invoices with custom view metadata.
|
||||
*/
|
||||
getSalesInvoices: {
|
||||
validation: [
|
||||
static get saleInvoiceListValidationSchema() {
|
||||
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']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const filter = {
|
||||
filter_roles: [],
|
||||
sort_order: 'asc',
|
||||
...req.query,
|
||||
};
|
||||
if (filter.stringified_filter_roles) {
|
||||
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
||||
}
|
||||
const { SaleInvoice, Resource } = req.models;
|
||||
const resource = await Resource.query()
|
||||
.remember()
|
||||
.where('name', 'sales_invoices')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
];
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SALES_INVOICES_RESOURCE_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
const viewMeta = View.query()
|
||||
.modify('allMetadata')
|
||||
.modify('specificOrFavourite', filter.custom_view_id)
|
||||
.first();
|
||||
|
||||
const listingBuilder = new DynamicListingBuilder();
|
||||
const errorReasons = [];
|
||||
|
||||
listingBuilder.addModelClass(SaleInvoice);
|
||||
listingBuilder.addCustomViewId(filter.custom_view_id);
|
||||
listingBuilder.addFilterRoles(filter.filter_roles);
|
||||
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
|
||||
listingBuilder.addView(viewMeta);
|
||||
|
||||
const dynamicListing = new DynamicListing(dynamicListingBuilder);
|
||||
|
||||
if (dynamicListing instanceof Error) {
|
||||
const errors = dynamicListingErrorsToResponse(dynamicListing);
|
||||
errorReasons.push(...errors);
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const salesInvoices = await SaleInvoice.query().onBuild((builder) => {
|
||||
dynamicListing.buildQuery()(builder);
|
||||
/**
|
||||
* Validate whether sale invoice customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateInvoiceCustomerExistance(req, res, next) {
|
||||
const saleInvoice = { ...req.body };
|
||||
const isCustomerIDExists = await CustomersService.isCustomerExists(
|
||||
saleInvoice.customer_id
|
||||
);
|
||||
if (!isCustomerIDExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
sales_invoices: salesInvoices,
|
||||
/**
|
||||
* Validate whether sale invoice items ids esits on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateInvoiceItemsIdsExistance(req, res, next) {
|
||||
const saleInvoice = { ...req.body };
|
||||
const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id);
|
||||
const isItemsIdsExists = await ItemsService.isItemsIdsExists(
|
||||
entriesItemsIds
|
||||
);
|
||||
if (isItemsIdsExists.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice number unqiue on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateInvoiceNumberUnique(req, res, next) {
|
||||
const saleInvoice = { ...req.body };
|
||||
const isInvoiceNoExists = await SaleInvoiceService.isSaleInvoiceNumberExists(
|
||||
saleInvoice.invoice_no,
|
||||
req.params.id
|
||||
);
|
||||
if (isInvoiceNoExists) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({
|
||||
errors: [{ type: 'SALE.INVOICE.NUMBER.IS.EXISTS', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale invoice exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateInvoiceExistance(req, res, next) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const isSaleInvoiceExists = await SaleInvoiceService.isSaleInvoiceExists(
|
||||
saleInvoiceId
|
||||
);
|
||||
if (!isSaleInvoiceExists) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ errors: [{ type: 'SALE.INVOICE.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 saleInvoice = { ...req.body };
|
||||
const entriesItemsIds = saleInvoice.entries.map((e) => e.item_id);
|
||||
|
||||
const isItemsIdsExists = await ItemsService.isItemsIdsExists(
|
||||
entriesItemsIds
|
||||
);
|
||||
if (isItemsIdsExists.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the sale estimate entries IDs exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateEntriesIdsExistance(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 storedEntries = await ItemEntry.tenant().query()
|
||||
.whereIn('reference_id', [saleInvoiceId])
|
||||
.whereIn('reference_type', ['SaleInvoice']);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(
|
||||
entriesIds,
|
||||
storedEntriesIds,
|
||||
);
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SALE.INVOICE.ENTRIES.IDS.NOT.FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async newSaleInvoice(req, res) {
|
||||
const errorReasons = [];
|
||||
const saleInvoice = {
|
||||
...req.body,
|
||||
entries: req.body.entries.map((entry) => ({
|
||||
...entry,
|
||||
amount: ItemEntry.calcAmount(entry),
|
||||
})),
|
||||
};
|
||||
// Creates a new sale invoice with associated entries.
|
||||
const storedSaleInvoice = await SaleInvoiceService.createSaleInvoice(
|
||||
saleInvoice
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleInvoice.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit sale invoice details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async editSaleInvoice(req, res) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoice = { ...req.body };
|
||||
|
||||
// Update the given sale invoice details.
|
||||
await SaleInvoiceService.editSaleInvoice(saleInvoiceId, saleInvoice);
|
||||
|
||||
return res.status(200).send({ id: saleInvoice.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the sale invoice with associated entries and journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async deleteSaleInvoice(req, res) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
// Deletes the sale invoice with associated entries and journal transaction.
|
||||
await SaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
|
||||
|
||||
return res.status(200).send({ id: saleInvoiceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sale invoice with associated entries.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async getSaleInvoice(req, res) {
|
||||
const { id: saleInvoiceId } = req.params;
|
||||
const saleInvoice = await SaleInvoiceService.getSaleInvoiceWithEntries(
|
||||
saleInvoiceId
|
||||
);
|
||||
return res.status(200).send({ sale_invoice: saleInvoice });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve paginated sales invoices with custom view metadata.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async getSalesInvoices(req, res) {
|
||||
const filter = {
|
||||
filter_roles: [],
|
||||
sort_order: 'asc',
|
||||
...req.query,
|
||||
};
|
||||
if (filter.stringified_filter_roles) {
|
||||
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
||||
}
|
||||
const { SaleInvoice, View, Resource } = req.models;
|
||||
const resource = await Resource.query()
|
||||
.remember()
|
||||
.where('name', 'sales_invoices')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
if (!resource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SALES_INVOICES_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.addModelClass(SaleInvoice);
|
||||
listingBuilder.addCustomViewId(filter.custom_view_id);
|
||||
listingBuilder.addFilterRoles(filter.filter_roles);
|
||||
listingBuilder.addSortBy(filter.sort_by, filter.sort_order);
|
||||
listingBuilder.addView(viewMeta);
|
||||
|
||||
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 salesInvoices = await SaleInvoice.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
dynamicListing.buildQuery()(builder);
|
||||
}).pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
sales_invoices: {
|
||||
...salesInvoices,
|
||||
...(viewMeta
|
||||
? {
|
||||
view_meta: {
|
||||
customViewId: viewMeta.id,
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { ItemEntry } from '@/models';
|
||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import CustomersService from '@/services/Customers/CustomersService';
|
||||
@@ -12,45 +13,57 @@ import {
|
||||
dynamicListingErrorsToResponse
|
||||
} from '@/services/DynamicListing/HasDynamicListing';
|
||||
|
||||
export default {
|
||||
export default class SalesReceiptsController {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/:id',
|
||||
this.editSaleReceipt.validation,
|
||||
'/:id', [
|
||||
...this.specificReceiptValidationSchema,
|
||||
...this.salesReceiptsValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.editSaleReceipt.handler)
|
||||
asyncMiddleware(this.validateSaleReceiptExistance),
|
||||
asyncMiddleware(this.validateReceiptCustomerExistance),
|
||||
asyncMiddleware(this.validateReceiptDepositAccountExistance),
|
||||
asyncMiddleware(this.validateReceiptItemsIdsExistance),
|
||||
asyncMiddleware(this.validateReceiptEntriesIds),
|
||||
asyncMiddleware(this.editSaleReceipt)
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
this.newSaleReceipt.validation,
|
||||
this.salesReceiptsValidationSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.newSaleReceipt.handler)
|
||||
asyncMiddleware(this.validateReceiptCustomerExistance),
|
||||
asyncMiddleware(this.validateReceiptDepositAccountExistance),
|
||||
asyncMiddleware(this.validateReceiptItemsIdsExistance),
|
||||
asyncMiddleware(this.newSaleReceipt)
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteSaleReceipt.handler,
|
||||
this.specificReceiptValidationSchema,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.deleteSaleReceipt.handler)
|
||||
asyncMiddleware(this.validateSaleReceiptExistance),
|
||||
asyncMiddleware(this.deleteSaleReceipt)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
this.listingSalesReceipts.validation,
|
||||
this.listingSalesReceipts,
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.listingSalesReceipts.handler)
|
||||
asyncMiddleware(this.listingSalesReceipts)
|
||||
);
|
||||
return router;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new receipt.
|
||||
* Sales receipt validation schema.
|
||||
* @return {Array}
|
||||
*/
|
||||
newSaleReceipt: {
|
||||
validation: [
|
||||
static get salesReceiptsValidationSchema() {
|
||||
return [
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('receipt_date').exists().isISO8601(),
|
||||
@@ -58,92 +71,9 @@ export default {
|
||||
check('reference_no').optional().trim().escape(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.description').optional().trim().escape(),
|
||||
check('entries.*.quantity').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toInt(),
|
||||
check('entries.*.discount').optional().isNumeric().toInt(),
|
||||
|
||||
|
||||
check('receipt_message').optional().trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const saleReceipt = { ...req.body };
|
||||
|
||||
const isCustomerExists = await CustomersService.isCustomerExists(
|
||||
saleReceipt.customer_id
|
||||
);
|
||||
const isDepositAccountExists = await AccountsService.isAccountExists(
|
||||
saleReceipt.deposit_account_id
|
||||
);
|
||||
const errorReasons = [];
|
||||
|
||||
if (!isCustomerExists) {
|
||||
errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 });
|
||||
}
|
||||
if (!isDepositAccountExists) {
|
||||
errorReasons.push({ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 });
|
||||
}
|
||||
// Validate items ids in estimate entries exists.
|
||||
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(
|
||||
estimateItemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Store the given sale receipt details with associated entries.
|
||||
const storedSaleReceipt = await SaleReceiptService.createSaleReceipt(
|
||||
saleReceipt
|
||||
);
|
||||
|
||||
return res.status(200).send({ id: storedSaleReceipt.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the sale receipt with associated entries and journal transactions.
|
||||
*/
|
||||
deleteSaleReceipt: {
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const isSaleReceiptExists = await SaleReceiptService.isSaleReceiptExists(
|
||||
saleReceiptId
|
||||
);
|
||||
|
||||
if (!isSaleReceiptExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
// Deletes the sale receipt.
|
||||
await SaleReceiptService.deleteSaleReceipt(saleReceiptId);
|
||||
|
||||
return res.status(200).send({ id: saleReceiptId });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the sale receipt details with associated entries and re-write
|
||||
* journal transaction on the same date.
|
||||
*/
|
||||
editSaleReceipt: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('receipt_date').exists().isISO8601(),
|
||||
check('send_to_email').optional().isEmail(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
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(),
|
||||
@@ -152,125 +82,244 @@ export default {
|
||||
|
||||
check('receipt_message').optional().trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const saleReceipt = { ...req.body };
|
||||
|
||||
const isSaleReceiptExists = await SaleReceiptService.isSaleReceiptExists(
|
||||
saleReceiptId
|
||||
);
|
||||
if (!isSaleReceiptExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
const isCustomerExists = await CustomersService.isCustomerExists(
|
||||
saleReceipt.customer_id
|
||||
);
|
||||
const isDepositAccountExists = await AccountsService.isAccountsExists(
|
||||
saleReceipt.deposit_account_id
|
||||
);
|
||||
const errorReasons = [];
|
||||
|
||||
if (!isCustomerExists) {
|
||||
errorReasons.push({ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 });
|
||||
}
|
||||
if (!isDepositAccountExists) {
|
||||
errorReasons.push({ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 });
|
||||
}
|
||||
// Validate items ids in estimate entries exists.
|
||||
const entriesItemsIDs = saleReceipt.entries.map((e) => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(
|
||||
entriesItemsIDs
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
errorReasons.push({ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 });
|
||||
}
|
||||
// Validate the entries IDs that not stored or associated to the sale receipt.
|
||||
const notExistsEntriesIds = await SaleReceiptService.isSaleReceiptEntriesIDsExists(
|
||||
saleReceiptId,
|
||||
saleReceipt
|
||||
);
|
||||
if (notExistsEntriesIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'ENTRIES.IDS.NOT.FOUND',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
// Handle all errors with reasons messages.
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
// Update the given sale receipt details.
|
||||
await SaleReceiptService.editSaleReceipt(saleReceiptId, saleReceipt);
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing sales receipts.
|
||||
* Specific sale receipt validation schema.
|
||||
*/
|
||||
listingSalesReceipts: {
|
||||
validation: [
|
||||
static get specificReceiptValidationSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* List sales receipts validation schema.
|
||||
*/
|
||||
static get listSalesReceiptsValidationSchema() {
|
||||
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']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const filter = {
|
||||
filter_roles: [],
|
||||
sort_order: 'asc',
|
||||
};
|
||||
if (filter.stringified_filter_roles) {
|
||||
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
||||
}
|
||||
const { SaleReceipt, Resource, View } = req.models;
|
||||
const resource = await Resource.tenant().query()
|
||||
.remember()
|
||||
.where('name', 'sales_receipts')
|
||||
.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(SaleReceipt);
|
||||
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);
|
||||
}
|
||||
const salesReceipts = await SaleReceipt.query().onBuild((builder) => {
|
||||
dynamicListing.buildQuery()(builder);
|
||||
return builder;
|
||||
/**
|
||||
* Validate whether sale receipt exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async validateSaleReceiptExistance(req, res, next) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const isSaleReceiptExists = await SaleReceiptService.isSaleReceiptExists(
|
||||
saleReceiptId
|
||||
);
|
||||
if (!isSaleReceiptExists) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'SALE.RECEIPT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
sales_receipts: salesReceipts,
|
||||
...(viewMeta ? {
|
||||
customViewId: viewMeta.id,
|
||||
} : {}),
|
||||
/**
|
||||
* Validate whether sale receipt customer exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateReceiptCustomerExistance(req, res, next) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const isCustomerExists = await CustomersService.isCustomerExists(
|
||||
saleReceipt.customer_id
|
||||
);
|
||||
if (!isCustomerExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'CUSTOMER.ID.NOT.EXISTS', code: 200 }],
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether sale receipt deposit account exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateReceiptDepositAccountExistance(req, res, next) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const isDepositAccountExists = await AccountsService.isAccountExists(
|
||||
saleReceipt.deposit_account_id
|
||||
);
|
||||
if (!isDepositAccountExists) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DEPOSIT.ACCOUNT.NOT.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether receipt items ids exist on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateReceiptItemsIdsExistance(req, res, next) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const estimateItemsIds = saleReceipt.entries.map((e) => e.item_id);
|
||||
const notFoundItemsIds = await ItemsService.isItemsIdsExists(
|
||||
estimateItemsIds
|
||||
);
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{ type: 'ITEMS.IDS.NOT.EXISTS', code: 400 }] });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate receipt entries ids existance on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static async validateReceiptEntriesIds(req, res, next) {
|
||||
const saleReceipt = { ...req.body };
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
// Validate the entries IDs that not stored or associated to the sale receipt.
|
||||
const notExistsEntriesIds = await SaleReceiptService.isSaleReceiptEntriesIDsExists(
|
||||
saleReceiptId,
|
||||
saleReceipt
|
||||
);
|
||||
if (notExistsEntriesIds.length > 0) {
|
||||
return res.status(400).send({ errors: [{
|
||||
type: 'ENTRIES.IDS.NOT.FOUND',
|
||||
code: 500,
|
||||
}]
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new receipt.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async newSaleReceipt(req, res) {
|
||||
const saleReceipt = {
|
||||
...req.body,
|
||||
entries: req.body.entries.map((entry) => ({
|
||||
...entry,
|
||||
amount: ItemEntry.calcAmount(entry),
|
||||
})),
|
||||
};
|
||||
|
||||
// Store the given sale receipt details with associated entries.
|
||||
const storedSaleReceipt = await SaleReceiptService.createSaleReceipt(
|
||||
saleReceipt
|
||||
);
|
||||
return res.status(200).send({ id: storedSaleReceipt.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the sale receipt with associated entries and journal transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async deleteSaleReceipt(req, res) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
|
||||
// Deletes the sale receipt.
|
||||
await SaleReceiptService.deleteSaleReceipt(saleReceiptId);
|
||||
|
||||
return res.status(200).send({ id: saleReceiptId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the sale receipt details with associated entries and re-write
|
||||
* journal transaction on the same date.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async editSaleReceipt(req, res) {
|
||||
const { id: saleReceiptId } = req.params;
|
||||
const saleReceipt = { ...req.body };
|
||||
const errorReasons = [];
|
||||
|
||||
// Handle all errors with reasons messages.
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
// Update the given sale receipt details.
|
||||
await SaleReceiptService.editSaleReceipt(saleReceiptId, saleReceipt);
|
||||
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing sales receipts.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
static async listingSalesReceipts(req, res) {
|
||||
const filter = {
|
||||
filter_roles: [],
|
||||
sort_order: 'asc',
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
};
|
||||
if (filter.stringified_filter_roles) {
|
||||
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
||||
}
|
||||
const { SaleReceipt, Resource, View } = req.models;
|
||||
const resource = await Resource.tenant().query()
|
||||
.remember()
|
||||
.where('name', 'sales_receipts')
|
||||
.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(SaleReceipt);
|
||||
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);
|
||||
}
|
||||
const salesReceipts = await SaleReceipt.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('entries');
|
||||
dynamicListing.buildQuery()(builder);
|
||||
return builder;
|
||||
}).pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
sales_receipts: salesReceipts,
|
||||
...(viewMeta ? {
|
||||
customViewId: viewMeta.id,
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
13
server/src/interfaces/BillPayment.ts
Normal file
13
server/src/interfaces/BillPayment.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
export interface IBillPaymentEntry {
|
||||
billId: number,
|
||||
paymentAmount: number,
|
||||
};
|
||||
|
||||
export interface IBillPayment {
|
||||
amount: number,
|
||||
reference: string,
|
||||
billNo: string,
|
||||
entries: IBillPaymentEntry[],
|
||||
}
|
||||
27
server/src/lib/QueryBuilderBulkOperations/QueryBuilder.js
Normal file
27
server/src/lib/QueryBuilderBulkOperations/QueryBuilder.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { QueryBuilder } from "knex"
|
||||
import { QueryBuilder } from 'objection';
|
||||
|
||||
export default class BulkOperationsQueryBuilder extends QueryBuilder {
|
||||
|
||||
bulkInsert(collection) {
|
||||
const opers = [];
|
||||
|
||||
collection.forEach((dataset) => {
|
||||
const insertOper = this.insert({ ...dataset });
|
||||
opers.push(insertOper);
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
bulkDelete(rowsIds) {
|
||||
|
||||
}
|
||||
|
||||
bulkUpdate(dataset, whereColumn) {
|
||||
|
||||
}
|
||||
|
||||
bulkPatch(newDataset, oldDataset) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@ import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
|
||||
|
||||
export default class Bill extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['dueAmount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -21,9 +28,9 @@ export default class Bill extends mixin(TenantModel, [CachableModel]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
* Due amount of the given.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
get dueAmount() {
|
||||
return Math.max(this.balance - this.paymentAmount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mixin } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
@@ -25,4 +25,33 @@ export default class BillPayment extends mixin(TenantModel, [CachableModel]) {
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
static changePaymentAmount(billId, amount) {
|
||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||
return this.tenant()
|
||||
.query()
|
||||
.where('id', billId)
|
||||
[changeMethod]('payment_amount', Math.abs(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const BillPaymentEntry = require('@/models/BillPaymentEntry');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Account model may belongs to account type.
|
||||
*/
|
||||
entries: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: this.relationBindKnex(BillPaymentEntry.default),
|
||||
join: {
|
||||
from: 'bills_payments.id',
|
||||
to: 'bills_payments_entries.billPaymentId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
18
server/src/models/BillPaymentEntry.js
Normal file
18
server/src/models/BillPaymentEntry.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { mixin } 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 ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,42 @@ export default class Customer extends TenantModel {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change vendor balance.
|
||||
* @param {Integer} customerId
|
||||
* @param {Numeric} amount
|
||||
*/
|
||||
static async changeBalance(customerId, amount) {
|
||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||
|
||||
await this.tenant()
|
||||
.query()
|
||||
.where('id', customerId)
|
||||
[changeMethod]('balance', Math.abs(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the given customer balance.
|
||||
* @param {Integer} customerId
|
||||
* @param {Integer} amount
|
||||
*/
|
||||
static async incrementBalance(customerId, amount) {
|
||||
await this.tenant()
|
||||
.query()
|
||||
.where('id', customerId)
|
||||
.increment('balance', amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the given customer balance.
|
||||
* @param {integer} customerId -
|
||||
* @param {integer} amount -
|
||||
*/
|
||||
static async decrementBalance(customerId, amount) {
|
||||
await this.tenant()
|
||||
.query()
|
||||
.where('id', customerId)
|
||||
.decrement('balance', amount);
|
||||
}
|
||||
}
|
||||
|
||||
18
server/src/models/InventoryTransaction.js
Normal file
18
server/src/models/InventoryTransaction.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
|
||||
export default class InventoryTransaction extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'inventory_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
35
server/src/models/ItemEntry.js
Normal file
35
server/src/models/ItemEntry.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'path';
|
||||
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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
return {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
static calcAmount(itemEntry) {
|
||||
const { discount, quantity, rate } = itemEntry;
|
||||
const total = quantity * rate;
|
||||
|
||||
return discount ? total - (total * discount * 0.01) : total;
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,6 @@ export default class PaymentReceive extends mixin(TenantModel, [CachableModel])
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
@@ -38,7 +31,7 @@ export default class PaymentReceive extends mixin(TenantModel, [CachableModel])
|
||||
modelClass: this.relationBindKnex(PaymentReceiveEntry.default),
|
||||
join: {
|
||||
from: 'payment_receives.id',
|
||||
to: 'payment_receives_entries.payment_receive_id',
|
||||
to: 'payment_receives_entries.paymentReceiveId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import moment from 'moment';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
|
||||
|
||||
export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Table name
|
||||
@@ -20,26 +18,19 @@ export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const SaleEstimateEntry = require('@/models/SaleEstimateEntry');
|
||||
const ItemEntry = require('@/models/ItemEntry');
|
||||
|
||||
return {
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: this.relationBindKnex(SaleEstimateEntry.default),
|
||||
modelClass: this.relationBindKnex(ItemEntry.default),
|
||||
join: {
|
||||
from: 'sales_estimates.id',
|
||||
to: 'sales_estimate_entries.id',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,13 @@ import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
|
||||
export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['dueAmount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -20,27 +27,41 @@ export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
* Due amount of the given.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
get dueAmount() {
|
||||
return Math.max(this.balance - this.paymentAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const SaleInvoiceEntry = require('@/models/SaleInvoiceEntry');
|
||||
const ItemEntry = require('@/models/ItemEntry');
|
||||
|
||||
return {
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: this.relationBindKnex(SaleInvoiceEntry.default),
|
||||
modelClass: this.relationBindKnex(ItemEntry.default),
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'sales_invoices_entries.sale_invoice_id',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Change payment amount.
|
||||
* @param {Integer} invoiceId
|
||||
* @param {Numeric} amount
|
||||
*/
|
||||
static async changePaymentAmount(invoiceId, amount) {
|
||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||
|
||||
await this.tenant()
|
||||
.query()
|
||||
.where('id', invoiceId)
|
||||
[changeMethod]('payment_amount', Math.abs(amount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,26 +19,19 @@ export default class SaleReceipt extends mixin(TenantModel, [CachableModel]) {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const SaleReceiptEntry = require('@/models/SaleReceiptEntry');
|
||||
const ItemEntry = require('@/models/ItemEntry');
|
||||
|
||||
return {
|
||||
entries: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: this.relationBindKnex(SaleReceiptEntry.default),
|
||||
modelClass: this.relationBindKnex(ItemEntry.default),
|
||||
join: {
|
||||
from: 'sales_receipts.id',
|
||||
to: 'sales_receipt_entries.sale_receipt_id',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,4 +15,41 @@ export default class Vendor extends TenantModel {
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the vendor balance.
|
||||
* @param {Integer} customerId
|
||||
* @param {Number} amount
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async changeBalance(vendorId, amount) {
|
||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||
|
||||
return this.tenant()
|
||||
.query()
|
||||
.where('id', vendorId)
|
||||
[changeMethod]('balance', Math.abs(amount));
|
||||
}
|
||||
|
||||
static changeDiffBalance(vendorId, oldVendorId, amount, oldAmount) {
|
||||
const diffAmount = (amount - oldAmount) * -1;
|
||||
const asyncOpers = [];
|
||||
|
||||
if (vendorId != oldVendorId) {
|
||||
const oldVendorOper = Vendor.changeBalance(
|
||||
oldVendorId,
|
||||
oldAmount
|
||||
);
|
||||
const vendorOper = Vendor.changeBalance(
|
||||
vendorId,
|
||||
(amount + diffAmount) * -1
|
||||
);
|
||||
asyncOpers.push(vendorOper);
|
||||
asyncOpers.push(oldVendorOper);
|
||||
} else {
|
||||
const balanceChangeOper = Vendor.changeBalance(vendorId, diffAmount);
|
||||
asyncOpers.push(balanceChangeOper);
|
||||
}
|
||||
return Promise.all(asyncOpers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,12 @@ import PaymentReceive from './PaymentReceive';
|
||||
import PaymentReceiveEntry from './PaymentReceiveEntry';
|
||||
import Bill from './Bill';
|
||||
import BillPayment from './BillPayment';
|
||||
import BillPaymentEntry from './BillPaymentEntry';
|
||||
import Resource from './Resource';
|
||||
import View from './View';
|
||||
import ItemEntry from './ItemEntry';
|
||||
import InventoryTransaction from './InventoryTransaction';
|
||||
import AccountType from './AccountType';
|
||||
|
||||
export {
|
||||
Customer,
|
||||
@@ -32,6 +36,10 @@ export {
|
||||
PaymentReceiveEntry,
|
||||
Bill,
|
||||
BillPayment,
|
||||
BillPaymentEntry,
|
||||
Resource,
|
||||
View,
|
||||
ItemEntry,
|
||||
InventoryTransaction,
|
||||
AccountType,
|
||||
};
|
||||
26
server/src/repositories/CustomerRepository.js
Normal file
26
server/src/repositories/CustomerRepository.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Customer } from '@/models';
|
||||
|
||||
export default class CustomerRepository {
|
||||
|
||||
static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) {
|
||||
const diffAmount = (amount - oldAmount) * -1;
|
||||
const asyncOpers = [];
|
||||
|
||||
if (customerId != oldCustomerId) {
|
||||
const oldCustomerOper = Customer.changeBalance(
|
||||
oldCustomerId,
|
||||
oldAmount
|
||||
);
|
||||
const customerOper = Customer.changeBalance(
|
||||
customerId,
|
||||
(amount + diffAmount) * -1
|
||||
);
|
||||
asyncOpers.push(customerOper);
|
||||
asyncOpers.push(oldCustomerOper);
|
||||
} else {
|
||||
const balanceChangeOper = Customer.changeBalance(customerId, diffAmount);
|
||||
asyncOpers.push(balanceChangeOper);
|
||||
}
|
||||
return Promise.all(asyncOpers);
|
||||
}
|
||||
}
|
||||
0
server/src/repositories/ItemEntryRepository.js
Normal file
0
server/src/repositories/ItemEntryRepository.js
Normal file
55
server/src/repositories/PaymentReceiveEntryRepository.js
Normal file
55
server/src/repositories/PaymentReceiveEntryRepository.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { omit } from 'lodash';
|
||||
import BaseModelRepository from '@/repositories/BaseModelRepository';
|
||||
import { PaymentReceiveEntry } from '@/models';
|
||||
|
||||
export default class PaymentReceiveEntryRepository extends BaseModelRepository {
|
||||
/**
|
||||
* Insert payment receive entries in bulk.
|
||||
* @param {Array} entries
|
||||
* @param {Integr} paymentReceiveId
|
||||
* @return {Promise}
|
||||
*/
|
||||
static insertBulk(entries, paymentReceiveId) {
|
||||
const opers = [];
|
||||
entries.forEach((entry) => {
|
||||
const insertOper = PaymentReceiveEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
payment_receive_id: paymentReceiveId,
|
||||
...entry,
|
||||
});
|
||||
opers.push(insertOper);
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment receive entries in bulk.
|
||||
* @param {Array} entries
|
||||
* @return {Promise}
|
||||
*/
|
||||
static updateBulk(entries) {
|
||||
const opers = [];
|
||||
entries.forEach((entry) => {
|
||||
const updateOper = PaymentReceiveEntry.tenant()
|
||||
.query()
|
||||
.patchAndFetchById(entry.id, {
|
||||
...omit(entry, ['id', 'index']),
|
||||
});
|
||||
opers.push(updateOper);
|
||||
});
|
||||
return Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given payment receive entries ids in bulk.
|
||||
* @param {Array} entriesIds
|
||||
* @return {Promise}
|
||||
*/
|
||||
static deleteBulk(entriesIds) {
|
||||
return PaymentReceiveEntry.tenant()
|
||||
.query()
|
||||
.whereIn('id', entriesIds)
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
7
server/src/repositories/PaymentReceiveRepository.js
Normal file
7
server/src/repositories/PaymentReceiveRepository.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { PaymentReceiveEntry } from '@/models';
|
||||
import BaseModelRepository from '@/repositories/BaseModelRepository';
|
||||
|
||||
export default class PaymentReceiveRepository extends BaseModelRepository {
|
||||
|
||||
}
|
||||
@@ -1,9 +1,24 @@
|
||||
import { Account } from '@/models';
|
||||
import { Account, AccountType } from '@/models';
|
||||
|
||||
export default class AccountsService {
|
||||
|
||||
static async isAccountExists(accountId) {
|
||||
const foundAccounts = await Account.tenant().query().where('id', accountId);
|
||||
return foundAccounts.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async getAccountByType(accountTypeKey) {
|
||||
const accountType = await AccountType.tenant()
|
||||
.query()
|
||||
.where('key', accountTypeKey)
|
||||
.first();
|
||||
|
||||
const account = await Account.tenant()
|
||||
.query()
|
||||
.where('account_type_id', accountType.id)
|
||||
.first();
|
||||
|
||||
console.log(account);
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
16
server/src/services/Inventory/Inventory.js
Normal file
16
server/src/services/Inventory/Inventory.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { InventoryTransaction } from "../../models";
|
||||
|
||||
|
||||
export default class InventoryService {
|
||||
|
||||
async isInventoryPurchaseSold(transactionType, transactionId) {
|
||||
|
||||
}
|
||||
|
||||
static deleteTransactions(transactionId, transactionType) {
|
||||
return InventoryTransaction.tenant().query()
|
||||
.where('transaction_type', transactionType)
|
||||
.where('transaction_id', transactionId)
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,244 @@
|
||||
import { omit } from "lodash";
|
||||
import { BillPayment } from '@/models';
|
||||
|
||||
export default class BillPaymentsService {
|
||||
import express from 'express';
|
||||
import { omit } from 'lodash';
|
||||
import { check, query, validationResult, param } from 'express-validator';
|
||||
import { BillPayment, BillPaymentEntry, Vendor } from '@/models';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import ServiceItemsEntries from '../Sales/ServiceItemsEntries';
|
||||
import AccountsService from '../Accounts/AccountsService';
|
||||
import JournalPoster from '../Accounting/JournalPoster';
|
||||
import JournalEntry from '../Accounting/JournalEntry';
|
||||
|
||||
export default class BillPaymentsService {
|
||||
/**
|
||||
* Creates a new bill payment transcations and store it to the storage
|
||||
* with associated bills entries and journal transactions.
|
||||
*
|
||||
* Precedures
|
||||
* ------
|
||||
* - Records the bill payment transaction.
|
||||
* - Records the bill payment associated entries.
|
||||
* - Increment the payment amount of the given vendor bills.
|
||||
* - Decrement the vendor balance.
|
||||
* - Records payment journal entries.
|
||||
*
|
||||
* @param {IBillPayment} billPayment
|
||||
*/
|
||||
static async createBillPayment(billPayment) {
|
||||
const storedBillPayment = await BillPayment.tenant().query().insert({
|
||||
...omit(billPayment, ['entries']),
|
||||
const amount = sumBy(billPayment.entries, 'paymentAmount');
|
||||
const storedBillPayment = await BillPayment.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
amount,
|
||||
...omit(billPayment, ['entries']),
|
||||
});
|
||||
const storeOpers = [];
|
||||
|
||||
billPayment.entries.forEach((entry) => {
|
||||
const oper = BillPaymentEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
bill_payment_id: storedBillPayment.id,
|
||||
...entry,
|
||||
});
|
||||
// Increment the bill payment amount.
|
||||
const billOper = BillPayment.changePaymentAmount(
|
||||
entry.billId,
|
||||
entry.paymentAmount
|
||||
);
|
||||
storeOpers.push(billOper);
|
||||
storeOpers.push(oper);
|
||||
});
|
||||
|
||||
// Decrement the vendor balance after bills payments.
|
||||
const vendorDecrementOper = Vendor.changeBalanace(
|
||||
billPayment.vendor_id,
|
||||
amount * -1
|
||||
);
|
||||
// Records the journal transactions after bills payment
|
||||
// and change diff acoount balance.
|
||||
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries({
|
||||
id: storedBillPayment.id,
|
||||
...billPayment,
|
||||
});
|
||||
await Promise.all([
|
||||
...storeOpers,
|
||||
recordJournalTransaction,
|
||||
vendorDecrementOper,
|
||||
]);
|
||||
return storedBillPayment;
|
||||
}
|
||||
|
||||
editBillPayment(billPaymentId, billPayment) {
|
||||
/**
|
||||
* Edits the details of the given bill payment.
|
||||
*
|
||||
* Preceducres.
|
||||
* -------
|
||||
* - Update the bill payment transaction.
|
||||
* - Insert the new bill payment entries that have no ids.
|
||||
* - Update the bill paymeny entries that have ids.
|
||||
* - Delete the bill payment entries that not presented.
|
||||
* - Re-insert the journal transactions and update the diff accounts balance.
|
||||
* - Update the diff vendor balance.
|
||||
* - Update the diff bill payment amount.
|
||||
*
|
||||
* @param {Integer} billPaymentId
|
||||
* @param {IBillPayment} billPayment
|
||||
* @param {IBillPayment} oldBillPayment
|
||||
*/
|
||||
static async editBillPayment(billPaymentId, billPayment, oldBillPayment) {
|
||||
const amount = sumBy(bilPayment.entries, 'payment_amount');
|
||||
const updateBillPayment = await BillPayment.tenant()
|
||||
.query()
|
||||
.where('id', billPaymentId)
|
||||
.update({
|
||||
amount,
|
||||
...omit(billPayment, ['entries']),
|
||||
});
|
||||
const opers = [];
|
||||
const entriesHasIds = billpayment.entries.filter((i) => i.id);
|
||||
const entriesHasNoIds = billPayment.entries.filter((e) => !e.id);
|
||||
|
||||
const entriesIds = entriesHasIds.map((e) => e.id);
|
||||
|
||||
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted(
|
||||
oldBillPayment.entries,
|
||||
entriesHasIds
|
||||
);
|
||||
if (entriesIdsShouldDelete.length > 0) {
|
||||
const deleteOper = BillPaymentEntry.tenant()
|
||||
.query()
|
||||
.bulkDelete(entriesIdsShouldDelete);
|
||||
opers.push(deleteOper);
|
||||
}
|
||||
// Entries that should be update to the storage.
|
||||
if (entriesHasIds.length > 0) {
|
||||
const updateOper = BillPaymentEntry.tenant()
|
||||
.query()
|
||||
.bulkUpdate(entriesHasIds, { where: 'id' });
|
||||
opers.push(updateOper);
|
||||
}
|
||||
// Entries that should be inserted to the storage.
|
||||
if (entriesHasNoIds.length > 0) {
|
||||
const insertOper = BillPaymentEntry.tenant()
|
||||
.query()
|
||||
.bulkInsert(
|
||||
entriesHasNoIds.map((e) => ({ ...e, bill_payment_id: billPaymentId }))
|
||||
);
|
||||
opers.push(insertOper);
|
||||
}
|
||||
// Records the journal transactions after bills payment and change
|
||||
// different acoount balance.
|
||||
const recordJournalTransaction = this.recordPaymentReceiveJournalEntries({
|
||||
id: storedBillPayment.id,
|
||||
...billPayment,
|
||||
});
|
||||
// Change the different vendor balance between the new and old one.
|
||||
const changeDiffBalance = Vendor.changeDiffBalance(
|
||||
billPayment.vendor_id,
|
||||
oldBillPayment.vendor_id,
|
||||
billPayment.amount,
|
||||
oldBillPayment.amount
|
||||
);
|
||||
await Promise.all([
|
||||
...opers,
|
||||
recordJournalTransaction,
|
||||
changeDiffBalance,
|
||||
]);
|
||||
}
|
||||
|
||||
static async isBillPaymentExists(billPaymentId) {
|
||||
const foundBillPayments = await BillPayment.tenant().query().where('id', billPaymentId);
|
||||
return foundBillPayments.lengh > 0;
|
||||
/**
|
||||
* Deletes the bill payment and associated transactions.
|
||||
* @param {Integer} billPaymentId -
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async deleteBillPayment(billPaymentId) {
|
||||
const billPayment = await BillPayment.tenant().query().where('id', billPaymentId).first();
|
||||
|
||||
await BillPayment.tenant().query().where('id', billPaymentId).delete();
|
||||
await BillPaymentEntry.tenant()
|
||||
.query()
|
||||
.where('bill_payment_id', billPaymentId)
|
||||
.delete();
|
||||
|
||||
const deleteTransactionsOper = this.deleteJournalTransactions(
|
||||
billPaymentId,
|
||||
'BillPayment'
|
||||
);
|
||||
const revertVendorBalance = Vendor.changeBalanace(
|
||||
billpayment.vendor_id,
|
||||
billPayment.amount * -1,
|
||||
);
|
||||
return Promise.all([
|
||||
deleteTransactionsOper,
|
||||
revertVendorBalance,
|
||||
]);
|
||||
}
|
||||
|
||||
static async isBillPaymentNumberExists(billPaymentNumber) {
|
||||
const foundPayments = await Bill.tenant().query().where('bill_payment_number', billPaymentNumber);
|
||||
return foundPayments.length > 0;
|
||||
/**
|
||||
* Records bill payment receive journal transactions.
|
||||
* @param {BillPayment} billPayment
|
||||
* @param {Integer} billPaymentId
|
||||
*/
|
||||
static async recordPaymentReceiveJournalEntries(billPayment) {
|
||||
const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
|
||||
const formattedDate = moment(billPayment.payment_date).format('YYYY-MM-DD');
|
||||
const payableAccount = await AccountsService.getAccountByType(
|
||||
'accounts_payable'
|
||||
);
|
||||
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
const commonJournal = {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
referenceId: billPayment.id,
|
||||
referenceType: 'BillPayment',
|
||||
date: formattedDate,
|
||||
};
|
||||
if (billPayment.id) {
|
||||
const transactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', ['BillPayment'])
|
||||
.where('reference_id', billPayment.id)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.removeEntries();
|
||||
}
|
||||
const debitReceivable = new JournalEntry({
|
||||
...commonJournal,
|
||||
debit: paymentAmount,
|
||||
contactType: 'Vendor',
|
||||
contactId: billpayment.vendor_id,
|
||||
account: payableAccount.id,
|
||||
});
|
||||
const creditPaymentAccount = new JournalEntry({
|
||||
...commonJournal,
|
||||
credit: paymentAmount,
|
||||
account: billPayment.payment_account_id,
|
||||
});
|
||||
journal.debit(debitReceivable);
|
||||
journal.credit(creditPaymentAccount);
|
||||
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveEntries(),
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
}
|
||||
|
||||
isBillPaymentsExist(billPaymentIds) {
|
||||
static async getBillPayment(billPaymentId) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the bill payment exists on the storage.
|
||||
* @param {Integer} billPaymentId
|
||||
*/
|
||||
static async isBillPaymentExists(billPaymentId) {
|
||||
const billPayment = await BillPayment.tenant().query()
|
||||
.where('id', billPaymentId)
|
||||
.first();
|
||||
return billPayment.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,316 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Bill, BillPayment } from '@/models';
|
||||
import { Item } from '@/models';
|
||||
import { Account } from '../../models';
|
||||
import JournalPoster from '../Accounting/JournalPoster';
|
||||
import { omit, sumBy, difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
Bill,
|
||||
Vendor,
|
||||
InventoryTransaction,
|
||||
ItemEntry,
|
||||
Item,
|
||||
Account,
|
||||
} from '@/models';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
import InventoryService from '../Inventory/Inventory';
|
||||
import { AccountTransaction } from '../../models';
|
||||
|
||||
/**
|
||||
* Vendor bills services.
|
||||
*/
|
||||
export default class BillsService {
|
||||
/**
|
||||
* Creates a new bill and stored it to the storage.
|
||||
*
|
||||
* Precedures.
|
||||
* ----
|
||||
* - Insert bill transactions to the storage.
|
||||
* - Insert bill entries to the storage.
|
||||
* - Increment the given vendor id.
|
||||
* - Record bill journal transactions on the given accounts.
|
||||
* - Record bill items inventory transactions.
|
||||
*
|
||||
* @param {IBill} bill -
|
||||
* @return {void}
|
||||
*/
|
||||
static async createBill(bill) {
|
||||
const storedBill = await Bill.tenant().query().insert({
|
||||
...omit(bill, ['entries']),
|
||||
const amount = sumBy(bill.entries, 'amount');
|
||||
const saveEntriesOpers = [];
|
||||
|
||||
const storedBill = await Bill.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
amount,
|
||||
...omit(bill, ['entries']),
|
||||
});
|
||||
bill.entries.forEach((entry) => {
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
reference_type: 'Bill',
|
||||
reference_id: storedBill.id,
|
||||
...omit(entry, ['amount']),
|
||||
});
|
||||
saveEntriesOpers.push(oper);
|
||||
});
|
||||
// Increment vendor balance.
|
||||
const incrementOper = Vendor.changeBalance(bill.vendor_id, amount);
|
||||
|
||||
await Promise.all([
|
||||
...saveEntriesOpers,
|
||||
incrementOper,
|
||||
this.recordInventoryTransactions(bill, storedBill.id),
|
||||
this.recordJournalTransactions({ ...bill, id: storedBill.id }),
|
||||
]);
|
||||
return storedBill;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch items entries to the storage.
|
||||
*
|
||||
* @param {Array} newEntries
|
||||
* @param {Array} oldEntries
|
||||
* @param {String} referenceType
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async patchItemsEntries(newEntries, oldEntries, referenceType, billId) {
|
||||
const entriesHasIds = newEntries.filter((entry) => entry.id);
|
||||
const entriesHasNoIds = newEntries.filter((entry) => !entry.id);
|
||||
|
||||
const entriesIds = entriesHasIds.map(entry => entry.id);
|
||||
|
||||
const oldEntriesIds = oldEntries.map((e) => e.id);
|
||||
const opers = [];
|
||||
|
||||
const entriesIdsShouldDelete = difference(
|
||||
oldEntriesIds,
|
||||
entriesIds,
|
||||
);
|
||||
if (entriesIdsShouldDelete.length > 0) {
|
||||
const deleteOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.whereIn('id', entriesIdsShouldDelete)
|
||||
.delete();
|
||||
opers.push(deleteOper);
|
||||
}
|
||||
entriesHasIds.forEach((entry) => {
|
||||
const updateOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.where('id', entry.id)
|
||||
.update({
|
||||
...omit(entry, ['id']),
|
||||
});
|
||||
opers.push(updateOper);
|
||||
});
|
||||
entriesHasNoIds.forEach((entry) => {
|
||||
const insertOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
reference_id: billId,
|
||||
reference_type: referenceType,
|
||||
...omit(entry, ['id', 'amount']),
|
||||
});
|
||||
opers.push(insertOper);
|
||||
});
|
||||
return Promise.all([...opers]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits details of the given bill id with associated entries.
|
||||
* @param {Integer} billId
|
||||
* @param {IBill} bill
|
||||
*
|
||||
* Precedures:
|
||||
* -------
|
||||
* - Update the bill transaction on the storage.
|
||||
* - Update the bill entries on the storage and insert the not have id and delete
|
||||
* once that not presented.
|
||||
* - Increment the diff amount on the given vendor id.
|
||||
* - Re-write the inventory transactions.
|
||||
* - Re-write the bill journal transactions.
|
||||
*
|
||||
* @param {Integer} billId
|
||||
* @param {IBill} bill
|
||||
*/
|
||||
static async editBill(billId, bill) {
|
||||
const updatedBill = await Bill.tenant().query().insert({
|
||||
...omit(bill, ['entries']),
|
||||
const amount = sumBy(bill.entries, 'amount');
|
||||
|
||||
// Update the bill transaction.
|
||||
const updatedBill = await Bill.tenant()
|
||||
.query()
|
||||
.where('id', billId)
|
||||
.update({
|
||||
amount,
|
||||
...omit(bill, ['entries'])
|
||||
});
|
||||
|
||||
// Old stored entries.
|
||||
const storedEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('reference_id', billId)
|
||||
.where('reference_type', 'Bill');
|
||||
|
||||
// Patch the bill entries.
|
||||
const patchEntriesOper = this.patchItemsEntries(bill.entries, storedEntries, 'Bill', billId);
|
||||
|
||||
// Record bill journal transactions.
|
||||
const recordTransactionsOper = this.recordJournalTransactions(bill, billId);
|
||||
|
||||
await Promise.all([
|
||||
patchEntriesOper,
|
||||
recordTransactionsOper,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records inventory transactions.
|
||||
* @param {IBill} bill -
|
||||
* @return {void}
|
||||
*/
|
||||
static async recordInventoryTransactions(bill, billId) {
|
||||
const storeInventoryTransactions = [];
|
||||
const entriesItemsIds = bill.entries.map((e) => e.item_id);
|
||||
const inventoryItems = await Item.tenant()
|
||||
.query()
|
||||
.whereIn('id', entriesItemsIds)
|
||||
.where('type', 'inventory');
|
||||
|
||||
const inventoryItemsIds = inventoryItems.map((i) => i.id);
|
||||
const inventoryEntries = bill.entries.filter(
|
||||
(entry) => inventoryItemsIds.indexOf(entry.item_id) !== -1
|
||||
);
|
||||
inventoryEntries.forEach((entry) => {
|
||||
const oper = InventoryTransaction.tenant().query().insert({
|
||||
direction: 'IN',
|
||||
date: bill.bill_date,
|
||||
|
||||
item_id: entry.item_id,
|
||||
quantity: entry.quantity,
|
||||
rate: entry.rate,
|
||||
remaining: entry.quantity,
|
||||
|
||||
transaction_type: 'Bill',
|
||||
transaction_id: billId,
|
||||
});
|
||||
storeInventoryTransactions.push(oper);
|
||||
});
|
||||
return Promise.all([...storeInventoryTransactions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the bill journal transactions.
|
||||
* @param {IBill} bill
|
||||
* @async
|
||||
* @param {IBill} bill
|
||||
* @param {Integer} billId
|
||||
*/
|
||||
async recordJournalTransactions(bill) {
|
||||
const entriesItemsIds = bill.entries.map(entry => entry.item_id);
|
||||
const payableTotal = sumBy(bill, 'entries.total');
|
||||
const storedItems = await Item.tenant().query().whereIn('id', entriesItemsIds);
|
||||
static async recordJournalTransactions(bill, billId) {
|
||||
const entriesItemsIds = bill.entries.map((entry) => entry.item_id);
|
||||
const payableTotal = sumBy(bill.entries, 'amount');
|
||||
const formattedDate = moment(bill.bill_date).format('YYYY-MM-DD');
|
||||
|
||||
const payableAccount = await Account.tenant().query();
|
||||
const formattedDate = moment(saleInvoice.invoice_date).format('YYYY-MM-DD');
|
||||
const storedItems = await Item.tenant()
|
||||
.query()
|
||||
.whereIn('id', entriesItemsIds);
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
|
||||
const payableAccount = await AccountsService.getAccountByType(
|
||||
'accounts_payable'
|
||||
);
|
||||
if (!payableAccount) {
|
||||
throw new Error('New payable account on the storage.');
|
||||
}
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
const commonJournalMeta = {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
referenceId: bill.id,
|
||||
referenceId: billId,
|
||||
referenceType: 'Bill',
|
||||
date: formattedDate,
|
||||
accural: true,
|
||||
};
|
||||
const payableEntry = await JournalEntry({
|
||||
if (billId) {
|
||||
const transactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', ['Bill'])
|
||||
.whereIn('reference_id', [billId])
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.removeEntries();
|
||||
}
|
||||
const payableEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
credit: payableTotal,
|
||||
contactId: bill.vendorId,
|
||||
account: payableAccount.id,
|
||||
contactId: bill.vendor_id,
|
||||
contactType: 'Vendor',
|
||||
});
|
||||
journal.credit(payableEntry);
|
||||
|
||||
bill.entries.forEach((item) => {
|
||||
if (['inventory'].indexOf(item.type) !== -1) {
|
||||
const inventoryEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
account: item.inventoryAccountId,
|
||||
});
|
||||
journal.debit(inventoryEntry);
|
||||
} else {
|
||||
const costEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
account: item.costAccountId,
|
||||
});
|
||||
journal.debit(costEntry);
|
||||
}
|
||||
bill.entries.forEach((entry) => {
|
||||
const item = storedItemsMap.get(entry.item_id);
|
||||
|
||||
const debitEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
debit: entry.amount,
|
||||
account:
|
||||
['inventory'].indexOf(item.type) !== -1
|
||||
? item.inventoryAccountId
|
||||
: item.costAccountId,
|
||||
});
|
||||
journal.debit(debitEntry);
|
||||
});
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveEntries(),
|
||||
journal.saveBalance(),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the bill with associated entries.
|
||||
* @param {Integer} billId
|
||||
* @param {Integer} billId
|
||||
* @return {void}
|
||||
*/
|
||||
static async deleteBill(billId) {
|
||||
await BillPayment.tenant().query().where('id', billId);
|
||||
const bill = await Bill.tenant().query().where('id', billId).first();
|
||||
|
||||
// Delete all associated bill entries.
|
||||
const deleteBillEntriesOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.where('reference_type', 'Bill')
|
||||
.where('reference_id', billId)
|
||||
.delete();
|
||||
|
||||
// Delete the bill transaction.
|
||||
const deleteBillOper = Bill.tenant().query().where('id', billId).delete();
|
||||
|
||||
// Delete associated bill journal transactions.
|
||||
const deleteTransactionsOper = JournalPosterService.deleteJournalTransactions(
|
||||
billId,
|
||||
'Bill'
|
||||
);
|
||||
// Delete bill associated inventory transactions.
|
||||
const deleteInventoryTransOper = InventoryService.deleteTransactions(
|
||||
billId,
|
||||
'Bill'
|
||||
);
|
||||
// Revert vendor balance.
|
||||
const revertVendorBalance = Vendor.changeBalance(billId, bill.amount * -1);
|
||||
|
||||
await Promise.all([
|
||||
deleteBillOper,
|
||||
deleteBillEntriesOper,
|
||||
deleteTransactionsOper,
|
||||
deleteInventoryTransOper,
|
||||
revertVendorBalance,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the bill exists on the storage.
|
||||
* @param {Integer} billId
|
||||
* @param {Integer} billId
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static async isBillExists(billId) {
|
||||
@@ -100,15 +320,31 @@ export default class BillsService {
|
||||
|
||||
/**
|
||||
* Detarmines whether the given bills exist on the storage in bulk.
|
||||
* @param {Array} billsIds
|
||||
* @param {Array} billsIds
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isBillsExist(billsIds) {
|
||||
|
||||
static async isBillsExist(billsIds) {
|
||||
const bills = await Bill.tenant().query().whereIn('id', billsIds);
|
||||
return bills.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the given bill id exists on the storage.
|
||||
* @param {Integer} billNumber
|
||||
*/
|
||||
static async isBillNoExists(billNumber) {
|
||||
const foundBills = await Bill.tenant().query().where('bill_number', billNumber);
|
||||
const foundBills = await Bill.tenant()
|
||||
.query()
|
||||
.where('bill_number', billNumber);
|
||||
return foundBills.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given bill details with associated items entries.
|
||||
* @param {Integer} billId -
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static getBill(billId) {
|
||||
return Bill.tenant().query().where('id', billId).first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ export default class JournalPosterService {
|
||||
/**
|
||||
* Deletes the journal transactions that associated to the given reference id.
|
||||
*/
|
||||
static async deleteJournalTransactions(referenceId) {
|
||||
static async deleteJournalTransactions(referenceId, referenceType) {
|
||||
const transactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', ['SaleInvoice'])
|
||||
.whereIn('reference_type', [referenceType])
|
||||
.where('reference_id', referenceId)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
@@ -21,5 +21,4 @@ export default class JournalPosterService {
|
||||
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,116 +1,371 @@
|
||||
import { omit } from 'lodash';
|
||||
import { PaymentReceive, PaymentReceiveEntry } from '@/models';
|
||||
import { omit, sumBy, mapValues, groupBy, chain } from 'lodash';
|
||||
import moment, { updateLocale } from 'moment';
|
||||
import {
|
||||
AccountTransaction,
|
||||
PaymentReceive,
|
||||
PaymentReceiveEntry,
|
||||
SaleInvoice,
|
||||
Customer,
|
||||
Account,
|
||||
} from '@/models';
|
||||
import AccountsService from '@/services/Accounts/AccountsService';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
|
||||
import PaymentReceiveEntryRepository from '@/repositories/PaymentReceiveEntryRepository';
|
||||
import CustomerRepository from '@/repositories/CustomerRepository';
|
||||
|
||||
export default class PaymentReceiveService extends JournalPosterService {
|
||||
/**
|
||||
* Creates a new payment receive and store it to the storage
|
||||
* with associated invoices payment and journal transactions.
|
||||
* @async
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
*/
|
||||
static async createPaymentReceive(paymentReceive) {
|
||||
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
|
||||
const storedPaymentReceive = await PaymentReceive.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
amount: paymentAmount,
|
||||
...omit(paymentReceive, ['entries']),
|
||||
});
|
||||
const storeOpers = [];
|
||||
|
||||
paymentReceive.entries.forEach((invoice) => {
|
||||
const oper = PaymentReceiveEntry.tenant().query().insert({
|
||||
payment_receive_id: storedPaymentReceive.id,
|
||||
...invoice,
|
||||
});
|
||||
storeOpers.push(oper);
|
||||
});
|
||||
await Promise.all([ ...storeOpers ]);
|
||||
paymentReceive.entries.forEach((entry) => {
|
||||
const oper = PaymentReceiveEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
payment_receive_id: storedPaymentReceive.id,
|
||||
...entry,
|
||||
});
|
||||
// Increment the invoice payment amount.
|
||||
const invoice = SaleInvoice.tenant()
|
||||
.query()
|
||||
.where('id', entry.invoice_id)
|
||||
.increment('payment_amount', entry.payment_amount);
|
||||
|
||||
storeOpers.push(oper);
|
||||
storeOpers.push(invoice);
|
||||
});
|
||||
const customerIncrementOper = Customer.decrementBalance(
|
||||
paymentReceive.customer_id,
|
||||
paymentAmount
|
||||
);
|
||||
const recordJournalTransactions = this.recordPaymentReceiveJournalEntries({
|
||||
id: storedPaymentReceive.id,
|
||||
...paymentReceive,
|
||||
});
|
||||
await Promise.all([
|
||||
...storeOpers,
|
||||
customerIncrementOper,
|
||||
recordJournalTransactions,
|
||||
]);
|
||||
return storedPaymentReceive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit details the given payment receive with associated entries.
|
||||
* ------
|
||||
* - Update the payment receive transactions.
|
||||
* - Insert the new payment receive entries.
|
||||
* - Update the given payment receive entries.
|
||||
* - Delete the not presented payment receive entries.
|
||||
* - Re-insert the journal transactions and update the different accounts balance.
|
||||
* - Update the different customer balances.
|
||||
* - Update the different invoice payment amount.
|
||||
* @async
|
||||
* @param {Integer} paymentReceiveId
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {Integer} paymentReceiveId
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
* @param {IPaymentReceive} oldPaymentReceive
|
||||
*/
|
||||
static async editPaymentReceive(paymentReceiveId, paymentReceive) {
|
||||
const updatePaymentReceive = await PaymentReceive.tenant().query()
|
||||
static async editPaymentReceive(
|
||||
paymentReceiveId,
|
||||
paymentReceive,
|
||||
oldPaymentReceive
|
||||
) {
|
||||
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
|
||||
// Update the payment receive transaction.
|
||||
const updatePaymentReceive = await PaymentReceive.tenant()
|
||||
.query()
|
||||
.where('id', paymentReceiveId)
|
||||
.update({
|
||||
amount: paymentAmount,
|
||||
...omit(paymentReceive, ['entries']),
|
||||
});
|
||||
const storedEntries = await PaymentReceiveEntry.tenant().query()
|
||||
.where('payment_receive_id', paymentReceiveId);
|
||||
|
||||
const entriesIds = paymentReceive.entries.filter(i => i.id);
|
||||
const opers = [];
|
||||
const entriesIds = paymentReceive.entries.filter((i) => i.id);
|
||||
const entriesShouldInsert = paymentReceive.entries.filter((i) => !i.id);
|
||||
|
||||
const entriesIdsShouldDelete = this.entriesShouldDeleted(
|
||||
storedEntries,
|
||||
entriesIds,
|
||||
// Detarmines which entries ids should be deleted.
|
||||
const entriesIdsShouldDelete = ServiceItemsEntries.entriesShouldDeleted(
|
||||
oldPaymentReceive.entries,
|
||||
entriesIds
|
||||
);
|
||||
if (entriesIdsShouldDelete.length > 0) {
|
||||
const deleteOper = PaymentReceiveEntry.tenant().query()
|
||||
.whereIn('id', entriesIdsShouldDelete)
|
||||
.delete();
|
||||
// Deletes the given payment receive entries.
|
||||
const deleteOper = PaymentReceiveEntryRepository.deleteBulk(
|
||||
entriesIdsShouldDelete
|
||||
);
|
||||
opers.push(deleteOper);
|
||||
}
|
||||
entriesIds.forEach((entry) => {
|
||||
const updateOper = PaymentReceiveEntry.tenant()
|
||||
.query()
|
||||
.pathAndFetchById(entry.id, {
|
||||
...omit(entry, ['id']),
|
||||
});
|
||||
// Entries that should be updated to the storage.
|
||||
if (entriesIds.length > 0) {
|
||||
const updateOper = PaymentReceiveEntryRepository.updateBulk(entriesIds);
|
||||
opers.push(updateOper);
|
||||
});
|
||||
await Promise.all([...opers]);
|
||||
}
|
||||
// Entries should insert to the storage.
|
||||
if (entriesShouldInsert.length > 0) {
|
||||
const insertOper = PaymentReceiveEntryRepository.insertBulk(
|
||||
entriesShouldInsert,
|
||||
paymentReceiveId
|
||||
);
|
||||
opers.push(insertOper);
|
||||
}
|
||||
// Re-write the journal transactions of the given payment receive.
|
||||
const recordJournalTransactions = this.recordPaymentReceiveJournalEntries(
|
||||
{
|
||||
id: oldPaymentReceive.id,
|
||||
...paymentReceive,
|
||||
},
|
||||
paymentReceiveId
|
||||
);
|
||||
// Increment/decrement the customer balance after calc the diff
|
||||
// between old and new value.
|
||||
const changeCustomerBalance = CustomerRepository.changeDiffBalance(
|
||||
paymentReceive.customer_id,
|
||||
oldPaymentReceive.customerId,
|
||||
paymentAmount,
|
||||
oldPaymentReceive.amount,
|
||||
);
|
||||
// Change the difference between the old and new invoice payment amount.
|
||||
const diffInvoicePaymentAmount = this.saveChangeInvoicePaymentAmount(
|
||||
oldPaymentReceive.entries,
|
||||
paymentReceive.entries
|
||||
);
|
||||
// Await the async operations.
|
||||
await Promise.all([
|
||||
...opers,
|
||||
recordJournalTransactions,
|
||||
changeCustomerBalance,
|
||||
diffInvoicePaymentAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given payment receive with associated entries
|
||||
* Deletes the given payment receive with associated entries
|
||||
* and journal transactions.
|
||||
* @param {Integer} paymentReceiveId
|
||||
* -----
|
||||
* - Deletes the payment receive transaction.
|
||||
* - Deletes the payment receive associated entries.
|
||||
* - Deletes the payment receive associated journal transactions.
|
||||
* - Revert the customer balance.
|
||||
* - Revert the payment amount of the associated invoices.
|
||||
* @async
|
||||
* @param {Integer} paymentReceiveId
|
||||
*/
|
||||
static async deletePaymentReceive(paymentReceiveId) {
|
||||
await PaymentReceive.tenant().query().where('id', paymentReceiveId).delete();
|
||||
await PaymentReceiveEntry.tenant().query().where('payment_receive_id', paymentReceiveId).delete();
|
||||
static async deletePaymentReceive(paymentReceiveId, paymentReceive) {
|
||||
// Deletes the payment receive transaction.
|
||||
await PaymentReceive.tenant()
|
||||
.query()
|
||||
.where('id', paymentReceiveId)
|
||||
.delete();
|
||||
|
||||
await this.deleteJournalTransactions(paymentReceiveId);
|
||||
// Deletes the payment receive associated entries.
|
||||
await PaymentReceiveEntry.tenant()
|
||||
.query()
|
||||
.where('payment_receive_id', paymentReceiveId)
|
||||
.delete();
|
||||
|
||||
// Delete all associated journal transactions to payment receive transaction.
|
||||
const deleteTransactionsOper = this.deleteJournalTransactions(
|
||||
paymentReceiveId,
|
||||
'PaymentReceive'
|
||||
);
|
||||
// Revert the customer balance.
|
||||
const revertCustomerBalance = Customer.incrementBalance(
|
||||
paymentReceive.customerId,
|
||||
paymentReceive.amount
|
||||
);
|
||||
// Revert the invoices payments amount.
|
||||
const revertInvoicesPaymentAmount = this.revertInvoicePaymentAmount(
|
||||
paymentReceive.entries.map((entry) => ({
|
||||
invoiceId: entry.invoiceId,
|
||||
revertAmount: entry.paymentAmount,
|
||||
}))
|
||||
);
|
||||
await Promise.all([
|
||||
deleteTransactionsOper,
|
||||
revertCustomerBalance,
|
||||
revertInvoicesPaymentAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the payment receive details of the given id.
|
||||
* @param {Integer} paymentReceiveId
|
||||
* @param {Integer} paymentReceiveId
|
||||
*/
|
||||
static async getPaymentReceive(paymentReceiveId) {
|
||||
const paymentReceive = await PaymentReceive.tenant().query().where('id', paymentReceiveId).first();
|
||||
const paymentReceive = await PaymentReceive.tenant()
|
||||
.query()
|
||||
.where('id', paymentReceiveId)
|
||||
.withGraphFetched('entries')
|
||||
.first();
|
||||
return paymentReceive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the payment receive details with associated invoices.
|
||||
* @param {Integer} paymentReceiveId
|
||||
* @param {Integer} paymentReceiveId
|
||||
*/
|
||||
static async getPaymentReceiveWithInvoices(paymentReceiveId) {
|
||||
const paymentReceive = await PaymentReceive.tenant().query()
|
||||
return PaymentReceive.tenant()
|
||||
.query()
|
||||
.where('id', paymentReceiveId)
|
||||
.withGraphFetched('invoices')
|
||||
.first();
|
||||
return paymentReceive;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detarmines whether the payment receive exists on the storage.
|
||||
* @param {Integer} paymentReceiveId
|
||||
*/
|
||||
static async isPaymentReceiveExists(paymentReceiveId) {
|
||||
const paymentReceives = await PaymentReceive.tenant().query().where('id', paymentReceiveId)
|
||||
const paymentReceives = await PaymentReceive.tenant()
|
||||
.query()
|
||||
.where('id', paymentReceiveId);
|
||||
return paymentReceives.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the payment receive number existance.
|
||||
* @async
|
||||
* @param {Integer} paymentReceiveNumber - Payment receive number.
|
||||
* @param {Integer} paymentReceiveId - Payment receive id.
|
||||
*/
|
||||
static async isPaymentReceiveNoExists(paymentReceiveNumber) {
|
||||
const paymentReceives = await PaymentReceive.tenant().query().where('payment_receive_no', paymentReceiveNumber);
|
||||
static async isPaymentReceiveNoExists(
|
||||
paymentReceiveNumber,
|
||||
paymentReceiveId
|
||||
) {
|
||||
const paymentReceives = await PaymentReceive.tenant()
|
||||
.query()
|
||||
.where('payment_receive_no', paymentReceiveNumber)
|
||||
.onBuild((query) => {
|
||||
if (paymentReceiveId) {
|
||||
query.whereNot('id', paymentReceiveId);
|
||||
}
|
||||
});
|
||||
return paymentReceives.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records payment receive journal transactions.
|
||||
* @async
|
||||
* @param {IPaymentReceive} paymentReceive
|
||||
*/
|
||||
static async recordPaymentReceiveJournalEntries(
|
||||
paymentReceive,
|
||||
paymentReceiveId
|
||||
) {
|
||||
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
|
||||
const formattedDate = moment(paymentReceive.payment_date).format(
|
||||
'YYYY-MM-DD'
|
||||
);
|
||||
const receivableAccount = await AccountsService.getAccountByType(
|
||||
'accounts_receivable'
|
||||
);
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
const commonJournal = {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
referenceId: paymentReceive.id,
|
||||
referenceType: 'PaymentReceive',
|
||||
date: formattedDate,
|
||||
};
|
||||
if (paymentReceiveId) {
|
||||
const transactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', ['PaymentReceive'])
|
||||
.where('reference_id', paymentReceiveId)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.removeEntries();
|
||||
}
|
||||
const creditReceivable = new JournalEntry({
|
||||
...commonJournal,
|
||||
credit: paymentAmount,
|
||||
contactType: 'Customer',
|
||||
contactId: paymentReceive.customer_id,
|
||||
account: receivableAccount.id,
|
||||
});
|
||||
const debitDepositAccount = new JournalEntry({
|
||||
...commonJournal,
|
||||
debit: paymentAmount,
|
||||
account: paymentReceive.deposit_account_id,
|
||||
});
|
||||
journal.credit(creditReceivable);
|
||||
journal.debit(debitDepositAccount);
|
||||
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveEntries(),
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the payment amount of the given invoices ids.
|
||||
* @param {Array} revertInvoices
|
||||
*/
|
||||
static async revertInvoicePaymentAmount(revertInvoices) {
|
||||
const opers = [];
|
||||
|
||||
revertInvoices.forEach((revertInvoice) => {
|
||||
const { revertAmount, invoiceId } = revertInvoice;
|
||||
const oper = SaleInvoice.tenant()
|
||||
.query()
|
||||
.where('id', invoiceId)
|
||||
.decrement('payment_amount', revertAmount);
|
||||
opers.push(oper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves difference changing between old and new invoice payment amount.
|
||||
* @param {Array} paymentReceiveEntries
|
||||
* @param {Array} newPaymentReceiveEntries
|
||||
* @return
|
||||
*/
|
||||
static async saveChangeInvoicePaymentAmount(
|
||||
paymentReceiveEntries,
|
||||
newPaymentReceiveEntries
|
||||
) {
|
||||
const opers = [];
|
||||
const newEntriesTable = chain(newPaymentReceiveEntries)
|
||||
.groupBy('invoice_id')
|
||||
.mapValues((group) => (sumBy(group, 'payment_amount') || 0) * -1)
|
||||
.value();
|
||||
|
||||
const diffEntries = chain(paymentReceiveEntries)
|
||||
.groupBy('invoiceId')
|
||||
.mapValues((group) => (sumBy(group, 'paymentAmount') || 0) * -1)
|
||||
.mapValues((value, key) => value - (newEntriesTable[key] || 0))
|
||||
.mapValues((value, key) => ({ invoice_id: key, payment_amount: value }))
|
||||
.filter((entry) => entry.payment_amount != 0)
|
||||
.values()
|
||||
.value();
|
||||
|
||||
diffEntries.forEach((diffEntry) => {
|
||||
const oper = SaleInvoice.changePaymentAmount(
|
||||
diffEntry.invoice_id,
|
||||
diffEntry.payment_amount
|
||||
);
|
||||
opers.push(oper);
|
||||
});
|
||||
return Promise.all([ ...opers ]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { omit, update, difference } from 'lodash';
|
||||
import { omit, sumBy, difference } from 'lodash';
|
||||
import {
|
||||
SaleInvoice,
|
||||
SaleInvoiceEntry,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
Item,
|
||||
ItemEntry,
|
||||
Customer,
|
||||
} from '@/models';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
|
||||
@@ -17,47 +18,37 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
* @return {ISaleInvoice}
|
||||
*/
|
||||
static async createSaleInvoice(saleInvoice) {
|
||||
const balance = sumBy(saleInvoice.entries, 'amount');
|
||||
const storedInvoice = await SaleInvoice.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
...omit(saleInvoice, ['entries']),
|
||||
balance,
|
||||
payment_amount: 0,
|
||||
});
|
||||
const opers = [];
|
||||
|
||||
saleInvoice.entries.forEach((entry) => {
|
||||
const oper = SaleInvoiceEntry.tenant()
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
sale_invoice_id: storedInvoice.id,
|
||||
...entry,
|
||||
reference_type: 'SaleInvoice',
|
||||
reference_id: storedInvoice.id,
|
||||
...omit(entry, ['amount', 'id']),
|
||||
});
|
||||
opers.push(oper);
|
||||
});
|
||||
await Promise.all([
|
||||
...opers,
|
||||
this.recordCreateJournalEntries(saleInvoice),
|
||||
]);
|
||||
const incrementOper = Customer.incrementBalance(
|
||||
saleInvoice.customer_id,
|
||||
balance,
|
||||
);
|
||||
await Promise.all([...opers, incrementOper]);
|
||||
return storedInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total of the sale invoice entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @return {ISaleInvoice}
|
||||
*/
|
||||
calcSaleInvoiceEntriesTotal(saleInvoice) {
|
||||
return {
|
||||
...saleInvoice,
|
||||
entries: saleInvoice.entries.map((entry) => ({
|
||||
...entry,
|
||||
total: 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the journal entries of sale invoice.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @return {void}
|
||||
*/
|
||||
async recordJournalEntries(saleInvoice) {
|
||||
@@ -69,8 +60,10 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
const formattedDate = moment(saleInvoice.invoice_date).format('YYYY-MM-DD');
|
||||
|
||||
const saleItemsIds = saleInvoice.entries.map((e) => e.item_id);
|
||||
const storedInvoiceItems = await Item.tenant().query().whereIn('id', saleItemsIds)
|
||||
|
||||
const storedInvoiceItems = await Item.tenant()
|
||||
.query()
|
||||
.whereIn('id', saleItemsIds);
|
||||
|
||||
const commonJournalMeta = {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
@@ -111,7 +104,6 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
accountNormal: 'debit',
|
||||
note: '',
|
||||
});
|
||||
|
||||
journal.debit(costEntry);
|
||||
}
|
||||
journal.credit(incomeEntry);
|
||||
@@ -129,9 +121,10 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
*/
|
||||
static async deleteSaleInvoice(saleInvoiceId) {
|
||||
await SaleInvoice.tenant().query().where('id', saleInvoiceId).delete();
|
||||
await SaleInvoiceEntry.tenant()
|
||||
await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('sale_invoice_id', saleInvoiceId)
|
||||
.where('reference_id', saleInvoiceId)
|
||||
.where('reference_type', 'SaleInvoice')
|
||||
.delete();
|
||||
|
||||
const invoiceTransactions = await AccountTransaction.tenant()
|
||||
@@ -151,39 +144,69 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
|
||||
/**
|
||||
* Edit the given sale invoice.
|
||||
* @param {Integer} saleInvoiceId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @param {Integer} saleInvoiceId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
*/
|
||||
static async editSaleInvoice(saleInvoiceId, saleInvoice) {
|
||||
const updatedSaleInvoices = await SaleInvoice.tenant().query()
|
||||
const updatedSaleInvoices = await SaleInvoice.tenant()
|
||||
.query()
|
||||
.where('id', saleInvoiceId)
|
||||
.update({
|
||||
...omit(saleInvoice, ['entries']),
|
||||
});
|
||||
const opers = [];
|
||||
const entriesIds = saleInvoice.entries.filter((entry) => entry.id);
|
||||
const storedEntries = await SaleInvoiceEntry.tenant().query()
|
||||
.where('sale_invoice_id', saleInvoiceId);
|
||||
const entriesNoIds = saleInvoice.entries.filter((entry) => !entry.id);
|
||||
|
||||
const storedEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('reference_id', saleInvoiceId)
|
||||
.where('reference_type', 'SaleInvoice');
|
||||
|
||||
const entriesIdsShouldDelete = this.entriesShouldDeleted(
|
||||
storedEntries,
|
||||
entriesIds,
|
||||
entriesIds
|
||||
);
|
||||
if (entriesIdsShouldDelete.length > 0) {
|
||||
const updateOper = SaleInvoiceEntry.tenant().query().where('id', entriesIdsShouldDelete);
|
||||
const updateOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.whereIn('id', entriesIdsShouldDelete)
|
||||
.delete();
|
||||
opers.push(updateOper);
|
||||
}
|
||||
entriesIds.forEach((entry) => {
|
||||
const updateOper = SaleInvoiceEntry.tenant()
|
||||
const updateOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.patchAndFetchById(entry.id, {
|
||||
.where('id', entry.id)
|
||||
.update({
|
||||
...omit(entry, ['id']),
|
||||
});
|
||||
opers.push(updateOper);
|
||||
});
|
||||
entriesNoIds.forEach((entry) => {
|
||||
const insertOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
reference_type: 'SaleInvoice',
|
||||
reference_id: saleInvoiceId,
|
||||
...omit(entry, ['id']),
|
||||
});
|
||||
opers.push(insertOper);
|
||||
})
|
||||
await Promise.all([...opers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice with associated entries.
|
||||
* @param {Integer} saleInvoiceId
|
||||
*/
|
||||
static async getSaleInvoiceWithEntries(saleInvoiceId) {
|
||||
return SaleInvoice.tenant().query()
|
||||
.where('id', saleInvoiceId)
|
||||
.withGraphFetched('entries')
|
||||
.first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the sale invoice number id exists on the storage.
|
||||
* @param {Integer} saleInvoiceId
|
||||
@@ -208,7 +231,7 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
query.where('invoice_no', saleInvoiceNumber);
|
||||
|
||||
if (saleInvoiceId) {
|
||||
query.whereNot('id', saleInvoiceId)
|
||||
query.whereNot('id', saleInvoiceId);
|
||||
}
|
||||
return query;
|
||||
});
|
||||
@@ -217,7 +240,7 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
|
||||
/**
|
||||
* Detarmine the invoices IDs in bulk and returns the not found ones.
|
||||
* @param {Array} invoicesIds
|
||||
* @param {Array} invoicesIds
|
||||
* @return {Array}
|
||||
*/
|
||||
static async isInvoicesExist(invoicesIds) {
|
||||
@@ -227,11 +250,8 @@ export default class SaleInvoicesService extends ServiceItemsEntries {
|
||||
builder.whereIn('id', invoicesIds);
|
||||
return builder;
|
||||
});
|
||||
const storedInvoicesIds = storedInvoices.map(i => i.id);
|
||||
const notStoredInvoices = difference(
|
||||
invoicesIds,
|
||||
storedInvoicesIds,
|
||||
);
|
||||
const storedInvoicesIds = storedInvoices.map((i) => i.id);
|
||||
const notStoredInvoices = difference(invoicesIds, storedInvoicesIds);
|
||||
return notStoredInvoices;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { omit, difference } from 'lodash';
|
||||
import { SaleEstimate, SaleEstimateEntry } from '@/models';
|
||||
|
||||
export default class SaleEstimateService {
|
||||
constructor() {}
|
||||
import { omit, difference, sumBy } from 'lodash';
|
||||
import { SaleEstimate, ItemEntry } from '@/models';
|
||||
import ServiceItemsEntries from '@/services/Sales/ServiceItemsEntries';
|
||||
|
||||
export default class SaleEstimateService extends ServiceItemsEntries {
|
||||
/**
|
||||
* Creates a new estimate with associated entries.
|
||||
* @async
|
||||
@@ -11,23 +10,27 @@ export default class SaleEstimateService {
|
||||
* @return {void}
|
||||
*/
|
||||
static async createEstimate(estimate) {
|
||||
const amount = sumBy(estimate.entries, 'amount');
|
||||
const storedEstimate = await SaleEstimate.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
amount,
|
||||
...omit(estimate, ['entries']),
|
||||
});
|
||||
const storeEstimateEntriesOpers = [];
|
||||
|
||||
estimate.entries.forEach((entry) => {
|
||||
const oper = SaleEstimateEntry.tenant()
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
estimate_id: storedEstimate.id,
|
||||
...entry,
|
||||
reference_type: 'SaleEstimate',
|
||||
reference_id: storedEstimate.id,
|
||||
...omit(entry, ['total', 'amount']),
|
||||
});
|
||||
storeEstimateEntriesOpers.push(oper);
|
||||
});
|
||||
await Promise.all([...storeEstimateEntriesOpers]);
|
||||
|
||||
return storedEstimate;
|
||||
}
|
||||
|
||||
@@ -38,9 +41,10 @@ export default class SaleEstimateService {
|
||||
* @return {void}
|
||||
*/
|
||||
static async deleteEstimate(estimateId) {
|
||||
await SaleEstimateEntry.tenant()
|
||||
await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('estimate_id', estimateId)
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate')
|
||||
.delete();
|
||||
await SaleEstimate.tenant().query().where('id', estimateId).delete();
|
||||
}
|
||||
@@ -53,43 +57,57 @@ export default class SaleEstimateService {
|
||||
* @return {void}
|
||||
*/
|
||||
static async editEstimate(estimateId, estimate) {
|
||||
const amount = sumBy(estimate.entries, 'amount');
|
||||
const updatedEstimate = await SaleEstimate.tenant()
|
||||
.query()
|
||||
.update({
|
||||
amount,
|
||||
...omit(estimate, ['entries']),
|
||||
});
|
||||
const storedEstimateEntries = await SaleEstimateEntry.tenant()
|
||||
const storedEstimateEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('estimate_id', estimateId);
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate');
|
||||
|
||||
const opers = [];
|
||||
const storedEstimateEntriesIds = storedEstimateEntries.map((e) => e.id);
|
||||
const estimateEntriesHasID = estimate.entries.filter((entry) => entry.id);
|
||||
const formEstimateEntriesIds = estimateEntriesHasID.map(
|
||||
(entry) => entry.id
|
||||
);
|
||||
const entriesHasID = estimate.entries.filter((entry) => entry.id);
|
||||
const entriesHasNoIDs = estimate.entries.filter((entry) => !entry.id);
|
||||
|
||||
const storedEntriesIds = storedEstimateEntries.map((e) => e.id);
|
||||
const formEstimateEntriesIds = entriesHasID.map((entry) => entry.id);
|
||||
|
||||
const entriesIdsShouldBeDeleted = difference(
|
||||
storedEstimateEntriesIds,
|
||||
storedEntriesIds,
|
||||
formEstimateEntriesIds,
|
||||
);
|
||||
|
||||
console.log(entriesIdsShouldBeDeleted);
|
||||
// Deletes the given sale estimate entries ids.
|
||||
if (entriesIdsShouldBeDeleted.length > 0) {
|
||||
const oper = SaleEstimateEntry.tenant()
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.where('id', entriesIdsShouldBeDeleted)
|
||||
.whereIn('id', entriesIdsShouldBeDeleted)
|
||||
.delete();
|
||||
opers.push(oper);
|
||||
}
|
||||
estimateEntriesHasID.forEach((entry) => {
|
||||
const oper = SaleEstimateEntry.tenant()
|
||||
// Insert the new sale estimate entries.
|
||||
entriesHasNoIDs.forEach((entry) => {
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
reference_type: 'SaleEstimate',
|
||||
reference_id: estimateId,
|
||||
...entry,
|
||||
});
|
||||
opers.push(oper);
|
||||
});
|
||||
entriesHasID.forEach((entry) => {
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.patchAndFetchById(entry.id, {
|
||||
...omit(entry, ['id']),
|
||||
});
|
||||
opers.push(oper);
|
||||
});
|
||||
await Promise.all([...opers]);
|
||||
return Promise.all([...opers]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,10 +134,11 @@ export default class SaleEstimateService {
|
||||
.filter((e) => e.id)
|
||||
.map((e) => e.id);
|
||||
|
||||
const estimateEntries = await SaleEstimateEntry.tenant()
|
||||
const estimateEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.whereIn('id', estimateEntriesIds)
|
||||
.where('estimate_id', estimateId);
|
||||
.where('reference_id', estimateId)
|
||||
.where('reference_type', 'SaleEstimate');
|
||||
|
||||
const storedEstimateEntriesIds = estimateEntries.map((e) => e.id);
|
||||
const notFoundEntriesIDs = difference(
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { omit, difference } from 'lodash';
|
||||
import { omit, difference, sumBy } from 'lodash';
|
||||
import {
|
||||
SaleReceipt,
|
||||
SaleReceiptEntry,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
} from '@/models';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import ItemEntry from '../../models/ItemEntry';
|
||||
import JournalPosterService from '@/services/Sales/JournalPosterService';
|
||||
|
||||
export default class SalesReceipt {
|
||||
constructor() {}
|
||||
|
||||
export default class SalesReceipt extends JournalPosterService {
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @async
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @return {Object}
|
||||
*/
|
||||
static async createSaleReceipt(saleReceipt) {
|
||||
const amount = sumBy(saleReceipt.entries, 'amount');
|
||||
const storedSaleReceipt = await SaleReceipt.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
amount,
|
||||
...omit(saleReceipt, ['entries']),
|
||||
});
|
||||
const storeSaleReceiptEntriesOpers = [];
|
||||
|
||||
saleReceipt.entries.forEach((entry) => {
|
||||
const oper = SaleReceiptEntry.tenant()
|
||||
const oper = ItemEntry.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
sale_receipt_id: storedSaleReceipt.id,
|
||||
...entry,
|
||||
reference_type: 'SaleReceipt',
|
||||
reference_id: storedSaleReceipt.id,
|
||||
...omit(entry, ['id', 'amount']),
|
||||
});
|
||||
storeSaleReceiptEntriesOpers.push(oper);
|
||||
});
|
||||
@@ -38,34 +41,11 @@ export default class SalesReceipt {
|
||||
/**
|
||||
* Records journal transactions for sale receipt.
|
||||
* @param {ISaleReceipt} saleReceipt
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async _recordJournalTransactions(saleReceipt) {
|
||||
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||
|
||||
const creditEntry = new journalEntry({
|
||||
debit: 0,
|
||||
credit: saleReceipt.total,
|
||||
account: saleReceipt.incomeAccountId,
|
||||
referenceType: 'SaleReceipt',
|
||||
referenceId: saleReceipt.id,
|
||||
note: saleReceipt.note,
|
||||
});
|
||||
const debitEntry = new journalEntry({
|
||||
debit: saleReceipt.total,
|
||||
credit: 0,
|
||||
account: saleReceipt.incomeAccountId,
|
||||
referenceType: 'SaleReceipt',
|
||||
referenceId: saleReceipt.id,
|
||||
note: saleReceipt.note,
|
||||
});
|
||||
journalPoster.credit(creditEntry);
|
||||
journalPoster.credit(debitEntry);
|
||||
|
||||
await Promise.all([
|
||||
journalPoster.saveEntries(),
|
||||
journalPoster.saveBalance(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,42 +55,45 @@ export default class SalesReceipt {
|
||||
* @return {void}
|
||||
*/
|
||||
static async editSaleReceipt(saleReceiptId, saleReceipt) {
|
||||
const amount = sumBy(saleReceipt.entries, 'amount');
|
||||
const updatedSaleReceipt = await SaleReceipt.tenant()
|
||||
.query()
|
||||
.where('id', saleReceiptId)
|
||||
.update({
|
||||
amount,
|
||||
...omit(saleReceipt, ['entries']),
|
||||
});
|
||||
const storedSaleReceiptEntries = await SaleReceiptEntry.tenant()
|
||||
const storedSaleReceiptEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('sale_receipt_id', saleReceiptId);
|
||||
.where('reference_id', saleReceiptId)
|
||||
.where('reference_type', 'SaleReceipt');
|
||||
|
||||
const storedSaleReceiptsIds = storedSaleReceiptEntries.map((e) => e.id);
|
||||
const entriesHasID = saleReceipt.entries.filter((entry) => entry.id);
|
||||
const entriesIds = entriesHasID.map((e) => e.id);
|
||||
|
||||
const opers = [];
|
||||
const entriesIdsShouldBeDeleted = difference(
|
||||
storedSaleReceiptsIds,
|
||||
entriesIds
|
||||
);
|
||||
const opers = [];
|
||||
|
||||
if (entriesIdsShouldBeDeleted.length > 0) {
|
||||
const deleteOper = SaleReceiptEntry.tenant()
|
||||
const deleteOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.where('id', entriesIdsShouldBeDeleted)
|
||||
.whereIn('id', entriesIdsShouldBeDeleted)
|
||||
.where('reference_type', 'SaleReceipt')
|
||||
.delete();
|
||||
opers.push(deleteOper);
|
||||
}
|
||||
entriesHasID.forEach((entry) => {
|
||||
const updateOper = SaleReceiptEntry.tenant()
|
||||
const updateOper = ItemEntry.tenant()
|
||||
.query()
|
||||
.patchAndFetchById(entry.id, {
|
||||
...omit(entry, ['id']),
|
||||
});
|
||||
opers.push(updateOper);
|
||||
});
|
||||
await Promise.all([...opers]);
|
||||
return Promise.all([...opers]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,27 +103,20 @@ export default class SalesReceipt {
|
||||
*/
|
||||
static async deleteSaleReceipt(saleReceiptId) {
|
||||
await SaleReceipt.tenant().query().where('id', saleReceiptId).delete();
|
||||
await SaleReceiptEntry.tenant()
|
||||
await ItemEntry.tenant()
|
||||
.query()
|
||||
.where('sale_receipt_id', saleReceiptId)
|
||||
.where('reference_id', saleReceiptId)
|
||||
.where('reference_type', 'SaleReceipt')
|
||||
.delete();
|
||||
|
||||
const receiptTransactions = await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('reference_type', ['SaleReceipt'])
|
||||
.where('reference_id', saleReceiptId)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const accountsDepGraph = await Account.tenant()
|
||||
.depGraph()
|
||||
.query()
|
||||
.remember();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
journal.loadEntries(receiptTransactions);
|
||||
journal.removeEntries();
|
||||
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
// Delete all associated journal transactions to payment receive transaction.
|
||||
const deleteTransactionsOper = this.deleteJournalTransactions(
|
||||
saleReceiptId,
|
||||
'SaleReceipt'
|
||||
);
|
||||
return Promise.all([
|
||||
deleteTransactionsOper,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,10 +141,11 @@ export default class SalesReceipt {
|
||||
.filter((e) => e.id)
|
||||
.map((e) => e.id);
|
||||
|
||||
const storedEntries = await SaleReceiptEntry.tenant()
|
||||
const storedEntries = await ItemEntry.tenant()
|
||||
.query()
|
||||
.whereIn('id', entriesIDs)
|
||||
.where('sale_receipt_id', saleReceiptId);
|
||||
.where('reference_id', saleReceiptId)
|
||||
.where('reference_type', 'SaleReceipt');
|
||||
|
||||
const storedEntriesIDs = storedEntries.map((e) => e.id);
|
||||
const notFoundEntriesIDs = difference(
|
||||
@@ -178,6 +155,10 @@ export default class SalesReceipt {
|
||||
return notFoundEntriesIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sale receipt with associated entries.
|
||||
* @param {Integer} saleReceiptId
|
||||
*/
|
||||
static async getSaleReceiptWithEntries(saleReceiptId) {
|
||||
const saleReceipt = await SaleReceipt.tenant().query()
|
||||
.where('id', saleReceiptId)
|
||||
|
||||
11
server/tsconfig.json
Normal file
11
server/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"jsx": "react",
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ module.exports = {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'~': path.resolve(__dirname, 'tests'),
|
||||
},
|
||||
extensions: [ '.tsx', '.ts', '.js' ],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -47,6 +48,11 @@ module.exports = {
|
||||
exclude: /(node_modules)/,
|
||||
test: /\.js$/,
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user