8
8
import threading
9
9
import urllib .parse
10
10
from datetime import datetime , timedelta
11
- from io import BytesIO
12
11
from json import JSONDecodeError
13
12
14
13
import requests
15
- from PIL import Image
14
+ from typing_extensions import deprecated
16
15
17
16
from odoo .exceptions import UserError
18
17
33
32
}
34
33
35
34
35
+ def sanitize_string (value , mapping = None ):
36
+ """Remove disallowed characters ("|", "\" , "<", ">", "’", "‘") from a string
37
+
38
+ :param value: string to sanitize
39
+ :param mapping: dict of disallowed characters to remove
40
+ :return: sanitized string
41
+
42
+ """
43
+ mapping = mapping or DISALLOWED_CHARS_MAPPING
44
+ value = value or ""
45
+ for char , repl in mapping .items ():
46
+ value = value .replace (char , repl )
47
+ return value
48
+
49
+
36
50
class PostlogisticsWebService :
37
51
"""Connector with PostLogistics for labels using post.ch API
38
52
@@ -71,25 +85,21 @@ def _get_language(self, lang):
71
85
return lang_code
72
86
return "en"
73
87
74
- def _prepare_recipient (self , picking ):
88
+ def _prepare_recipient (self , picking , sanitize_mapping = None ):
75
89
"""Create a ns0:Recipient as a dict from a partner
76
90
77
91
:param partner: partner browse record
78
92
:return a dict containing data for ns0:Recipient
79
93
80
94
"""
81
- partner = picking .partner_id
82
- if picking .picking_type_id .code != "outgoing" :
83
- location_dest = picking .location_dest_id
84
- partner = (
85
- location_dest .company_id .partner_id
86
- or self .env .user .company_id .partner_id
87
- )
95
+ partner = picking .get_recipient_partner_hook ()
88
96
89
- partner_mobile = self ._sanitize_string (
90
- picking .delivery_mobile or partner .mobile
97
+ partner_mobile = sanitize_string (
98
+ picking .delivery_mobile or partner .mobile , sanitize_mapping
99
+ )
100
+ partner_phone = sanitize_string (
101
+ picking .delivery_phone or partner .phone , sanitize_mapping
91
102
)
92
- partner_phone = self ._sanitize_string (picking .delivery_phone or partner .phone )
93
103
94
104
if partner .postlogistics_notification == "email" and not partner .email :
95
105
raise UserError (picking .env ._ ("Email is required for notification." ))
@@ -115,10 +125,10 @@ def _prepare_recipient(self, picking):
115
125
raise UserError (picking .env ._ ("Partner city is required." ))
116
126
117
127
partner_name = partner .name or partner .parent_id .name
118
- sanitized_partner_name = self . _sanitize_string (partner_name )
119
- partner_street = self . _sanitize_string (partner .street )
120
- partner_zip = self . _sanitize_string (partner .zip )
121
- partner_city = self . _sanitize_string (partner .city )
128
+ sanitized_partner_name = sanitize_string (partner_name , sanitize_mapping )
129
+ partner_street = sanitize_string (partner .street , sanitize_mapping )
130
+ partner_zip = sanitize_string (partner .zip , sanitize_mapping )
131
+ partner_city = sanitize_string (partner .city , sanitize_mapping )
122
132
recipient = {
123
133
"name1" : sanitized_partner_name [:35 ],
124
134
"street" : partner_street [:35 ],
@@ -127,66 +137,37 @@ def _prepare_recipient(self, picking):
127
137
}
128
138
129
139
if partner .country_id .code :
130
- country_code = self ._sanitize_string (partner .country_id .code .upper ())
140
+ country_code = sanitize_string (
141
+ partner .country_id .code .upper (), sanitize_mapping
142
+ )
131
143
recipient ["country" ] = country_code
132
144
133
145
if partner .street2 :
134
146
# addressSuffix is shown before street on label
135
147
recipient ["addressSuffix" ] = recipient ["street" ]
136
- recipient ["street" ] = self ._sanitize_string (partner .street2 [:35 ])
148
+ recipient ["street" ] = sanitize_string (
149
+ partner .street2 [:35 ], sanitize_mapping
150
+ )
137
151
138
152
company_partner_name = partner .commercial_company_name
139
153
if company_partner_name and company_partner_name != partner_name :
140
- parent_name = self . _sanitize_string (partner .parent_id .name )
154
+ parent_name = sanitize_string (partner .parent_id .name , sanitize_mapping )
141
155
recipient ["name2" ] = parent_name [:35 ]
142
156
recipient ["personallyAddressed" ] = False
143
157
144
158
# Phone and / or mobile should only be displayed if instruction to
145
159
# Notify delivery by telephone is set
146
160
if partner .postlogistics_notification == "email" :
147
- recipient ["email" ] = self . _sanitize_string (partner .email )
161
+ recipient ["email" ] = sanitize_string (partner .email , sanitize_mapping )
148
162
elif partner .postlogistics_notification == "phone" :
149
- recipient ["phone" ] = self . _sanitize_string (partner_phone )
163
+ recipient ["phone" ] = sanitize_string (partner_phone , sanitize_mapping )
150
164
if partner_mobile :
151
165
recipient ["mobile" ] = partner_mobile
152
166
elif partner .postlogistics_notification == "sms" :
153
167
recipient ["mobile" ] = partner_mobile
154
168
155
169
return recipient
156
170
157
- def _prepare_customer (self , picking ):
158
- """Create a ns0:Customer as a dict from picking
159
-
160
- This is the PostLogistics Customer, thus the sender
161
-
162
- :param picking: picking browse record
163
- :return a dict containing data for ns0:Customer
164
-
165
- """
166
- company = picking .company_id
167
- partner = company .partner_id
168
- if picking .picking_type_id .code != "outgoing" :
169
- partner = picking .partner_id
170
-
171
- partner_name = partner .name or partner .parent_id .name
172
- if not partner_name :
173
- raise UserError (picking .env ._ ("Customer name is required." ))
174
- customer = {
175
- "name1" : self ._sanitize_string (partner_name )[:25 ],
176
- "street" : self ._sanitize_string (partner .street )[:25 ],
177
- "zip" : self ._sanitize_string (partner .zip )[:10 ],
178
- "city" : self ._sanitize_string (partner .city )[:25 ],
179
- "country" : partner .country_id .code ,
180
- "domicilePostOffice" : picking .carrier_id .postlogistics_office or None ,
181
- }
182
- logo = picking .carrier_id .postlogistics_logo
183
- if logo :
184
- logo_image = Image .open (BytesIO (base64 .b64decode (logo )))
185
- logo_format = logo_image .format
186
- customer ["logo" ] = logo .decode ()
187
- customer ["logoFormat" ] = logo_format
188
- return customer
189
-
190
171
def _get_label_layout (self , picking ):
191
172
"""
192
173
Get Label layout define in carrier
@@ -215,66 +196,6 @@ def _get_license(self, picking):
215
196
franking_license = picking .carrier_id .postlogistics_license_id
216
197
return franking_license .number
217
198
218
- def _prepare_attributes (
219
- self , picking , pack = None , pack_num = None , pack_total = None , pack_weight = None
220
- ):
221
- package_type = (
222
- pack
223
- and pack .package_type_id
224
- or picking .carrier_id .postlogistics_default_package_type_id
225
- )
226
- package_codes = package_type ._get_shipper_package_code_list ()
227
-
228
- if pack_weight :
229
- total_weight = pack_weight
230
- else :
231
- total_weight = pack .shipping_weight if pack else picking .shipping_weight
232
- total_weight *= 1000
233
-
234
- if not package_codes :
235
- raise UserError (
236
- picking .env ._ (
237
- "No PostLogistics packaging services found "
238
- "in package type {package_type_name}, for picking {picking_name}."
239
- ).format (package_type_name = package_type .name , picking_name = picking .name )
240
- )
241
-
242
- # Activate phone notification ZAW3213
243
- # if phone call notification is set on partner
244
- if picking .partner_id .postlogistics_notification == "phone" :
245
- package_codes .append ("ZAW3213" )
246
-
247
- attributes = {
248
- "weight" : int (total_weight ),
249
- }
250
-
251
- # Remove the services if the delivery fixed date is not set
252
- if "ZAW3217" in package_codes :
253
- if picking .delivery_fixed_date :
254
- attributes ["deliveryDate" ] = picking .delivery_fixed_date
255
- else :
256
- package_codes .remove ("ZAW3217" )
257
-
258
- # parcelNo / parcelTotal cannot be used if service ZAW3218 is not activated
259
- if "ZAW3218" in package_codes :
260
- if pack_total > 1 :
261
- attributes .update (
262
- {"parcelTotal" : pack_total - 1 , "parcelNo" : pack_num - 1 }
263
- )
264
- else :
265
- package_codes .remove ("ZAW3218" )
266
-
267
- if "ZAW3219" in package_codes and picking .delivery_place :
268
- attributes ["deliveryPlace" ] = picking .delivery_place
269
- if picking .carrier_id .postlogistics_proclima_logo :
270
- attributes ["proClima" ] = True
271
- else :
272
- attributes ["proClima" ] = False
273
-
274
- attributes ["przl" ] = package_codes
275
-
276
- return attributes
277
-
278
199
def _get_itemid (self , picking , package ):
279
200
"""Allowed characters are alphanumeric plus `+`, `-` and `_`
280
201
Last `+` separates picking name and package number (if any)
@@ -290,28 +211,21 @@ def _get_itemid(self, picking, package):
290
211
codes = [name , pack_no ]
291
212
return "+" .join (c for c in codes if c )
292
213
293
- def _cash_on_delivery (self , picking , package = None ):
294
- amount = (package or picking ).postlogistics_cod_amount ()
295
- amount = f"{ amount :.2f} "
296
- return [{"Type" : "NN_BETRAG" , "Value" : amount }]
297
-
298
- def _get_item_additional_data (self , picking , package = None ):
299
- if package and not package .package_type_id :
300
- raise UserError (
301
- picking .env ._ ("The package %s must have a package type." ) % package .name
302
- )
303
-
304
- result = []
305
- packaging_codes = (
306
- package and package .package_type_id ._get_shipper_package_code_list () or []
307
- )
308
-
309
- if set (packaging_codes ) & {"BLN" , "N" }:
310
- cod_attributes = self ._cash_on_delivery (picking , package = package )
311
- result += cod_attributes
312
- return result
214
+ def _prepare_data (
215
+ self , lang , frankingLicense , post_customer , labelDefinition , item
216
+ ):
217
+ return {
218
+ "language" : lang .upper (),
219
+ "frankingLicense" : frankingLicense ,
220
+ "ppFranking" : False ,
221
+ "customer" : post_customer ,
222
+ "customerSystem" : None ,
223
+ "labelDefinition" : labelDefinition ,
224
+ "sendingID" : None ,
225
+ "item" : item ,
226
+ }
313
227
314
- def _get_item_number (self , picking , package ):
228
+ def _get_item_number (self , picking , package , index = 1 ):
315
229
"""Generate the tracking reference for the last 8 digits
316
230
of tracking number of the label.
317
231
@@ -321,15 +235,17 @@ def _get_item_number(self, picking, package):
321
235
e.g. 03000042 for 3rd pack of picking OUT/19000042
322
236
"""
323
237
picking_num = _compile_itemnum .sub ("" , picking .name )
324
- package_number = self .get_package_number_hook (package )
238
+ package_number = picking .get_package_number_hook (package )
239
+ if not package_number :
240
+ package_number = index
325
241
return "%02d%s" % (package_number , picking_num [- 6 :].zfill (6 ))
326
242
327
243
def _prepare_item_list (self , picking , recipient , packages ):
328
244
"""Return a list of item made from the pickings"""
329
245
carrier = picking .carrier_id
330
246
item_list = []
331
247
332
- def add_item (package_number = 1 , package = None ):
248
+ def add_item (index = 1 , package = None ):
333
249
assert picking or package
334
250
itemid = self ._get_itemid (picking , package )
335
251
item = {
@@ -344,7 +260,7 @@ def add_item(package_number=1, package=None):
344
260
picking_num = _compile_itemnum .sub ("" , picking .name )
345
261
item_number = f"9{ picking_num [- 7 :].zfill (7 )} "
346
262
else :
347
- item_number = self ._get_item_number (picking , package_number )
263
+ item_number = self ._get_item_number (picking , package , index )
348
264
item ["itemNumber" ] = item_number
349
265
350
266
additional_data = self ._get_item_additional_data (picking , package = package )
@@ -354,8 +270,10 @@ def add_item(package_number=1, package=None):
354
270
item_list .append (item )
355
271
356
272
total_packages = len (packages )
357
- for package in packages :
358
- package_number = self .get_package_number_hook (package )
273
+ for index , package in enumerate (packages ):
274
+ package_number = picking .get_package_number_hook (package )
275
+ if not package_number :
276
+ package_number = index + 1
359
277
attributes = self ._prepare_attributes (
360
278
picking , package , package_number , total_packages
361
279
)
@@ -403,20 +321,6 @@ def _prepare_label_definition(self, picking):
403
321
"printPreview" : False ,
404
322
}
405
323
406
- def _prepare_data (
407
- self , lang , frankingLicense , post_customer , labelDefinition , item
408
- ):
409
- return {
410
- "language" : lang .upper (),
411
- "frankingLicense" : frankingLicense ,
412
- "ppFranking" : False ,
413
- "customer" : post_customer ,
414
- "customerSystem" : None ,
415
- "labelDefinition" : labelDefinition ,
416
- "sendingID" : None ,
417
- "item" : item ,
418
- }
419
-
420
324
@classmethod
421
325
def _request_access_token (cls , delivery_carrier ):
422
326
if not delivery_carrier .postlogistics_endpoint_url :
@@ -500,13 +404,6 @@ def get_access_token(cls, picking_carrier):
500
404
cls .access_token_expiry = now + timedelta (seconds = response ["expires_in" ])
501
405
return cls .access_token
502
406
503
- def _sanitize_string (self , value ):
504
- """Removes disallowed chars ("|", "\" , "<", ">", "’", "‘") from strings."""
505
- value = value or ""
506
- for char , repl in DISALLOWED_CHARS_MAPPING .items ():
507
- value = value .replace (char , repl )
508
- return value
509
-
510
407
def generate_label (self , picking , packages ):
511
408
"""Generate a label for a picking
512
409
@@ -601,6 +498,64 @@ def generate_label(self, picking, packages):
601
498
results .append (res )
602
499
return results
603
500
604
- def get_package_number_hook (self , package ):
605
- """Hook method to customize the package number retrieval"""
606
- return package
501
+ # These methods could be overridden in a custom module, thus if several
502
+ # modules are installed but not chained properly, the last one will be used.
503
+ # Meaning if you doesn't inherit from the last module, this module override
504
+ # will not be used. This is a big issue in a modular environment, thus we
505
+ # need to provide a better way to allow to override these methods by
506
+ # implementing them in the picking model.
507
+
508
+ @deprecated (
509
+ "This method will be removed in version > 18.0. Please use \
510
+ `stock.picking::postlogistics_label_cash_on_delivery` instead."
511
+ )
512
+ def _cash_on_delivery (self , picking , package = None ):
513
+ picking .postlogistics_label_cash_on_delivery (package = package )
514
+
515
+ @deprecated (
516
+ "This method will be removed in version > 18.0. Please use \
517
+ `stock.picking::postlogistics_label_get_item_additional_data` instead."
518
+ )
519
+ def _get_item_additional_data (self , picking , package = None ):
520
+ # TODO: remove this method in versions > 18.0 and reimplement current
521
+ # behavior inside stock.picking::postlogistics_label_get_item_additional_data
522
+ if package and not package .package_type_id :
523
+ raise UserError (
524
+ self .env ._ ("The package %s must have a package type." ) % package .name
525
+ )
526
+ result = picking .postlogistics_label_get_item_additional_data (package = package )
527
+ packaging_codes = (
528
+ package and package .package_type_id ._get_shipper_package_code_list () or []
529
+ )
530
+ if set (packaging_codes ) & {"BLN" , "N" }:
531
+ cod_attributes = self ._cash_on_delivery (picking , package = package )
532
+ result += cod_attributes
533
+ return result
534
+
535
+ @deprecated (
536
+ "This method will be removed in version > 18.0. \
537
+ Please use global `sanitize_string` instead."
538
+ )
539
+ def _sanitize_string (self , value , mapping = None ):
540
+ # TODO: remove this method in versions > 18.0
541
+ return sanitize_string (value , mapping )
542
+
543
+ @deprecated (
544
+ "This method will be removed in version > 18.0. Please use \
545
+ `stock.picking::postlogistics_label_prepare_attributes` instead."
546
+ )
547
+ def _prepare_attributes (
548
+ self , picking , pack = None , pack_num = None , pack_total = None , pack_weight = None
549
+ ):
550
+ # TODO: remove this method in versions > 18.0
551
+ return picking .postlogistics_label_prepare_attributes (
552
+ pack = pack , pack_num = pack_num , pack_total = pack_total , pack_weight = pack_weight
553
+ )
554
+
555
+ @deprecated (
556
+ "This method will be removed in version > 18.0. Please use \
557
+ `stock.picking::postlogistics_label_prepare_recipient` instead."
558
+ )
559
+ def _prepare_customer (self , picking ):
560
+ # TODO: remove this method in versions > 18.0
561
+ return picking .postlogistics_label_prepare_customer ()
0 commit comments