-
-
Notifications
You must be signed in to change notification settings - Fork 538
/
Copy pathdelivery_carrier.py
494 lines (477 loc) · 21.2 KB
/
delivery_carrier.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# Copyright 2020 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from xml.sax.saxutils import escape
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from .gls_asm_request import (
GLS_ASM_SERVICES,
GLS_DELIVERY_STATES_STATIC,
GLS_PICKUP_STATES_STATIC,
GLS_PICKUP_TYPE_STATES,
GLS_POSTAGE_TYPE,
GLS_SHIPMENT_TYPE_STATES,
GLS_SHIPPING_TIMES,
GlsAsmRequest,
)
class DeliveryCarrier(models.Model):
_inherit = "delivery.carrier"
delivery_type = fields.Selection(
selection_add=[("gls_asm", "GLS ASM")], ondelete={"gls_asm": "set default"}
)
gls_asm_uid = fields.Char(string="GLS UID")
gls_asm_service = fields.Selection(
selection=GLS_ASM_SERVICES,
string="GLS Service",
help="Set the contracted GLS Service",
default="1", # Courier
)
gls_asm_shiptime = fields.Selection(
selection=GLS_SHIPPING_TIMES,
string="Shipping Time",
help="Set the desired GLS shipping time for this carrier",
default="0", # 10h
)
gls_asm_postage_type = fields.Selection(
selection=GLS_POSTAGE_TYPE,
string="Postage Type",
help="Postage type, usually 'Prepaid'",
default="P",
)
gls_is_pickup_service = fields.Boolean(
string="Pick-up service",
help="Checked if this service is used for pickups",
compute="_compute_gls_pickup_service",
)
gls_asm_cash_on_delivery = fields.Boolean(
string="Cash on delivery",
help=(
"If checked, it means that the carrier is paid with cash. It assumes "
"there is a sale order linked and it will use that "
"total amount as the value to be paid"
),
)
@api.depends("gls_asm_service")
def _compute_gls_pickup_service(self):
for carrier in self:
carrier.gls_is_pickup_service = carrier.gls_asm_service in [
"7", # RECOGIDA
"8", # RECOGIDA CRUZADA
"17", # RECOGIDA SIN MERCANCIA
"39", # REC. INT
"45", # RECOGIDA MEN. MOTO
"46", # RECOGIDA MEN. FURGONETA
"47", # RECOGIDA MEN. F. GRANDE
"48", # RECGOIDA CAMIÓN
"49", # RECOGIDA MENSAJERO
"51", # REC. INT WW
"56", # RECOGIDA ECONOMY
"57", # REC. INTERCIUDAD ECONOMY
]
def _gls_asm_uid(self):
"""The carrier can be put in test mode. The tests user must be set.
A default given by GLS is put in the config parameter data"""
self.ensure_one()
uid = (
self.gls_asm_uid
if self.prod_environment
else self.env["ir.config_parameter"]
.sudo()
.get_param("delivery_gls_asm.api_user_demo", "")
)
return uid
def gls_asm_get_tracking_link(self, picking):
"""Provide tracking link for the customer"""
tracking_url = (
"http://www.asmred.com/extranet/public/"
"ExpedicionASM.aspx?codigo={}&cpDst={}"
)
return tracking_url.format(picking.carrier_tracking_ref, picking.partner_id.zip)
def _prepare_gls_asm_shipping(self, picking):
"""Convert picking values for asm api
:param picking record with picking to send
:returns dict values for the connector
"""
self.ensure_one()
# A picking can be delivered from any warehouse
sender_partner = (
picking.picking_type_id.warehouse_id.partner_id
or picking.company_id.partner_id
)
if not sender_partner.street:
raise UserError(_("Couldn't find the sender street"))
cash_amount = 0
if self.gls_asm_cash_on_delivery:
cash_amount = picking.sale_id.amount_total
return {
"fecha": fields.Date.today().strftime("%d/%m/%Y"),
"portes": self.gls_asm_postage_type,
"servicio": self.gls_asm_service,
"horario": self.gls_asm_shiptime,
"bultos": picking.number_of_packages,
"peso": round(picking.shipping_weight, 3),
"volumen": "", # [optional] Volume, in m3
"declarado": "", # [optional]
"dninomb": "0", # [optional]
"fechaentrega": "", # [optional]
"retorno": "0", # [optional]
"pod": "N", # [optional]
"podobligatorio": "N", # [deprecated]
"remite_plaza": "", # [optional] Origin agency
"remite_nombre": escape(
sender_partner.name or sender_partner.parent_id.name
),
"remite_direccion": escape(sender_partner.street or ""),
"remite_poblacion": escape(sender_partner.city or ""),
"remite_provincia": escape(sender_partner.state_id.name or ""),
"remite_pais": "34", # [mandatory] always 34=Spain
"remite_cp": sender_partner.zip or "",
"remite_telefono": sender_partner.phone or "",
"remite_movil": sender_partner.mobile or "",
"remite_email": escape(sender_partner.email or ""),
"remite_departamento": "",
"remite_nif": sender_partner.vat or "",
"remite_observaciones": "",
"destinatario_codigo": "",
"destinatario_plaza": "",
"destinatario_nombre": (
escape(picking.partner_id.name or picking.partner_id.parent_id.name)
or escape(
picking.partner_id.commercial_partner_id.name
or picking.partner_id.commercial_partner_id.parent_id.name
)
),
"destinatario_direccion": escape(picking.partner_id.street or ""),
"destinatario_poblacion": escape(picking.partner_id.city or ""),
"destinatario_provincia": escape(picking.partner_id.state_id.name or ""),
"destinatario_pais": picking.partner_id.country_id.phone_code or "",
"destinatario_cp": picking.partner_id.zip,
"destinatario_telefono": picking.partner_id.phone or "",
"destinatario_movil": picking.partner_id.mobile or "",
"destinatario_email": escape(picking.partner_id.email or ""),
"destinatario_observaciones": "",
"destinatario_att": "",
"destinatario_departamento": "",
"destinatario_nif": "",
"referencia_c": escape(
picking.name.replace("\\", "/") # It errors with \ characters
), # Our unique reference
"referencia_0": "", # Not used if the above is set
"importes_debido": "0", # The customer pays the shipping
"importes_reembolso": cash_amount or "",
"seguro": "0", # [optional]
"seguro_descripcion": "", # [optional]
"seguro_importe": "", # [optional]
"etiqueta": "PDF", # Get Label in response
"etiqueta_devolucion": "PDF",
# [optional] GLS Customer Code
# (when customer have several codes in GLS)
"cliente_codigo": "",
"cliente_plaza": "",
"cliente_agente": "",
}
def _prepare_gls_asm_pickup(self, picking):
"""Convert picking values for asm api pickup
:param picking record with picking to send
:returns dict values for the connector
"""
self.ensure_one()
sender_partner = picking.partner_id
receiving_partner = (
picking.picking_type_id.warehouse_id.partner_id
or picking.company_id.partner_id
)
if not sender_partner.street:
raise UserError(_("Couldn't find the sender street"))
if not receiving_partner.street:
raise UserError(_("Couldn't find the consignee street"))
return {
"fecha": fields.Date.today().strftime("%d/%m/%Y"),
"portes": self.gls_asm_postage_type,
"servicio": self.gls_asm_service,
"horario": self.gls_asm_shiptime,
"bultos": picking.number_of_packages,
"peso": round(picking.shipping_weight, 3),
"fechaentrega": "", # [optional]
"observaciones": "", # [optional]
"remite_nombre": escape(
sender_partner.name or sender_partner.parent_id.name
),
"remite_direccion": escape(sender_partner.street) or "",
"remite_poblacion": sender_partner.city or "",
"remite_provincia": sender_partner.state_id.name or "",
"remite_pais": (sender_partner.country_id.phone_code or ""),
"remite_cp": sender_partner.zip or "",
"remite_telefono": (
sender_partner.phone or sender_partner.parent_id.phone or ""
),
"remite_movil": (
sender_partner.mobile or sender_partner.parent_id.mobile or ""
),
"remite_email": (
sender_partner.email or sender_partner.parent_id.email or ""
),
"destinatario_nombre": escape(
receiving_partner.name or receiving_partner.parent_id.name
),
"destinatario_direccion": escape(receiving_partner.street) or "",
"destinatario_poblacion": receiving_partner.city or "",
"destinatario_provincia": receiving_partner.state_id.name or "",
"destinatario_pais": (receiving_partner.country_id.phone_code or ""),
"destinatario_cp": receiving_partner.zip or "",
"destinatario_telefono": (
receiving_partner.phone or receiving_partner.parent_id.phone or ""
),
"destinatario_movil": (
receiving_partner.mobile or receiving_partner.parent_id.mobile or ""
),
"destinatario_email": (
receiving_partner.email or receiving_partner.parent_id.email or ""
),
"referencia_c": escape(picking.name), # Our unique reference
"referencia_a": "", # Not used if the above is set
}
def gls_asm_send_shipping(self, pickings):
"""Send the package to GLS
:param pickings: A recordset of pickings
:return list: A list of dictionaries although in practice it's
called one by one and only the first item in the dict is taken. Due
to this design, we have to inject vals in the context to be able to
add them to the message.
"""
gls_request = GlsAsmRequest(self._gls_asm_uid())
result = []
for picking in pickings:
if picking.carrier_id.gls_is_pickup_service:
continue
if len(picking.name) > 15:
raise UserError(
_(
"GLS-ASM API doesn't admit a reference number higher than "
"15 characters. In order to handle it, they trim the"
"reference and as the reference is unique to every "
"customer we soon would have duplicated reference "
"collisions. To prevent this, you should edit your picking "
"sequence to a max of 15 characters."
)
)
vals = self._prepare_gls_asm_shipping(picking)
vals.update({"tracking_number": False, "exact_price": 0})
response = gls_request._send_shipping(vals)
self.log_xml(
response and response.get("gls_sent_xml", ""),
"GLS ASM Shipping Request",
)
self.log_xml(response or "", "GLS ASM Shipping Response")
if not response or response.get("_return", -1) < 0:
result.append(vals)
continue
# For compatibility we provide this number although we get
# two more codes: codbarras and uid
vals["tracking_number"] = response.get("_codexp")
gls_asm_picking_ref = ""
try:
references = response.get("Referencias", {}).get("Referencia", [])
for ref in references:
if ref.get("_tipo", "") == "N":
gls_asm_picking_ref = ref.get("value", "")
break
except Exception:
pass
picking.write(
{
"gls_asm_public_tracking_ref": response.get("_codbarras"),
"gls_asm_picking_ref": gls_asm_picking_ref,
}
)
# We post an extra message in the chatter with the barcode and the
# label because there's clean way to override the one sent by core.
body = _("GLS Shipping extra info:\n" "barcode: %s") % response.get(
"_codbarras"
)
attachment = []
if response.get("gls_label"):
attachment = [
(
"gls_label_{}.pdf".format(response.get("_codbarras")),
response.get("gls_label"),
)
]
picking.message_post(body=body, attachments=attachment)
result.append(vals)
return result
def gls_asm_send_pickup(self, pickings):
"""Send the request to GLS to pick a package up
:param pickings: A recordset of pickings
:return list: A list of dictionaries although in practice it's
called one by one and only the first item in the dict is taken. Due
to this design, we have to inject vals in the context to be able to
add them to the message.
"""
gls_request = GlsAsmRequest(self._gls_asm_uid())
result = []
for picking in pickings:
if not picking.carrier_id.gls_is_pickup_service:
continue
vals = self._prepare_gls_asm_pickup(picking)
vals.update({"tracking_number": False, "exact_price": 0})
response = gls_request._send_pickup(vals)
self.log_xml(
response and response.get("gls_sent_xml", ""), "GLS ASM Pick-up Request"
)
self.log_xml(response or "", "GLS ASM Pick-up Response")
if not response or response.get("_return", -1) < 0:
result.append(vals)
continue
# For compatibility we provide this number although we get
# two more codes: codbarras and uid
vals["tracking_number"] = response.get("_codigo")
picking.gls_asm_public_tracking_ref = response.get("_codigo")
# We post an extra message in the chatter with the barcode and the
# label because there's clean way to override the one sent by core.
body = _(
"GLS Pickup extra info:<br/> Tracking number: %s<br/> Bultos: %s"
) % (response.get("_codigo"), vals["bultos"])
picking.message_post(body=body)
result.append(vals)
return result
def gls_asm_tracking_state_update(self, picking):
"""Tracking state update"""
self.ensure_one()
if not picking.carrier_tracking_ref:
return
gls_request = GlsAsmRequest(self._gls_asm_uid())
tracking_info = {}
if not picking.carrier_id.gls_is_pickup_service:
tracking_info = gls_request._get_tracking_states(
picking.carrier_tracking_ref
)
tracking_states = tracking_info.get("tracking_list", {}).get("tracking", [])
# If there's just one state, we'll get a single dict, otherwise we
# get a list of dicts
if isinstance(tracking_states, dict):
tracking_states = [tracking_states]
else:
tracking_states = gls_request._get_pickup_tracking_states(
picking.carrier_tracking_ref
)
if not tracking_states:
return
self.log_xml(tracking_states or "", "GLS ASM Tracking Response")
picking.tracking_state_history = "\n".join(
[
"{} - [{}] {}".format(
t.get("fecha") or "{} {}".format(t.get("Fecha"), t.get("Hora")),
t.get("codigo") or t.get("Codigo"),
t.get("evento") or t.get("Descripcion"),
)
for t in tracking_states
]
)
tracking = tracking_states.pop()
picking.tracking_state = "[{}] {}".format(
tracking_info.get("codestado") or tracking.get("Codigo"),
tracking_info.get("estado") or tracking.get("Descripcion"),
)
if not picking.carrier_id.gls_is_pickup_service:
states_to_check = GLS_DELIVERY_STATES_STATIC
picking.gls_shipment_state = GLS_SHIPMENT_TYPE_STATES.get(
tracking_info.get("codestado"), "incidence"
)
else:
states_to_check = GLS_PICKUP_STATES_STATIC
# Portuguese pick-ups use the 0 code for extra states that aren't "Canceled"
# In order to not incorrectly mark as canceled, we take the most recent
# non-0 code (that isn't "Cancel") as the current state
if (
picking.partner_id.country_id.code == "PT"
and "Anulada" not in tracking.get("Descripcion")
):
tracking = list(
filter(lambda t: t["Codigo"] != "0", tracking_states)
).pop()
picking.gls_pickup_state = GLS_PICKUP_TYPE_STATES.get(
tracking.get("Codigo"), "incidence"
)
picking.delivery_state = states_to_check.get(
tracking_info.get("codestado") or tracking.get("Codigo"), "incidence"
)
def gls_asm_cancel_shipment(self, pickings):
"""Cancel the expedition"""
gls_request = GlsAsmRequest(self._gls_asm_uid())
for picking in pickings.filtered("carrier_tracking_ref"):
self.gls_asm_tracking_state_update(picking=picking)
if picking.delivery_state != "shipping_recorded_in_carrier":
raise UserError(
_(
"Unable to cancel GLS Expedition with reference {} "
+ "as it is in state {}.\nPlease manage the cancellation "
+ "of this shipment/pickup with GLS via email."
).format(picking.carrier_tracking_ref, picking.tracking_state)
)
if picking.carrier_id.gls_is_pickup_service:
response = gls_request._cancel_pickup(picking.carrier_tracking_ref)
else:
response = gls_request._cancel_shipment(picking.carrier_tracking_ref)
self.log_xml(
response and response.get("gls_sent_xml", ""), "GLS ASM Cancel Request"
)
self.log_xml(response or "", "GLS ASM Cancel Response")
if not response or response.get("_return") < 0:
msg = _("GLS Cancellation failed with reason: %s") % response.get(
"value", "Connection Error"
)
picking.message_post(body=msg)
continue
picking.write(
{
"gls_asm_public_tracking_ref": False,
"gls_asm_picking_ref": False,
}
)
self.gls_asm_tracking_state_update(picking=picking)
def gls_asm_rate_shipment(self, order):
"""There's no public API so another price method should be used
Not implemented with GLS-ASM, these values are so it works with websites"""
return {
"success": True,
"price": self.product_id.lst_price,
"error_message": _(
"""GLS ASM API doesn't provide methods to compute delivery rates, so
you should relay on another price method instead or override this
one in your custom code."""
),
"warning_message": _(
"""GLS ASM API doesn't provide methods to compute delivery rates, so
you should relay on another price method instead or override this
one in your custom code."""
),
}
def gls_asm_get_label(self, gls_asm_public_tracking_ref):
"""Generate label for picking
:param picking - stock.picking record
:returns pdf file
"""
self.ensure_one()
if not gls_asm_public_tracking_ref:
return False
gls_request = GlsAsmRequest(self._gls_asm_uid())
label = gls_request._shipping_label(gls_asm_public_tracking_ref)
if not label:
return False
return label
def action_get_manifest(self):
"""Action to launch the manifest wizard"""
self.ensure_one()
wizard = self.env["gls.asm.minifest.wizard"].create({"carrier_id": self.id})
view_id = self.env.ref("delivery_gls_asm.delivery_manifest_wizard_form").id
return {
"name": _("GLS Manifest"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "gls.asm.minifest.wizard",
"view_id": view_id,
"views": [(view_id, "form")],
"target": "new",
"res_id": wizard.id,
"context": self.env.context,
}