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-6044 Export invoice lines details on invoices list page #4484

Conversation

drodzewicz
Copy link
Collaborator

No description provided.

@drodzewicz drodzewicz force-pushed the OBPIH-6044-export-invoice-lines-details-on-invoices-list-page branch from b927232 to 1a1c187 Compare January 29, 2024 14:54
controller = "invoiceApi"
action = [GET: "getAllInvoiceItems"]
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't mind it, but I think we used to do it via the export mapping, with a proper param (e.g. "orderItems").

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes this is probably the way to go, this should be on /api/invoiceItems/

def getAllInvoiceItems() {
Location location = Location.get(session.warehouse.id)
params.partyFromId = location?.organization?.id
List<InvoiceList> invoices = invoiceService.getInvoices(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 spoke about that with @drodzewicz, but I want to make my point here, so that everyone can give their opinion - in my opinion it should be dealt directly via InvoiceItem.createCriteria() and

invoice {
  'in'("id" ,params.invoicesIds)
}

clause.
Plus I would monitor the performance here, because we might run into many N+1 - if we do, we could play with the data services and joins, but for now, I'd like to get others opinion about the approach of first getting the invoices and then getting the invoice items.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we are at risk of N+1 queries, if I am not missing anything this should be max 2 queries one for invoices and one for invoiceItems.

My goal was to reuse the query filter logic on invoiceService.getInvoices(params) since we are expecting to get only those InvoiceItems that are a part of filtered invoices from invoice/list page, basically aggregate these queries.
First get invoices (filtered if necessary) and then based on these invoices get all of the invoiceItems

Copy link
Collaborator

Choose a reason for hiding this comment

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

@drodzewicz On the other hand if this is for "import all" case you could also just do: get invoices -> return invoices.invoiceItems. Is the pagination required here? However if this is going to land in its own api controller, or the pagination params are required, then:

I agree with @kchelstowski on that one in the long term. If you need to do some further invoice filtering you still can do it in the suggested by Kacper way as a:

invoice {
  'in'('id', params.invoiceIds) // if there are invoice ids passed
  whatever other invoice filter you have there
}

Instead of doing get invoices -> get invoice items by these invoices.

invoiceItem.invoice?.currencyUom.name,
invoiceItem.invoice?.status?.name(),
"${invoiceItem.invoice?.partyFrom?.code} ${invoiceItem.invoice?.partyFrom?.name}",
invoiceItem.invoice?.invoiceType?.code?.name() ?: "INVOICE",
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's use the enum from InvoiceTypeCode in the fallback

invoiceItem.invoice?.status?.name(),
"${invoiceItem.invoice?.partyFrom?.code} ${invoiceItem.invoice?.partyFrom?.name}",
invoiceItem.invoice?.invoiceType?.code?.name() ?: "INVOICE",
invoiceItem.product?.productCode ?: "all",
Copy link
Collaborator

Choose a reason for hiding this comment

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

what does "all" mean here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

on invoice show page we have a similar case where we label adjustments as all since adjustments don't have a product code

<td>
${invoiceItem?.product?.productCode?:g.message(code:'default.all.label')}
</td>

@@ -34,6 +34,7 @@ export const STOCK_TRANSFER_REMOVE_ALL_ITEMS = id => `${STOCK_TRANSFER_BY_ID(id)

// INVOICE
export const INVOICE_API = `${API}/invoices`;
export const INVOICE_ITEMS_API = `${API}/invoices/items`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

if we agree this should be the url, then I think this could be shortened to:

export const INVOICE_ITEMS_API = `${INVOICE_API}/items`;

@@ -145,7 +146,7 @@ const InvoiceListTable = ({
<button
type="button"
className="dropdown-item"
onClick={() => downloadInvoices()}
onClick={downloadInvoices}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I said it once to @alannadolny , but we need to be aware, that in this case, if we added any argument to the downloadInvoices method, this would be run with the event from the button.

invoiceItem?.quantity,
invoiceItem?.quantityPerUom,
invoiceItem?.unitPrice ?: 0,
invoiceItem?.totalAmount ?: 0,
Copy link
Collaborator

Choose a reason for hiding this comment

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

@drodzewicz I meant you might run into N+1 in this place, where you refer to many different entities and this would run N+1 for every invoice item. So my N+1 comment from above was not directly refering to the "two queries" you made, but overall.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@kchelstowski Yup, you're right, I also prefer to use our data services here (or just measure the performance and make a decision based on that)

- change endpoint for exporting invoiceItems
- fixes for const values
@drodzewicz
Copy link
Collaborator Author

Pushed some requested changes suggetsed by @kchelstowski and waiting for other developers (@alannadolny @awalkowiak), to review this PR and give an opinion on Kacpers comments

def getAllInvoiceItems() {
Location location = Location.get(session.warehouse.id)
params.partyFromId = location?.organization?.id
List<InvoiceList> invoices = invoiceService.getInvoices(params)
Copy link
Collaborator

Choose a reason for hiding this comment

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

One more point here - should we pass the whole params object as an argument as it is done here?

List<InvoiceList> invoices = invoiceService.getInvoices(params)

@@ -460,4 +469,50 @@ class InvoiceService {
}
return csv
}

CSVPrinter getInvoiceItemsCsv(List<InvoiceItem> invoiceItems) {
def g = Holders.grailsApplication.mainContext.getBean('org.grails.plugins.web.taglib.ApplicationTagLib')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe it is worth to use move this to a function (like it is already done in some places)

    def getApplicationTagLib() {
        return Holders.grailsApplication.mainContext.getBean(ApplicationTagLib)
    }

invoiceItem?.quantity,
invoiceItem?.quantityPerUom,
invoiceItem?.unitPrice ?: 0,
invoiceItem?.totalAmount ?: 0,
Copy link
Collaborator

Choose a reason for hiding this comment

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

@kchelstowski Yup, you're right, I also prefer to use our data services here (or just measure the performance and make a decision based on that)

@@ -490,6 +490,11 @@ class UrlMappings {
action = [GET: "invoiceTypeCodes"]
}

"/api/invoiceItems"(parseRequest: true) {
controller = "invoiceApi"
action = [GET: "getAllInvoiceItems"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this should be in a separate InvoiceItemApiController, because what I see the getInvoiceItems in the InvoiceAPIController is meant to get the invoice items from a specific invoice item, so if this one is meant to pull all invoice items from different invoices, then this should land in a separate controller.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we also have

"/api/invoiceItems/$id/validation" {
    controller = "invoiceApi"
    action = [POST: "validateInvoiceItem"]
}

so this will also have to end up in invoiceItemApicontroller

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not really, I think this one can stay where it is, at least for now

@drodzewicz drodzewicz force-pushed the OBPIH-6044-export-invoice-lines-details-on-invoices-list-page branch from 2ada6eb to 2fee9b2 Compare February 5, 2024 09:15
@drodzewicz
Copy link
Collaborator Author

So after looking through the problem again, I came to the conclusion that my initial approach was wrong and this exact export should not have bee handled by a different endpoint targeting a list of invoiceItems but instead I should have treated it as an expansion to invoice/list since we are interested in all of the filter parameters to persist from the invoice list to invoiceItem.

An exact case is handled in Purchase Order list

def list = {
List<OrderSummary> purchaseOrders = orderService.getPurchaseOrders(params)
if (params.format == 'csv') {
def csv = params.boolean("orderItems") ? getOrderItemsCsv(purchaseOrders) : getOrdersCsv(purchaseOrders)
def name = params.boolean("orderItems") ? "OrderLineItems" : "Orders"
response.setHeader("Content-disposition", "attachment; filename=\"${name}-${new Date().format("MM/dd/yyyy")}.csv\"")
render(contentType: "text/csv", text: csv.out.toString())
return
}
render([
data: purchaseOrders,
totalPrice: purchaseOrders?.sum { it.order.totalNormalized?:0.0 } ?:0.0,
totalCount: purchaseOrders?.totalCount
] as JSON)
}

So following the example of PO list I did the same thing, which is exporting a different document based on passed parameter invoiceItems=true.

Additionally as Kacper mentioned in previous comments, it would be nice to fetch invoice items eagerly for this export to avoid unnecessary N+1 queries to the database, so I added a parameter fetchItemsEagerly to specify the FetchMode eager for invoiceItems on invoicesList.
After comparing the export time of both cases here are some results.
Exporting 780 invoice items from 350 Invoices:

  • with eager fetch: 1309 ms (avg)
  • without eager fetch: 1347 ms (avg)

@@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux';

import { hideSpinner, showSpinner } from 'actions';
import invoiceApi from 'api/services/InvoiceApi';
import invoiceItemApi from 'api/services/InvoiceItemApi';
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is not used?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's not, thanks

- add invoiceItems parameter to specify invoiceItem export
- rollback previous changes
- add fetchItemsEagerly parameter to fetch invoiceitems eagerly
@drodzewicz drodzewicz force-pushed the OBPIH-6044-export-invoice-lines-details-on-invoices-list-page branch from 2fee9b2 to c86f6c3 Compare February 5, 2024 10:12
Copy link
Collaborator

@kchelstowski kchelstowski left a comment

Choose a reason for hiding this comment

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

thanks for the times @drodzewicz . Even though the difference is not that huge right now, we will get the benefit from it when the invoice table becomes bigger and bigger.

@awalkowiak awalkowiak merged commit 1a8a1c2 into feature/upgrade-to-grails-3.3.10 Feb 5, 2024
1 check passed
@awalkowiak awalkowiak deleted the OBPIH-6044-export-invoice-lines-details-on-invoices-list-page branch February 5, 2024 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants