Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
42 changes: 40 additions & 2 deletions applications/product/servicedef/services_picklist.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ under the License.

<!-- Persisted Picklist Services -->
<service name="createPicklistFromOrders" engine="simple"
location="component://product/minilang/shipment/picklist/PicklistServices.xml" invoke="createPicklistFromOrders" transaction-timeout="600">
location="component://product/minilang/shipment/picklist/PicklistServices.xml" invoke="createPicklistFromOrders" transaction-timeout="600" export="true" action="POST">
<description>Create Picklist From Orders</description>
<permission-service service-name="facilityPermissionCheck" main-action="CREATE"/>
<attribute name="facilityId" type="String" mode="IN" optional="false"/>
Expand Down Expand Up @@ -189,11 +189,12 @@ under the License.
<attribute name="oldLotId" mode="IN" type="String" optional="true" />
</service>
<service name="setPicklistItemToComplete" default-entity-name="PicklistItem" engine="simple"
location="component://product/minilang/shipment/picklist/PicklistServices.xml" invoke="setPicklistItemToComplete" auth="true">
location="component://product/minilang/shipment/picklist/PicklistServices.xml" invoke="setPicklistItemToComplete" auth="true" export="true" action="POST">
<description>Update PicklistItem's Status to COMPLETE</description>
<permission-service service-name="facilityPermissionCheck" main-action="UPDATE"/>
<auto-attributes include="pk" mode="IN" optional="false"/>
<auto-attributes include="nonpk" mode="IN" optional="false"/>
<override name="itemStatusId" optional="true"/>
</service>
<service name="cancelPicklistAndItems" engine="simple"
location="component://product/minilang/shipment/picklist/PicklistServices.xml" invoke="cancelPicklistAndItems" auth="true">
Expand Down Expand Up @@ -227,4 +228,41 @@ under the License.
<auto-attributes include="pk" mode="IN" optional="false"/>
</service>

<!-- Service for using as API from external app -->
<service name="getOrdersToPick" engine="groovy"
location="component://product/src/main/groovy/org/apache/ofbiz/product/shipment/PicklistServices.groovy" invoke="getOrdersToPick" auth="true" export="true" action="GET">
<description>Wrapper for findOrdersToPickMove to retrieve pickable orders.</description>
<attribute name="facilityId" type="String" mode="IN" optional="false"/>
<attribute name="orderList" type="List" mode="OUT" optional="true"/>
</service>

<service name="getPicklistDetails" engine="groovy"
location="component://product/src/main/groovy/org/apache/ofbiz/product/shipment/PicklistServices.groovy" invoke="getPicklistDetails" auth="true" export="true" action="GET">
<description>Wrapper for getPickAndPackReportInfo to retrieve details for a specific picklist.</description>
<attribute name="picklistId" type="String" mode="IN" optional="false"/>
<attribute name="picklistDetails" type="Map" mode="OUT" optional="true"/>
</service>

<service name="getPickingPicklists" engine="groovy"
location="component://product/src/main/groovy/org/apache/ofbiz/product/shipment/PicklistServices.groovy" invoke="getPickingPicklists" auth="true" export="true" action="GET">
<description>Retrieve all picklists for a facility, optionally filtered by status and search query.</description>
<attribute name="facilityId" type="String" mode="IN" optional="false"/>
<attribute name="statusId" type="String" mode="IN" optional="true"/>
<attribute name="searchQuery" type="String" mode="IN" optional="true"/>
<attribute name="picklistList" type="List" mode="OUT" optional="true"/>
</service>

<service name="cancelPickingPicklist" engine="groovy"
location="component://product/src/main/groovy/org/apache/ofbiz/product/shipment/PicklistServices.groovy" invoke="cancelPickingPicklist" auth="true" export="true" action="POST">
<description>Cancel a picklist and all its unpicked items.</description>
<attribute name="picklistId" type="String" mode="IN" optional="false"/>
</service>

<service name="getPickingPicklistPdf" engine="groovy"
location="component://product/src/main/groovy/org/apache/ofbiz/product/shipment/PicklistServices.groovy" invoke="getPickingPicklistPdf" auth="true" export="true" action="GET">
<description>Programmatically render picklist report FO to a Base64-encoded PDF string and transition its status from input to printed.</description>
<attribute name="picklistId" type="String" mode="IN" optional="false"/>
<attribute name="pdfBase64" type="String" mode="OUT" optional="true"/>
</service>

</services>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
package org.apache.ofbiz.product.shipment

import org.apache.ofbiz.entity.GenericValue
import org.apache.ofbiz.service.ServiceUtil
import org.apache.ofbiz.entity.condition.EntityCondition
import org.apache.ofbiz.entity.condition.EntityOperator
import org.apache.ofbiz.widget.model.ThemeFactory
import org.apache.ofbiz.widget.renderer.VisualTheme
import org.apache.ofbiz.widget.renderer.macro.MacroScreenRenderer
import org.apache.ofbiz.widget.renderer.ScreenRenderer
import org.apache.ofbiz.widget.renderer.fo.FoFormRenderer
import org.apache.ofbiz.webapp.view.ApacheFopWorker
import org.apache.ofbiz.base.util.collections.MapStack
import javax.xml.transform.stream.StreamSource
import org.apache.fop.apps.Fop

/*
* Migrate all element present on entity OldPicklistStatusHistory to entity PickListStatus
Expand All @@ -38,3 +50,251 @@ Map migrateOldPicklistStatusHistoryToPickListStatus() {
}
return success()
}

/**
* Retrieves a list of orders available to be picked for a given facility.
* Wraps existing findOrdersToPickMove service.
*/
Map getOrdersToPick() {
Map result = ServiceUtil.returnSuccess()

Map findResult = runService('findOrdersToPickMove', [facilityId: context.facilityId, userLogin: context.userLogin])
if (ServiceUtil.isError(findResult)) {
return findResult
}

// Transform pickMoveInfoList to a simpler list for the PWA
List orderList = []
if (findResult.pickMoveInfoList) {
findResult.pickMoveInfoList.each { pickMoveInfo ->
if (pickMoveInfo.orderReadyToPickInfoList) {
pickMoveInfo.orderReadyToPickInfoList.each { orderReadyInfo ->
GenericValue orderHeader = orderReadyInfo.orderHeader
if (orderHeader) {
if (!orderList.any { it.orderId == orderHeader.orderId }) {
orderList << [
orderId: orderHeader.orderId,
orderDate: orderHeader.orderDate,
statusId: orderHeader.statusId,
grandTotal: orderHeader.grandTotal
]
}
}
}
}
}
}

result.orderList = orderList
return result
}

/**
* Retrieves details of a specific picklist, including items sorted by location.
* Wraps existing getPickAndPackReportInfo service.
*/
Map getPicklistDetails() {
Map result = ServiceUtil.returnSuccess()

Map reportResult = runService('getPickAndPackReportInfo', [
picklistId: context.picklistId,
userLogin: context.userLogin
])

if (ServiceUtil.isError(reportResult)) {
return reportResult
}

// The reportResult contains a complex structure of picklistBins and their items.
// We will extract and flatten this for the PWA.
Map picklistDetails = [:]
picklistDetails.picklistId = context.picklistId

List items = []
if (reportResult.picklistInfo && reportResult.picklistInfo.picklistBinInfoList) {
reportResult.picklistInfo.picklistBinInfoList.each { binInfo ->
if (binInfo.picklistItemInfoList) {
binInfo.picklistItemInfoList.each { itemInfo ->
items << [
picklistBinId: binInfo.picklistBin?.picklistBinId,
orderId: itemInfo.orderItem?.orderId,
orderItemSeqId: itemInfo.orderItem?.orderItemSeqId,
shipGroupSeqId: itemInfo.picklistItem?.shipGroupSeqId,
inventoryItemId: itemInfo.picklistItem?.inventoryItemId,
itemStatusId: itemInfo.picklistItem?.itemStatusId,
productId: itemInfo.product?.productId,
productName: itemInfo.product?.internalName,
quantity: itemInfo.picklistItem?.quantity,
locationSeqId: itemInfo.inventoryItemAndLocation?.locationSeqId,
area: itemInfo.inventoryItemAndLocation?.areaId,
aisle: itemInfo.inventoryItemAndLocation?.aisleId,
section: itemInfo.inventoryItemAndLocation?.sectionId,
level: itemInfo.inventoryItemAndLocation?.levelId
]
}
}
}
}

result.picklistDetails = picklistDetails
result.picklistDetails.items = items
return result
}

Map getPickingPicklists() {
Map result = ServiceUtil.returnSuccess()
String facilityId = context.facilityId
String statusId = context.statusId
String searchQuery = context.searchQuery

// Query picklists
List conds = [EntityCondition.makeCondition('facilityId', EntityOperator.EQUALS, facilityId)]

// Status Filter:
// If statusId is "COMPLETED", show PICKLIST_PICKED and PICKLIST_CANCELLED.
// If statusId is "ALL", show all statuses.
// If statusId is empty (default), show active picklists only: PICKLIST_INPUT and PICKLIST_PRINTED.
// Otherwise, match the statusId directly.
if (statusId == 'COMPLETED') {
conds.add(EntityCondition.makeCondition('statusId', EntityOperator.IN, ['PICKLIST_PICKED', 'PICKLIST_CANCELLED']))
} else if (statusId && statusId != 'ALL') {
conds.add(EntityCondition.makeCondition('statusId', EntityOperator.EQUALS, statusId))
} else if (!statusId || statusId == '') {
conds.add(EntityCondition.makeCondition('statusId', EntityOperator.IN, ['PICKLIST_INPUT', 'PICKLIST_PRINTED']))
}

// Search Query:
if (searchQuery && searchQuery.trim() != '') {
String q = searchQuery.trim()
Set<String> searchPicklistIds = [] as Set

// 1. Search by picklistId directly
List picklistsById = from('Picklist')
.where(EntityCondition.makeCondition('picklistId', EntityOperator.LIKE, '%' + q + '%'))
.queryList()
picklistsById.each { searchPicklistIds.add(it.picklistId) }

// 2. Search by primaryOrderId (Order ID)
List binsByOrder = from('PicklistBin')
.where(EntityCondition.makeCondition('primaryOrderId', EntityOperator.LIKE, '%' + q + '%'))
.queryList()
binsByOrder.each { searchPicklistIds.add(it.picklistId) }

if (searchPicklistIds) {
conds.add(EntityCondition.makeCondition('picklistId', EntityOperator.IN, searchPicklistIds))
} else {
// Force empty results if query doesn't match anything
result.picklistList = []
return result
}
}

org.apache.ofbiz.entity.util.EntityQuery query = from('Picklist')
.where(EntityCondition.makeCondition(conds, EntityOperator.AND))
.orderBy('picklistDate DESC')

// Cap at 20 rows if NOT searching
if (!searchQuery || searchQuery.trim() == '') {
query = query.maxRows(20)
}

List picklists = query.queryList()

List picklistList = []
picklists.each { picklist ->
// Find bins
List bins = from('PicklistBin').where('picklistId', picklist.picklistId).queryList()
int totalOrders = bins.size()

// Find items
int totalItems = 0
int pickedItems = 0
bins.each { bin ->
List items = from('PicklistItem').where('picklistBinId', bin.picklistBinId).queryList()
items.each { item ->
totalItems += item.quantity ?: 0
if (item.itemStatusId == 'PICKITEM_COMPLETED') {
pickedItems += item.quantity ?: 0
}
}
}

picklistList << [
picklistId: picklist.picklistId,
statusId: picklist.statusId,
picklistDate: picklist.picklistDate,
totalOrders: totalOrders,
totalItems: totalItems,
pickedItems: pickedItems
]
}

result.picklistList = picklistList
return result
}

Map cancelPickingPicklist() {
Map result = ServiceUtil.returnSuccess()
runService('updatePicklist', [
picklistId: context.picklistId,
statusId: 'PICKLIST_CANCELLED',
userLogin: context.userLogin
])
return result
}

Map getPickingPicklistPdf() {
Map result = ServiceUtil.returnSuccess()
String picklistId = context.picklistId

// Retrieve picklist and transition status
GenericValue picklist = from('Picklist').where('picklistId', picklistId).queryOne()
if (!picklist) {
return ServiceUtil.returnError('Picklist not found with ID: ' + picklistId)
}

if (picklist.statusId == 'PICKLIST_INPUT') {
runService('updatePicklist', [
picklistId: picklistId,
statusId: 'PICKLIST_PRINTED',
userLogin: context.userLogin
])
}

// Programmatic FO rendering
VisualTheme visualTheme = ThemeFactory.resolveVisualTheme(null)
MapStack screenContext = MapStack.create()
screenContext.put('locale', context.locale ?: Locale.getDefault())
screenContext.put('picklistId', picklistId)
screenContext.put('userLogin', context.userLogin)

// Set up parameters map for screens and actions
Map parameters = [:]
parameters.put('picklistId', picklistId)
parameters.put('userLogin', context.userLogin)
screenContext.put('parameters', parameters)
// Render the report screen to FO string
StringWriter writer = new StringWriter()
MacroScreenRenderer foScreenStringRenderer = new MacroScreenRenderer(
visualTheme.getModelTheme().getType('screenfop'),
visualTheme.getModelTheme().getScreenRendererLocation('screenfop')
)
ScreenRenderer screens = new ScreenRenderer(writer, screenContext, foScreenStringRenderer)
screens.populateContextForService(dctx, screenContext)
screens.getContext().put('formStringRenderer', new FoFormRenderer())
screens.render('component://product/widget/facility/FacilityScreens.xml#PicklistReport.fo')

// Convert FO String to PDF byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream()
StreamSource src = new StreamSource(new StringReader(writer.toString()))
Fop fop = ApacheFopWorker.createFopInstance(baos, 'application/pdf')
ApacheFopWorker.transform(src, null, fop)

baos.flush()
byte[] pdfBytes = baos.toByteArray()
baos.close()

result.pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes)
return result
}

2 changes: 1 addition & 1 deletion applications/product/template/facility/Picklist.fo.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ under the License.
<fo:page-sequence master-reference="main">
<fo:flow flow-name="xsl-region-body" font-family="Helvetica">

<#if security.hasEntityPermission("FACILITY", "_VIEW", session)>
<#if security.hasEntityPermission("FACILITY", "_VIEW", userLogin)>

<#if picklistInfo?has_content>
<fo:block font-size="12pt">${uiLabelMap.ProductPickList} ${picklistInfo.picklist.picklistId} ${uiLabelMap.CommonIn} ${uiLabelMap.ProductFacility} ${picklistInfo.facility.facilityName} <fo:inline font-size="8pt">[${picklistInfo.facility.facilityId}]</fo:inline></fo:block>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;

import org.apache.catalina.filters.CorsFilter;
Expand All @@ -42,11 +44,18 @@

@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class APICorsFilter implements ContainerResponseFilter {
public class APICorsFilter implements ContainerRequestFilter, ContainerResponseFilter {

// check security.properties file for 'cors.origins.allowed'
private static final List<String> ALLOWED_CORS_ORIGINS = UtilMisc.getCorsOriginsAllowed();

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
if (HttpMethod.OPTIONS.equalsIgnoreCase(requestContext.getMethod())) {
requestContext.abortWith(Response.ok().build());
}
}

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
Expand All @@ -69,7 +78,7 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont

// publish supported request header field names
responseHeaders.addAll(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
Arrays.asList(HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION));
Arrays.asList(HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION, "Refresh-Token"));

// inform about all the supported methods. Itemize these due to the lack of support for the wildcard (*)
// in few browsers, e.g. in 'Safari' resp. 'FF for Android'
Expand Down
Loading