11package fr .iolabs .leaf .odoo .product ;
22
33import fr .iolabs .leaf .odoo .OdooCredentials ;
4+ import fr .iolabs .leaf .odoo .OdooCredentialsResolver ;
45import fr .iolabs .leaf .odoo .OdooIntegrationException ;
56import fr .iolabs .leaf .odoo .rpc .OdooRpcClient ;
67import fr .iolabs .leaf .odoo .rpc .OdooValueMapper ;
78import java .util .ArrayList ;
89import java .util .Arrays ;
10+ import java .util .Base64 ;
911import java .util .LinkedHashMap ;
1012import java .util .List ;
1113import 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