Skip to content

Commit ff2f717

Browse files
Mathias CoustéMathias Cousté
authored andcommitted
1.170 : add odoo product search for ngleaf widget
1 parent fb09122 commit ff2f717

13 files changed

Lines changed: 174 additions & 30 deletions

File tree

demo/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<groupId>fr.io-labs</groupId>
1515
<artifactId>leaf-demo</artifactId>
16-
<version>1.169-SNAPSHOT</version>
16+
<version>1.170-SNAPSHOT</version>
1717
<name>Leaf - demo</name>
1818

1919
<properties>
@@ -22,7 +22,7 @@
2222
<maven.compiler.target>17</maven.compiler.target>
2323
<maven.compiler.release>17</maven.compiler.release>
2424
<spring.version>2.7.18</spring.version>
25-
<leaf.version>1.169-SNAPSHOT</leaf.version>
25+
<leaf.version>1.170-SNAPSHOT</leaf.version>
2626
</properties>
2727

2828
<dependencies>

library/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>fr.io-labs</groupId>
66
<artifactId>leaf</artifactId>
7-
<version>1.169-SNAPSHOT</version>
7+
<version>1.170-SNAPSHOT</version>
88
<name>leaf</name>
99

1010
<properties>

library/src/main/java/fr/iolabs/leaf/organization/LeafOrganizationController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ public List<LeafOrganization> listOrganizations(
5454
@CrossOrigin
5555
@AdminOnly
5656
@PostMapping("/search")
57-
public OrganizationSearchResponse searchOrganizations(@RequestBody OrganizationSearchCriteria criteria) {
58-
return this.organizationService.search(criteria);
57+
public OrganizationSearchResponse searchOrganizations(
58+
@RequestBody(required = false) OrganizationSearchCriteria criteria) {
59+
return this.organizationService.search(criteria != null ? criteria : new OrganizationSearchCriteria());
5960
}
6061

6162
@CrossOrigin

library/src/main/java/fr/iolabs/leaf/organization/LeafOrganizationService.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,20 @@ public OrganizationSearchResponse search(OrganizationSearchCriteria criteria) {
142142
criteria = new OrganizationSearchCriteria();
143143
}
144144

145-
Query query = new Query();
145+
Query countQuery = new Query();
146146
if (Strings.isNotBlank(criteria.getName())) {
147-
query.addCriteria(Criteria.where("name").regex(Pattern.quote(criteria.getName().trim()), "i"));
147+
countQuery.addCriteria(Criteria.where("name").regex(Pattern.quote(criteria.getName().trim()), "i"));
148148
}
149149

150-
long totalCount = this.mongoTemplate.count(query, LeafOrganization.class);
150+
long totalCount = this.mongoTemplate.count(countQuery, LeafOrganization.class);
151151

152-
query.with(this.resolveSort(criteria.getOrderBy()));
153-
query.with(PageRequest.of(criteria.getPage(), criteria.getPageSize()));
152+
Query pageQuery = new Query();
153+
if (Strings.isNotBlank(criteria.getName())) {
154+
pageQuery.addCriteria(Criteria.where("name").regex(Pattern.quote(criteria.getName().trim()), "i"));
155+
}
156+
pageQuery.with(PageRequest.of(criteria.getPage(), criteria.getPageSize(), this.resolveSort(criteria.getOrderBy())));
154157

155-
List<LeafOrganization> organizations = this.mongoTemplate.find(query, LeafOrganization.class);
158+
List<LeafOrganization> organizations = this.mongoTemplate.find(pageQuery, LeafOrganization.class);
156159
int pageCount = (int) Math.ceil((double) totalCount / criteria.getPageSize());
157160

158161
return new OrganizationSearchResponse(organizations, totalCount, pageCount, criteria.getPage());
Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
package fr.iolabs.leaf.organization;
22

3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
35
public enum OrganizationSearchOrder {
46
CREATION_DATE,
5-
NAME
7+
NAME;
8+
9+
@JsonCreator
10+
public static OrganizationSearchOrder fromValue(String value) {
11+
if (value == null || value.isBlank()) {
12+
return NAME;
13+
}
14+
String normalized = value.trim();
15+
for (OrganizationSearchOrder order : values()) {
16+
if (order.name().equalsIgnoreCase(normalized)) {
17+
return order;
18+
}
19+
}
20+
if ("creationdate".equalsIgnoreCase(normalized.replace("_", ""))) {
21+
return CREATION_DATE;
22+
}
23+
return NAME;
24+
}
625
}

messenger/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>fr.io-labs</groupId>
55
<artifactId>leaf-messenger</artifactId>
6-
<version>1.169-SNAPSHOT</version>
6+
<version>1.170-SNAPSHOT</version>
77
<name>leaf-messenger</name>
88

99
<properties>
@@ -12,7 +12,7 @@
1212
<maven.compiler.target>17</maven.compiler.target>
1313
<maven.compiler.release>17</maven.compiler.release>
1414
<spring.version>2.7.18</spring.version>
15-
<leaf.version>1.169-SNAPSHOT</leaf.version>
15+
<leaf.version>1.170-SNAPSHOT</leaf.version>
1616
</properties>
1717

1818
<dependencies>

odoo-integration/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>fr.io-labs</groupId>
66
<artifactId>leaf-odoo-integration</artifactId>
7-
<version>1.169-SNAPSHOT</version>
7+
<version>1.170-SNAPSHOT</version>
88
<name>leaf-odoo-integration</name>
99

1010
<properties>
@@ -13,7 +13,7 @@
1313
<maven.compiler.target>17</maven.compiler.target>
1414
<maven.compiler.release>17</maven.compiler.release>
1515
<spring.version>2.7.18</spring.version>
16-
<leaf.version>1.169-SNAPSHOT</leaf.version>
16+
<leaf.version>1.170-SNAPSHOT</leaf.version>
1717
</properties>
1818

1919
<dependencies>

odoo-integration/src/main/java/fr/iolabs/leaf/odoo/LeafOdooController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import fr.iolabs.leaf.odoo.opportunity.OdooOpportunityService;
77
import fr.iolabs.leaf.odoo.invoice.OdooInvoice;
88
import fr.iolabs.leaf.odoo.invoice.OdooInvoiceService;
9+
import fr.iolabs.leaf.odoo.product.OdooProduct;
10+
import fr.iolabs.leaf.odoo.product.OdooProductQueryService;
911
import fr.iolabs.leaf.odoo.quote.OdooQuote;
1012
import fr.iolabs.leaf.odoo.quote.OdooQuoteService;
1113
import java.time.ZonedDateTime;
@@ -37,6 +39,9 @@ public class LeafOdooController {
3739
@Autowired
3840
private OdooInvoiceService odooInvoiceService;
3941

42+
@Autowired
43+
private OdooProductQueryService odooProductQueryService;
44+
4045
@CrossOrigin
4146
@GetMapping("/contacts")
4247
public List<OdooContact> listContacts(@RequestParam(name = "limit", required = false) Integer limit) {
@@ -57,6 +62,13 @@ public List<OdooOpportunity> listOpportunities(
5762
return this.odooOpportunityService.listOpportunities(credentials, limit);
5863
}
5964

65+
@CrossOrigin
66+
@GetMapping("/products")
67+
public List<OdooProduct> listProducts(@RequestParam(name = "limit", required = false) Integer limit) {
68+
OdooCredentials credentials = this.odooCredentialsResolver.resolveForApi();
69+
return this.odooProductQueryService.listProducts(credentials, limit);
70+
}
71+
6072
@CrossOrigin
6173
@GetMapping("/quotes/signed")
6274
public List<OdooQuote> listQuotesSignedBetween(

odoo-integration/src/main/java/fr/iolabs/leaf/odoo/product/OdooProduct.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ public class OdooProduct {
44

55
private Integer id;
66
private String name;
7+
private String defaultCode;
78
private Double standardPrice;
9+
private String imageUrl;
810

911
public Integer getId() {
1012
return id;
@@ -22,6 +24,14 @@ public void setName(String name) {
2224
this.name = name;
2325
}
2426

27+
public String getDefaultCode() {
28+
return defaultCode;
29+
}
30+
31+
public void setDefaultCode(String defaultCode) {
32+
this.defaultCode = defaultCode;
33+
}
34+
2535
public Double getStandardPrice() {
2636
return standardPrice;
2737
}
@@ -30,4 +40,12 @@ public void setStandardPrice(Double standardPrice) {
3040
this.standardPrice = standardPrice;
3141
}
3242

43+
public String getImageUrl() {
44+
return imageUrl;
45+
}
46+
47+
public void setImageUrl(String imageUrl) {
48+
this.imageUrl = imageUrl;
49+
}
50+
3351
}

odoo-integration/src/main/java/fr/iolabs/leaf/odoo/product/OdooProductQueryService.java

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package fr.iolabs.leaf.odoo.product;
22

33
import fr.iolabs.leaf.odoo.OdooCredentials;
4+
import fr.iolabs.leaf.odoo.OdooCredentialsResolver;
45
import fr.iolabs.leaf.odoo.OdooIntegrationException;
56
import fr.iolabs.leaf.odoo.rpc.OdooRpcClient;
67
import fr.iolabs.leaf.odoo.rpc.OdooValueMapper;
78
import java.util.ArrayList;
89
import java.util.Arrays;
10+
import java.util.Base64;
911
import java.util.LinkedHashMap;
1012
import java.util.List;
1113
import java.util.Map;
@@ -21,10 +23,44 @@ public class OdooProductQueryService {
2123
private static final Logger LOGGER = LoggerFactory.getLogger(OdooProductQueryService.class);
2224
private static final String PRODUCT_MODEL = "product.product";
2325
private static final int ID_CHUNK_SIZE = 100;
26+
private static final int DEFAULT_LIMIT = 50;
27+
private static final int MAX_LIMIT = 200;
2428

2529
@Autowired
2630
private OdooRpcClient odooRpcClient;
2731

32+
@Autowired
33+
private OdooCredentialsResolver odooCredentialsResolver;
34+
35+
public List<OdooProduct> listProducts(Integer limit) {
36+
return this.listProducts(this.odooCredentialsResolver.resolveForApi(), limit);
37+
}
38+
39+
public List<OdooProduct> listProducts(OdooCredentials credentials, Integer limit) {
40+
int safeLimit = sanitizeLimit(limit);
41+
try {
42+
int uid = this.odooRpcClient.authenticate(credentials);
43+
Set<String> availableFields =
44+
OdooValueMapper.toFieldNames(this.odooRpcClient.fieldsGet(credentials, uid, PRODUCT_MODEL));
45+
List<String> fields = this.resolveProductFields(availableFields, true);
46+
List<Object> domain = List.of(List.of("active", "=", true));
47+
List<Map<String, Object>> rows =
48+
this.odooRpcClient.searchRead(credentials, uid, PRODUCT_MODEL, domain, fields, safeLimit, "name asc");
49+
List<OdooProduct> products = new ArrayList<>();
50+
for (Map<String, Object> row : rows) {
51+
products.add(this.mapProduct(row, credentials, true));
52+
}
53+
LOGGER.debug("Listed {} Odoo products", products.size());
54+
return products;
55+
} catch (OdooIntegrationException exception) {
56+
LOGGER.error("Failed to list Odoo products", exception);
57+
throw exception;
58+
} catch (RuntimeException exception) {
59+
LOGGER.error("Failed to list Odoo products", exception);
60+
throw new OdooIntegrationException("Failed to list Odoo products: " + exception.getMessage(), exception);
61+
}
62+
}
63+
2864
public Map<Integer, OdooProduct> findByIds(OdooCredentials credentials, int uid, Set<Integer> productIds) {
2965
if (productIds == null || productIds.isEmpty()) {
3066
return Map.of();
@@ -33,7 +69,7 @@ public Map<Integer, OdooProduct> findByIds(OdooCredentials credentials, int uid,
3369
try {
3470
Set<String> availableFields =
3571
OdooValueMapper.toFieldNames(this.odooRpcClient.fieldsGet(credentials, uid, PRODUCT_MODEL));
36-
List<String> fields = this.resolveProductFields(availableFields);
72+
List<String> fields = this.resolveProductFields(availableFields, false);
3773
List<Integer> ids = new ArrayList<>(productIds);
3874
Map<Integer, OdooProduct> productsById = new LinkedHashMap<>();
3975

@@ -43,7 +79,7 @@ public Map<Integer, OdooProduct> findByIds(OdooCredentials credentials, int uid,
4379
List<Map<String, Object>> rows =
4480
this.odooRpcClient.searchRead(credentials, uid, PRODUCT_MODEL, domain, fields, chunk.size(), 0, "id asc");
4581
for (Map<String, Object> row : rows) {
46-
OdooProduct product = this.mapProduct(row);
82+
OdooProduct product = this.mapProduct(row, credentials, false);
4783
if (product.getId() != null) {
4884
productsById.put(product.getId(), product);
4985
}
@@ -61,27 +97,82 @@ public Map<Integer, OdooProduct> findByIds(OdooCredentials credentials, int uid,
6197
}
6298
}
6399

64-
private List<String> resolveProductFields(Set<String> availableFields) {
100+
private List<String> resolveProductFields(Set<String> availableFields, boolean includeImage) {
65101
List<String> fields = new ArrayList<>(List.of("id", "name"));
66-
if (availableFields.contains("standard_price")) {
67-
fields.add("standard_price");
102+
for (String candidate : Arrays.asList("display_name", "default_code", "standard_price")) {
103+
if (availableFields.contains(candidate)) {
104+
fields.add(candidate);
105+
}
106+
}
107+
if (includeImage && availableFields.contains("image_128")) {
108+
fields.add("image_128");
68109
}
69110
return fields;
70111
}
71112

72-
private OdooProduct mapProduct(Map<String, Object> row) {
113+
private OdooProduct mapProduct(Map<String, Object> row, OdooCredentials credentials, boolean includeImage) {
73114
OdooProduct product = new OdooProduct();
74-
product.setId(OdooValueMapper.asInteger(row.get("id")));
75-
product.setName(OdooValueMapper.asString(row.get("name")));
115+
Integer id = OdooValueMapper.asInteger(row.get("id"));
116+
product.setId(id);
117+
String displayName = OdooValueMapper.asString(row.get("display_name"));
118+
product.setName(displayName != null ? displayName : OdooValueMapper.asString(row.get("name")));
119+
product.setDefaultCode(OdooValueMapper.asString(row.get("default_code")));
76120
product.setStandardPrice(this.asDouble(row.get("standard_price")));
121+
if (includeImage) {
122+
product.setImageUrl(this.resolveImageUrl(credentials, id, row.get("image_128")));
123+
}
77124
return product;
78125
}
79126

127+
private String resolveImageUrl(OdooCredentials credentials, Integer productId, Object image128) {
128+
if (productId == null || !hasImage(image128)) {
129+
return null;
130+
}
131+
String base64 = asImageBase64(image128);
132+
if (base64 != null) {
133+
return "data:image/png;base64," + base64;
134+
}
135+
return credentials.getUrl() + "/web/image/product.product/" + productId + "/image_128";
136+
}
137+
138+
private boolean hasImage(Object value) {
139+
if (value == null || Boolean.FALSE.equals(value)) {
140+
return false;
141+
}
142+
if (value instanceof byte[] bytes) {
143+
return bytes.length > 0;
144+
}
145+
String text = value.toString().trim();
146+
return !text.isEmpty() && !"false".equalsIgnoreCase(text) && !"0".equals(text) && !"0 bytes".equalsIgnoreCase(text);
147+
}
148+
149+
private String asImageBase64(Object value) {
150+
if (value instanceof byte[] bytes && bytes.length > 0) {
151+
return Base64.getEncoder().encodeToString(bytes);
152+
}
153+
if (!(value instanceof String text)) {
154+
return null;
155+
}
156+
String trimmed = text.trim().replaceAll("\\s+", "");
157+
if (trimmed.isEmpty() || trimmed.matches("(?i)\\d+(\\.\\d+)?[kmg]?b")) {
158+
return null;
159+
}
160+
return trimmed;
161+
}
162+
80163
private Double asDouble(Object value) {
81164
if (value instanceof Number number) {
82165
return number.doubleValue();
83166
}
84167
return null;
85168
}
86169

170+
private int sanitizeLimit(Integer limit) {
171+
int value = limit != null ? limit : DEFAULT_LIMIT;
172+
if (value <= 0) {
173+
return DEFAULT_LIMIT;
174+
}
175+
return Math.min(value, MAX_LIMIT);
176+
}
177+
87178
}

0 commit comments

Comments
 (0)