Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OBPIH-5455 Add filter for GL account #3866

Merged
merged 15 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions grails-app/conf/UrlMappings.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ class UrlMappings {
action = [GET: "tagOptions"]
}

// Gl account options for filters on product list page
"/api/glAccountOptions"(parseRequest: true) {
controller = { "glAccount" }
jmiranda marked this conversation as resolved.
Show resolved Hide resolved
action = [GET: "glAccountOptions"]
}

"/api/products"(parseRequest: true) {
controller = { "productApi" }
action = [GET: "list"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package org.pih.warehouse.api

import grails.converters.JSON
import org.pih.warehouse.core.GlAccount
import org.pih.warehouse.core.Location
import org.pih.warehouse.core.Tag
import org.pih.warehouse.product.Category
Expand All @@ -32,6 +33,7 @@ class ProductApiController extends BaseDomainApiController {
def categories = params.categoryId ? Category.findAllByIdInList(params.list("categoryId")) : null
def tags = params.tagId ? Tag.getAll(params.list("tagId")) : []
def catalogs = params.catalogId ? ProductCatalog.getAll(params.list("catalogId")) : []
def glAccounts = params.glAccountsId ? GlAccount.getAll(params.list('glAccountsId')) : []

// Following this approach of assigning q into other params for productService.getProducts
params.name = params.q
Expand All @@ -49,7 +51,7 @@ class ProductApiController extends BaseDomainApiController {
params.max = -1
}

def products = productService.getProducts(categories, catalogs, tags, includeInactive, params)
def products = productService.getProducts(categories, catalogs, tags, glAccounts, includeInactive, params)

if (params.format == 'csv') {
boolean includeAttributes = params.boolean("includeAttributes") ?: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
* You must not remove this notice, or any other, from this software.
**/
package org.pih.warehouse.core

import grails.converters.JSON
import org.pih.warehouse.product.Product

class GlAccountController {

def GlAccountService glAccountService

def index = {
redirect(action: "list", params: params)
}
Expand Down Expand Up @@ -84,4 +88,9 @@ class GlAccountController {
redirect(action: "list")
}
}

def glAccountOptions = {
def glAccounts = glAccountService.getGlAccountsOptions()
render([data: glAccounts] as JSON)
}
}
1 change: 1 addition & 0 deletions grails-app/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4103,6 +4103,7 @@ react.productsList.addProduct.label=Add product
react.productsList.filters.category.label=Category
react.productsList.filters.catalog.label=Formulary
react.productsList.filters.tags.label=Tags
react.productsList.filters.glAccount.label=Gl Account
react.productsList.filters.search.placeholder.label=Search by product name
react.productsList.column.active.label=Active
react.productsList.column.code.label=Code
Expand Down
19 changes: 19 additions & 0 deletions grails-app/services/org/pih/warehouse/core/GlAccountService.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2012 Partners In Health. All rights reserved.
* The use and distribution terms for this software are covered by the
* Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
* which can be found in the file epl-v10.html at the root of this distribution.
* By using this software in any fashion, you are agreeing to be bound by
* the terms of this license.
* You must not remove this notice, or any other, from this software.
**/
package org.pih.warehouse.core

class GlAccountService {

def getGlAccountsOptions() {
return GlAccount.list().collect {
[id: it.id, label: "${it.code} - ${it.name}"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ class ProductService {
* @return
*/
List<Product> getProducts(Category category, List<Tag> tags, boolean includeInactive, Map params) {
return getProducts([category], [], tags, includeInactive, params)
return getProducts([category], [], tags, [], includeInactive, params)
}

/**
Expand All @@ -282,10 +282,11 @@ class ProductService {
* @param category
* @param catalogs
* @param tags
* @param glAccounts
* @param params
* @return
*/
List<Product> getProducts(List<Category> categories, List<ProductCatalog> catalogsInput, List<Tag> tagsInput, boolean includeInactive, Map params) {
List<Product> getProducts(List<Category> categories, List<ProductCatalog> catalogsInput, List<Tag> tagsInput, List<GlAccount> glAccounts, boolean includeInactive, Map params) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am starting to dislike these parameters more with each new one. I think we could think about refactoring this one.

int max = params.max ? params.int("max") : 10
int offset = params.offset ? params.int("offset") : 0
String sortColumn = params.sort ?: "name"
Expand Down Expand Up @@ -323,6 +324,11 @@ class ProductService {
}
}

if (glAccounts) {
glAccount {
'in'("id", glAccounts.collect { it.id })
}
}

if (tagsInput) {
tags {
Expand Down
6 changes: 6 additions & 0 deletions src/js/api/services/GlAccountApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GL_ACCOUNTS_OPTION } from 'api/urls';
import apiClient from 'utils/apiClient';

export default {
getGlAccountOptions: () => apiClient.get(GL_ACCOUNTS_OPTION),
};
17 changes: 11 additions & 6 deletions src/js/api/urls.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
const API = '/openboxes/api';

// PURCHASE ORDER
export const PURCHASE_ORDER_API = '/openboxes/api/purchaseOrders';
export const PURCHASE_ORDER_API = `${API}/purchaseOrders`;
export const PURCHASE_ORDER_DELETE = id => `${PURCHASE_ORDER_API}/${id}`;
export const PURCHASE_ORDER_ROLLBACK_ORDER = id => `${PURCHASE_ORDER_API}/${id}/rollback`;

// STOCK MOVEMENT
export const STOCK_MOVEMENT_API = '/openboxes/api/stockMovements';
export const STOCK_MOVEMENT_API = `${API}/stockMovements`;
export const STOCK_MOVEMENT_DELETE = id => `${STOCK_MOVEMENT_API}/${id}`;
export const STOCK_MOVEMENT_PENDING_SHIPMENT_ITEMS = `${STOCK_MOVEMENT_API}/pendingRequisitionItems`;
export const STOCK_MOVEMENT_INCOMING_ITEMS = `${STOCK_MOVEMENT_API}/shippedItems`;

// STOCK TRANSFER
export const STOCK_TRANSFER_API = '/openboxes/api/stockTransfers';
export const STOCK_TRANSFER_API = `${API}/stockTransfers`;
export const STOCK_TRANSFER_DELETE = id => `${STOCK_TRANSFER_API}/${id}`;

// INVOICE
export const INVOICE_API = '/openboxes/api/invoices';
export const INVOICE_API = `${API}/invoices`;

// PRODUCT
export const PRODUCT_API = '/openboxes/api/products';
export const PRODUCT_API = `${API}/products`;

// STOCK LIST
export const STOCKLIST_API = '/openboxes/api/stocklists';
export const STOCKLIST_API = `${API}/stocklists`;
export const STOCKLIST_EXPORT = id => `${STOCKLIST_API}/${id}/export`;
export const STOCKLIST_DELETE = id => `${STOCKLIST_API}/${id}`;
export const STOCKLIST_CLEAR = id => `${STOCKLIST_API}/${id}/clear`;
export const STOCKLIST_CLONE = id => `${STOCKLIST_API}/${id}/clone`;
export const STOCKLIST_PUBLISH = id => `${STOCKLIST_API}/${id}/publish`;
export const STOCKLIST_UNPUBLISH = id => `${STOCKLIST_API}/${id}/unpublish`;

// GL ACCOUNTS
export const GL_ACCOUNTS_OPTION = `${API}/glAccountOptions`;
15 changes: 15 additions & 0 deletions src/js/components/products/FilterFields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export default {
options: tags,
}),
},
glAccountsId: {
type: FilterSelectField,
attributes: {
valueKey: 'id',
placeholder: 'react.productsList.filters.glAccount.label',
defaultPlaceholder: 'Gl Account',
multi: true,
closeMenuOnSelect: false,
blurInputOnSelect: false,
filterElement: true,
},
getDynamicAttr: ({ glAccounts }) => ({
options: glAccounts,
}),
},
includeInactive: {
type: CheckboxField,
label: 'react.productsList.includeInactive.label',
Expand Down
6 changes: 4 additions & 2 deletions src/js/components/products/ProductsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import useTranslation from 'hooks/useTranslation';

const ProductsList = () => {
const {
defaultFilterValues, setFilterValues, categories, catalogs, tags, filterParams,
defaultFilterValues, setFilterValues, categories, catalogs, tags, glAccounts, filterParams,
} = useProductFilters();

useTranslation('productsList', 'reactTable');
Expand All @@ -24,7 +24,9 @@ const ProductsList = () => {
defaultValues={defaultFilterValues}
setFilterParams={setFilterValues}
filterFields={filterFields}
formProps={{ categories, catalogs, tags }}
formProps={{
categories, catalogs, tags, glAccounts,
}}
/>
<ProductsListTable filterParams={filterParams} />
</div>
Expand Down
60 changes: 31 additions & 29 deletions src/js/hooks/list-pages/product/useProductFilters.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getParamList, transformFilterParams } from 'utils/list-utils';
import {
fetchProductsCatalogs,
fetchProductsCategories,
fetchProductsGlAccounts,
fetchProductsTags,
} from 'utils/option-utils';

Expand All @@ -18,6 +19,7 @@ const useProductFilters = () => {
const [categories, setCategories] = useState([]);
const [catalogs, setCatalogs] = useState([]);
const [tags, setTags] = useState([]);
const [glAccounts, setGlAccounts] = useState([]);
const [filtersInitialized, setFiltersInitialized] = useState(false);

const history = useHistory();
Expand All @@ -27,59 +29,58 @@ const useProductFilters = () => {
history.push({ pathname });
};

const setDefaultValue = (queryPropsParam, elementsList) => {
if (queryPropsParam) {
const idList = getParamList(queryPropsParam);
return elementsList
.filter(({ id }) => idList.includes(id))
.map(({ id, label }) => ({
id, label, name: label, value: id,
}));
}
return null;
};

const initializeDefaultFilterValues = async () => {
// INITIALIZE EMPTY FILTER OBJECT
const defaultValues = Object.keys(filterFields)
.reduce((acc, key) => ({ ...acc, [key]: '' }), {});

const queryProps = queryString.parse(history.location.search);

const {
catalogId, tagId, categoryId, glAccountsId,
} = queryProps;

// IF VALUE IS IN A SEARCH QUERY SET DEFAULT VALUES
if (queryProps.includeInactive) {
defaultValues.includeInactive = queryProps.includeInactive;
}
if (queryProps.includeCategoryChildren) {
defaultValues.includeCategoryChildren = queryProps.includeCategoryChildren;
}
// If there are no values for catalogs, tags or categories
// If there are no values for catalogs, tags, glAccounts or categories
// then set default filters without waiting for those options to load
if (!queryProps.catalogId && !queryProps.tagId && !queryProps.categoryId) {
if (!catalogId && !tagId && !categoryId && !glAccountsId) {
setDefaultFilterValues(defaultValues);
}
const [categoryList, catalogList, tagList] = await Promise.all([
const [categoryList, catalogList, tagList, glAccountsList] = await Promise.all([
fetchProductsCategories(),
fetchProductsCatalogs(),
fetchProductsTags(),
fetchProductsGlAccounts(),
]);
setCatalogs(catalogList);
setCategories(categoryList);
setTags(tagList);
setGlAccounts(glAccountsList);

if (queryProps.catalogId) {
const catalogIdList = getParamList(queryProps.catalogId);
defaultValues.catalogId = catalogList
.filter(({ id }) => catalogIdList.includes(id))
.map(({ id, label }) => ({
id, label, name: label, value: id,
}));
}
if (queryProps.tagId) {
const tagIdList = getParamList(queryProps.tagId);
defaultValues.tagId = tagList
.filter(({ id }) => tagIdList.includes(id))
.map(({ id, label }) => ({
id, label, name: label, value: id,
}));
}
if (queryProps.categoryId) {
const categoryIdList = getParamList(queryProps.categoryId);
defaultValues.categoryId = categoryList
.filter(({ id }) => categoryIdList.includes(id))
.map(({ id, label }) => ({
id, label, name: label, value: id,
}));
}
defaultValues.catalogId = setDefaultValue(catalogId, catalogList);
defaultValues.tagId = setDefaultValue(tagId, tagList);
defaultValues.categoryId = setDefaultValue(categoryId, categoryList);
defaultValues.glAccountsId = setDefaultValue(glAccountsId, glAccountsList);

if (queryProps.catalogId || queryProps.tagId || queryProps.categoryId) {
if (catalogId || tagId || categoryId || glAccountsId) {
setDefaultFilterValues(defaultValues);
}
setFiltersInitialized(true);
Expand All @@ -95,6 +96,7 @@ const useProductFilters = () => {
catalogId: { name: 'catalogId', accessor: 'id' },
tagId: { name: 'tagId', accessor: 'id' },
categoryId: { name: 'categoryId', accessor: 'id' },
glAccountsId: { name: 'glAccountsId', accessor: 'id' },
};
const transformedParams = transformFilterParams(values, filterAccessors);
const queryFilterParams = queryString.stringify(transformedParams);
Expand All @@ -106,7 +108,7 @@ const useProductFilters = () => {
};

return {
defaultFilterValues, setFilterValues, categories, catalogs, tags, filterParams,
defaultFilterValues, setFilterValues, categories, catalogs, tags, glAccounts, filterParams,
};
};

Expand Down
5 changes: 4 additions & 1 deletion src/js/hooks/list-pages/product/useProductsListTableData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const useProductsListTableData = (filterParams) => {
state,
sortingParams,
}) => {
const { catalogId, categoryId, tagId } = filterParams;
const {
catalogId, categoryId, tagId, glAccountsId,
} = filterParams;
return _.omitBy({
offset: `${offset}`,
max: `${state.pageSize}`,
Expand All @@ -27,6 +29,7 @@ const useProductsListTableData = (filterParams) => {
catalogId: catalogId && catalogId.map(({ id }) => id),
categoryId: categoryId && categoryId.map(({ id }) => id),
tagId: tagId && tagId.map(({ id }) => id),
glAccountsId: glAccountsId && glAccountsId.map(({ id }) => id),
}, (val) => {
if (typeof val === 'boolean') {
return !val;
Expand Down
6 changes: 6 additions & 0 deletions src/js/utils/option-utils.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _ from 'lodash';
import queryString from 'query-string';

import glAccountApi from 'api/services/GlAccountApi';
import apiClient from 'utils/apiClient';


Expand Down Expand Up @@ -218,3 +219,8 @@ export const fetchProductsTags = async () => {
const response = await apiClient.get('/openboxes/api/tagOptions');
return response.data.data;
};

export const fetchProductsGlAccounts = async () => {
const { data } = await glAccountApi.getGlAccountOptions();
return data.data;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could follow, since you've refactored some of our api calls to use api/services, to use this approach here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding one additional comment, which should be rather answered by @awalkowiak - I'm wondering if we should wrap it inside try/catch and have somewhat user-friendly error message if any happens, or let the handler handle that, but we could have some "ugly" error message in terms of user perspective?

Copy link
Member

@jmiranda jmiranda Feb 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion for standardizing error messages #3849 to be included in coding conventions. May require more technical discussions around this. #3867

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import grails.converters.JSON
import grails.orm.PagedResultList
import grails.test.ControllerUnitTestCase
import org.codehaus.groovy.grails.commons.DefaultGrailsApplication
import org.pih.warehouse.core.GlAccount
import org.pih.warehouse.core.Location
import org.pih.warehouse.core.LocationGroup
import org.pih.warehouse.core.LocationType
Expand Down Expand Up @@ -112,7 +113,7 @@ class ProductApiControllerTests extends ControllerUnitTestCase {
// GIVEN
controller.params.q = "Product"
controller.productService = [
getProducts: { List<Category> categories, List< ProductCatalog> catalogsInput, List<Tag> tagsInput, boolean includeInactive, Map params ->
getProducts: { List<Category> categories, List<ProductCatalog> catalogsInput, List<Tag> tagsInput, List<GlAccount> glAccounts, boolean includeInactive, Map params ->
def results = [product]
return new PagedResultList(results, results.size())
}
Expand Down