OBPIH-6037 Add bin location data to expiration reports#4468
Conversation
…ed stock csv reports
…and without binlocation included - refactor controller action for expiring stock to include binLocation if such parameter is passed - refactor controller action for expired stock to include binLocation if such parameter is passed - refactored listExpiredStock gsp to handle new data model - refactored listExpiringStock gsp to handle new data model
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } | ||
| data = data.findAll { inventoryItems.contains(it.inventoryItem) } |
There was a problem hiding this comment.
Can't we do this filtering in the PA query?
I think we could just pass the inventoryItems (or inventoryItems.id) to getQuantityByBinLocation (or the second method as well) and just add the IN :inventoryItems clause?
This could additionally improve the performance.
And a suggestion (regarding what version you prefer):
List<Map> data = withBinLocation
? productAvailabilityService.getQuantityOnHandByBinLocation(location)
: productAvailabilityService.getQuantityOnHandByInventoryItem(location)
.collect{ key, val -> [ inventoryItem: key, quantity: val ] }There was a problem hiding this comment.
Sure I can do it.
I just didn't want to modify the old service methods for that but it probably is worth changing it.
I was following the old logic for getting the expired or expiring inventoryItems and filtering the PA by that but I agree, this should probably be done in the db.
There was a problem hiding this comment.
One thing to mention is that we should probably have this type of logic in a more central service, like ProductAvailabilityService. The DashboardService was created before our product availability and should probably be phased out or refactored (i.e. it should delegate to product availability service and other reporting services).
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } | ||
| data = data.findAll { inventoryItems.contains(it.inventoryItem) } |
There was a problem hiding this comment.
this seems to be a repetition of what is done in the listExpiredStock. Maybe we could create a private method in this controller (as you know me - I'd prefer to have that type of logic in the service, but no must, since this probably needs to be refactored anyway, anytime soon).
| Location location = Location.get(session.warehouse.id) | ||
| Category categorySelected = Category.get(params.category) |
There was a problem hiding this comment.
Shouldn't we use data services since we are on Grails 3?
There was a problem hiding this comment.
Let's come up with our conventions for data services before we go lock, stock, and barrel. But what exactly would be changing here?
Also, what is going to happen if params.category is null here?
And just for giggles, we could use Location, Category, Boolean as arguments to this action and be done with the data binding.
There was a problem hiding this comment.
@jmiranda in general the difference is that data service's read operations are run in a readOnly transaction.
I once mentioned to the guys, that I needed to do a research about what happens if we are in a service (marked as @Transactional) and we'd run a method from data service that is readOnly - if the outer (service's) transaction would "swallow" that, hence the fetch would be done using our outer transaction, or if it would set our outer transaction as readOnly for a while.
In the end I think this readOnly doesn't matter at this point, because as soon as we run the:
categoryDataService.get(params.category)the readOnly transaction would commit at this point (so in further lines we could still update a category).
Also, what is going to happen if params.category is null here?
The data service's get evaluates to the basic Domain.get anyway, hence it'd return null if an entity didn't exist.
I think the data services methods matter much, if we do not take care of a transaction lifecycle on our own, because in the end, they evaluate to basic domain methods like Domain.get(...), Domain.delete(...), Domain.save(...), so I don't have a strong opinion about forcing to use data services for such simple cases (if we do take care of transaction boundaries on our own).
I find the data services very helpful and powerful for stuff like fetch join query, etc. where it has helped us a lot already.
| if (withBinLocation) { | ||
| data = productAvailabilityService.getQuantityOnHandByBinLocation(location) | ||
| } else { | ||
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } |
There was a problem hiding this comment.
Can't it be just written using a ternary operator?
There was a problem hiding this comment.
sure, I can.
I see you guys really hate if/else statements... 😅
I feel like simple if/else most of the time is much more readable than a ternary.
Ternary is great for very short statements and this case kind of applies to that, although I was also considering the case where in the future we want to add another parameter which would call another service method and ternary in this case would be very bad.
But yeah, I can change it to ternary.
There was a problem hiding this comment.
from my perspective it was only a suggestion for you to decide, which approach you prefer there. I want everyone to feel comfortable with their code style, since your version is not "breaking" any rules.
There was a problem hiding this comment.
No @kchelstowski that is not how this works. It's ternary or nothing!!!!
There was a problem hiding this comment.
Just kidding. I don't feel strongly either way. I do feel strongly that the code should be refactored and probably moved to the service layer and that the getExpiredStock should probably handle all or most of the work. But we can save that for another day in case it'll be too much work.
| Location location = Location.get(session.warehouse.id) | ||
| Category category = Category.get(params.category) |
| if (withBinLocation) { | ||
| data = productAvailabilityService.getQuantityOnHandByBinLocation(location) | ||
| } else { | ||
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } |
| <g:checkBox id="${dataEntry.inventoryItem?.id }" | ||
| name="inventoryItem.id" | ||
| class="checkbox" | ||
| checked="${false }" |
There was a problem hiding this comment.
| checked="${false }" | |
| checked="${false}" |
| <g:message code="default.button.downloadAsCSV.label" default="Download as CSV"/> | ||
| </g:link> | ||
|
|
||
| <g:link params="[format:'csv',category: params.category, withBinLocation: true]" controller="inventory" action="listExpiredStock" class="button"> |
There was a problem hiding this comment.
| <g:link params="[format:'csv',category: params.category, withBinLocation: true]" controller="inventory" action="listExpiredStock" class="button"> | |
| <g:link params="[format: 'csv',category: params.category, withBinLocation: true]" controller="inventory" action="listExpiredStock" class="button"> |
There was a problem hiding this comment.
+ split into new lines
| </div> | ||
|
|
||
| <g:link params="[format:'csv',category:params.category]" controller="${controllerName}" action="${actionName}" class="button"> | ||
| <g:link params="[format:'csv',category: params.category]" controller="inventory" action="listExpiredStock" class="button"> |
There was a problem hiding this comment.
there are a lot of params, maybe it would be more readable if we split those into new lines?
| Location location = Location.get(session.warehouse.id) | ||
| Category categorySelected = Category.get(params.category) |
There was a problem hiding this comment.
Let's come up with our conventions for data services before we go lock, stock, and barrel. But what exactly would be changing here?
Also, what is going to happen if params.category is null here?
And just for giggles, we could use Location, Category, Boolean as arguments to this action and be done with the data binding.
| if (withBinLocation) { | ||
| data = productAvailabilityService.getQuantityOnHandByBinLocation(location) | ||
| } else { | ||
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } |
There was a problem hiding this comment.
No @kchelstowski that is not how this works. It's ternary or nothing!!!!
| if (withBinLocation) { | ||
| data = productAvailabilityService.getQuantityOnHandByBinLocation(location) | ||
| } else { | ||
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } |
There was a problem hiding this comment.
Just kidding. I don't feel strongly either way. I do feel strongly that the code should be refactored and probably moved to the service layer and that the getExpiredStock should probably handle all or most of the work. But we can save that for another day in case it'll be too much work.
| data = productAvailabilityService.getQuantityOnHandByInventoryItem(location) | ||
| .collect{ key, val -> [ inventoryItem: key, quantity: val ] } | ||
| } | ||
| data = data.findAll { inventoryItems.contains(it.inventoryItem) } |
There was a problem hiding this comment.
One thing to mention is that we should probably have this type of logic in a more central service, like ProductAvailabilityService. The DashboardService was created before our product availability and should probably be phased out or refactored (i.e. it should delegate to product availability service and other reporting services).
| List<Category> categories = inventoryItems?.collect { it.product.category }?.unique() | ||
|
|
||
| List<Map> data | ||
| if (withBinLocation) { |
There was a problem hiding this comment.
I haven't read the ticket, but is there still a case where we don't need or want the bin location data?
be54b3b to
fbac56d
Compare
…ndByBinLocation and getQuantityOnHandByInventoryItem
fbac56d to
bd4767c
Compare
| groupProperty("inventoryItem", "inventoryItem") | ||
| groupProperty("binLocation", "binLocation") | ||
| sum("quantityOnHand", "quantityOnHand") | ||
| sum("quantityAvailableToPromise", "quantityAvailableToPromise") |
There was a problem hiding this comment.
have you made sure this would eventually return 0? I'm asking because of this:
sum(case when pa.quantityAvailableToPromise > 0 then pa.quantityAvailableToPromise else 0 end)There was a problem hiding this comment.
a function that transforms this result collectQuantityOnHandByBinLocation handles the logic for setting a 0 for null values, but does not handle negative values...
Thanks for pointing this out, I think I am going to have to revert this to a simple SQL because it is quite hard to implement this logic with CASE WHEN in the criteria
| quantityMap[it[0]] = it[1] | ||
| Map<InventoryItem, Integer> getQuantityOnHandByInventoryItem(Location location, List<InventoryItem> inventoryItems = []) { | ||
| if (!location) { | ||
| throw new RuntimeException("Must specify location in order to get quantity on hand") |
There was a problem hiding this comment.
was there a reason you refactored this part to throw an exception? Maybe there is a case where we'd still want to proceed with e.g. refresh PA, and by doing so, you'd break that operation.
There was a problem hiding this comment.
From what I have checked it seems we always require a location, but to be on the safe side I think I will revert it to returning an empty array instead of throwing an Exception.
| quantityMap[it[0]] = it[1] | ||
| Map<InventoryItem, Integer> getQuantityOnHandByInventoryItem(Location location, List<InventoryItem> inventoryItems = []) { | ||
| if (!location) { | ||
| return [] |
There was a problem hiding this comment.
this method by default was returning an empty Map, not a List - even the returned type suggests it should be the Map.
I think this would not work as expected with the list.
There was a problem hiding this comment.
thanks, there was a similar case that returns a list so I messed it up.
- return empty lists instead of throwing an Exception when location is not provided - bring back previous SQL query for QOHByBinLocation
eaa29ba to
3474233
Compare
No description provided.