From bc9638c9a276febb0069458d97488714f49dd64b Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 28 Oct 2020 13:47:35 +0200 Subject: [PATCH 1/3] feat: retrieve all due invoices and bills or for specific customer/vendor. --- server/src/api/controllers/Purchases/Bills.ts | 32 ++++++++++ .../api/controllers/Sales/SalesInvoices.ts | 64 ++++++++----------- server/src/models/Bill.js | 10 ++- server/src/models/SaleInvoice.js | 6 +- server/src/services/Purchases/Bills.ts | 24 ++++++- server/src/services/Sales/SalesInvoices.ts | 21 ++++++ 6 files changed, 117 insertions(+), 40 deletions(-) diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 10b48df6a..26af691c2 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -47,6 +47,14 @@ export default class BillsController extends BaseController { asyncMiddleware(this.editBill.bind(this)), this.handleServiceError, ); + router.get( + '/due', [ + ...this.dueBillsListingValidationSchema + ], + this.validationResult, + asyncMiddleware(this.getDueBills.bind(this)), + this.handleServiceError, + ) router.get( '/:id', [ ...this.specificBillValidationSchema, @@ -139,6 +147,12 @@ export default class BillsController extends BaseController { query('sort_order').optional().isIn(['desc', 'asc']), ]; } + + get dueBillsListingValidationSchema() { + return [ + query('vendor_id').optional().trim().escape(), + ] + } /** * Creates a new bill and records journal transactions. @@ -255,6 +269,24 @@ export default class BillsController extends BaseController { } } + /** + * Listing all due bills of the given vendor. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async getDueBills(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { vendorId } = this.matchedQueryData(req); + + try { + const bills = await this.billsService.getDueBills(tenantId, vendorId); + return res.status(200).send({ bills }); + } catch (error) { + next(error); + } + } + /** * Handles service errors. * @param {Error} error diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 0f5e1dcf0..a4b07bd97 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -52,9 +52,11 @@ export default class SaleInvoicesController extends BaseController{ this.handleServiceErrors, ); router.get( - '/due_invoices', - this.dueSalesInvoicesListValidationSchema, - asyncMiddleware(this.getDueSalesInvoice.bind(this)), + '/due', [ + ...this.dueSalesInvoicesListValidationSchema, + ], + this.validationResult, + asyncMiddleware(this.getDueInvoices.bind(this)), this.handleServiceErrors, ); router.get( @@ -210,40 +212,6 @@ export default class SaleInvoicesController extends BaseController{ next(error); } } - - /** - * Retrieve the due sales invoices for the given customer. - * @param {Request} req - * @param {Response} res - */ - async getDueSalesInvoice(req: Request, res: Response) { - const { Customer, SaleInvoice } = req.models; - const { tenantId } = req; - - const filter = { - customer_id: null, - ...req.query, - }; - if (filter.customer_id) { - const foundCustomer = await Customer.query().findById(filter.customer_id); - - if (!foundCustomer) { - return res.status(200).send({ - errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 200 }], - }); - } - } - const dueSalesInvoices = await SaleInvoice.query().onBuild((query) => { - query.where(raw('BALANCE - PAYMENT_AMOUNT > 0')); - if (filter.customer_id) { - query.where('customer_id', filter.customer_id); - } - }); - return res.status(200).send({ - due_sales_invoices: dueSalesInvoices, - }); - } - /** * Retrieve paginated sales invoices with custom view metadata. * @param {Request} req @@ -276,6 +244,28 @@ export default class SaleInvoicesController extends BaseController{ } } + /** + * Retrieve due sales invoices. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + * @return {Response|void} + */ + public async getDueInvoices(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { customerId } = this.matchedQueryData(req); + + try { + const salesInvoices = await this.saleInvoiceService.getDueInvoices(tenantId, customerId); + + return res.status(200).send({ + sales_invoices: this.transfromToResponse(salesInvoices), + }); + } catch (error) { + next(error); + } + } + /** * Handles service errors. * @param {Error} error diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index c7bbfa032..26c0b88f2 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -1,4 +1,4 @@ -import { Model } from 'objection'; +import { Model, raw } from 'objection'; import { difference } from 'lodash'; import TenantModel from 'models/TenantModel'; @@ -21,6 +21,14 @@ export default class Bill extends TenantModel { return true; } + static get modifiers() { + return { + dueBills(query) { + query.where(raw('AMOUNT - PAYMENT_AMOUNT > 0')); + } + } + } + /** * Timestamps columns. */ diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index b8ab76fdc..e05da70e0 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -1,4 +1,4 @@ -import { Model, mixin } from 'objection'; +import { Model, raw } from 'objection'; import moment from 'moment'; import TenantModel from 'models/TenantModel'; @@ -33,6 +33,10 @@ export default class SaleInvoice extends TenantModel { */ static get modifiers() { return { + dueInvoices(query) { + query.where(raw('BALANCE - PAYMENT_AMOUNT > 0')); + }, + filterDateRange(query, startDate, endDate, type = 'day') { const dateFormat = 'YYYY-MM-DD HH:mm:ss'; const fromDate = moment(startDate).startOf(type).format(dateFormat); diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 1ed75445f..6aebb2a02 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -27,6 +27,7 @@ import { import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; +import { Bill } from 'models'; const ERRORS = { BILL_NOT_FOUND: 'BILL_NOT_FOUND', @@ -135,7 +136,7 @@ export default class BillsService extends SalesInvoicesCost { * * @returns {IBill} */ - private async billDTOToModel(tenantId: number, billDTO: IBillDTO|IBillEditDTO, oldBill?: IBill) { + private async billDTOToModel(tenantId: number, billDTO: IBillDTO | IBillEditDTO, oldBill?: IBill) { const { ItemEntry } = this.tenancy.models(tenantId); let invLotNumber = oldBill?.invLotNumber; @@ -415,6 +416,27 @@ export default class BillsService extends SalesInvoicesCost { }; } + /** + * Retrieve all due bills or for specific given vendor id. + * @param {number} tenantId - + * @param {number} vendorId - + */ + public async getDueBills( + tenantId: number, + vendorId?: number + ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + const dueBills = await Bill.query().onBuild((query) => { + query.modify('dueBills'); + + if (vendorId) { + query.where('vendor_id', vendorId); + } + }); + return dueBills; + } + /** * Retrieve the given bill details with associated items entries. * @param {Integer} billId - Specific bill. diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index e2560b1b6..99267a2df 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -407,4 +407,25 @@ export default class SaleInvoicesService extends SalesInvoicesCost { filterMeta: dynamicFilter.getResponseMeta(), }; } + + /** + * Retrieve due sales invoices. + * @param {number} tenantId + * @param {number} customerId + */ + public async getDueInvoices( + tenantId: number, + customerId?: number, + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const salesInvoices = await SaleInvoice.query().onBuild((query) => { + query.modify('dueInvoices'); + + if (customerId) { + query.where('customer_id', customerId); + } + }); + return salesInvoices; + } } From 8a20deacf39c167f03526dec13e670946fddff04 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 28 Oct 2020 18:29:51 +0200 Subject: [PATCH 2/3] feat: add due bills reducer and selector. --- .../containers/Purchases/Bill/withBills.js | 4 ++++ client/src/store/Bills/bills.actions.js | 14 +++++++++++ client/src/store/Bills/bills.reducer.js | 20 ++++++++++++++++ client/src/store/Bills/bills.selectors.js | 24 +++++++++++++++++++ client/src/store/Bills/bills.type.js | 1 + server/src/models/Bill.js | 2 +- 6 files changed, 64 insertions(+), 1 deletion(-) diff --git a/client/src/containers/Purchases/Bill/withBills.js b/client/src/containers/Purchases/Bill/withBills.js index 5e552fc43..4c8a15547 100644 --- a/client/src/containers/Purchases/Bill/withBills.js +++ b/client/src/containers/Purchases/Bill/withBills.js @@ -4,12 +4,14 @@ import { getBillCurrentPageFactory, getBillPaginationMetaFactory, getBillTableQueryFactory, + getVendorDueBillsFactory } from 'store/Bills/bills.selectors'; export default (mapState) => { const getBillsItems = getBillCurrentPageFactory(); const getBillsPaginationMeta = getBillPaginationMetaFactory(); const getBillTableQuery = getBillTableQueryFactory(); + const getVendorDueBills = getVendorDueBillsFactory(); const mapStateToProps = (state, props) => { const tableQuery = getBillTableQuery(state, props); @@ -23,6 +25,8 @@ export default (mapState) => { billsPageination: getBillsPaginationMeta(state, props, tableQuery), billsLoading: state.bills.loading, nextBillNumberChanged: state.bills.nextBillNumberChanged, + + vendorDueBills: getVendorDueBills(state, props), }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/client/src/store/Bills/bills.actions.js b/client/src/store/Bills/bills.actions.js index 1ff6e36ff..8e57fa4df 100644 --- a/client/src/store/Bills/bills.actions.js +++ b/client/src/store/Bills/bills.actions.js @@ -117,3 +117,17 @@ export const editBill = (id, form) => { }); }); }; + +export const fetchDueBills = ({ vendorId }) => (dispatch) => new Promise((resolve, reject) => { + const params = { vendor_id: vendorId }; + + ApiService.get(`purchases/bills/due`, { params }).then((response) => { + dispatch({ + type: t.DUE_BILLS_SET, + payload: { + bills: response.data.bills, + } + }); + resolve(response); + }).catch(error => { reject(error) }); +}); \ No newline at end of file diff --git a/client/src/store/Bills/bills.reducer.js b/client/src/store/Bills/bills.reducer.js index a8f2d85f6..6f7f438e0 100644 --- a/client/src/store/Bills/bills.reducer.js +++ b/client/src/store/Bills/bills.reducer.js @@ -13,6 +13,7 @@ const initialState = { page: 1, }, nextBillNumberChanged: false, + dueBills: {}, }; const defaultBill = { @@ -103,6 +104,25 @@ const reducer = createReducer(initialState, { const { isChanged } = action.payload; state.nextBillNumberChanged = isChanged; }, + + [t.DUE_BILLS_SET]: (state, action) => { + const { bills } = action.payload; + + const _dueBills = { ...state.dueBills }; + const _bills = { ...state.items }; + + bills.forEach((bill) => { + _bills[bill.id] = { ...bill }; + + if (!_dueBills[bill.vendor_id]) { + _dueBills[bill.vendor_id] = [] + } + _dueBills[bill.vendor_id].push(bill.id); + }); + + state.items = { ..._bills }; + state.dueBills = { ..._dueBills }; + } }); export default createTableQueryReducers('bills', reducer); diff --git a/client/src/store/Bills/bills.selectors.js b/client/src/store/Bills/bills.selectors.js index 633690ee3..b2840dc6d 100644 --- a/client/src/store/Bills/bills.selectors.js +++ b/client/src/store/Bills/bills.selectors.js @@ -9,8 +9,18 @@ const billPageSelector = (state, props, query) => { }; const billItemsSelector = (state) => state.bills.items; +/** + * Retrieve bill details. + * @return {IBill} + */ const billByIdSelector = (state, props) => state.bills.items[props.billId]; +/** + * Retrieve vendor due bills ids. + * @return {number[]} + */ +const billsDueVendorSelector = (state, props) => state.bills.dueBills[props.vendorId]; + const billPaginationSelector = (state, props) => { const viewId = state.bills.currentViewId; return state.bills.views?.[viewId]; @@ -51,3 +61,17 @@ export const getBillPaginationMetaFactory = () => createSelector(billPaginationSelector, (billPage) => { return billPage?.paginationMeta || {}; }); + +/** + * Retrieve due bills of specific vendor. + */ +export const getVendorDueBillsFactory = () => + createSelector( + billItemsSelector, + billsDueVendorSelector, + (billsItems, dueBillsIds) => { + return Array.isArray(dueBillsIds) + ? pickItemsFromIds(billsItems, dueBillsIds) || [] + : []; + }, + ); \ No newline at end of file diff --git a/client/src/store/Bills/bills.type.js b/client/src/store/Bills/bills.type.js index 671478154..c8d3fae9d 100644 --- a/client/src/store/Bills/bills.type.js +++ b/client/src/store/Bills/bills.type.js @@ -10,4 +10,5 @@ export default { BILLS_PAGE_SET: 'BILLS_PAGE_SET', BILLS_ITEMS_SET: 'BILLS_ITEMS_SET', BILL_NUMBER_CHANGED: 'BILL_NUMBER_CHANGED', + DUE_BILLS_SET: 'DUE_BILLS_SET' }; diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index 26c0b88f2..c01df5e91 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -41,7 +41,7 @@ export default class Bill extends TenantModel { * @return {number} */ get dueAmount() { - return this.amount - this.paymentAmount; + return Math.max(this.amount - this.paymentAmount, 0); } /** From fd6a3224faf9f1376fa876e688a067bfed26efc9 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 28 Oct 2020 18:41:07 +0200 Subject: [PATCH 3/3] fix: Edit account with parent_account_id. --- server/src/services/Accounts/AccountsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 525be6c56..bd6a562e2 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -221,7 +221,7 @@ export default class AccountsService { } if (accountDTO.parentAccountId) { const parentAccount = await this.getParentAccountOrThrowError( - accountDTO.parentAccountId, oldAccount.id, + tenantId, accountDTO.parentAccountId, oldAccount.id, ); this.throwErrorIfParentHasDiffType(accountDTO, parentAccount); }