odoo-addon-delivery-ups-oca 17.0.1.0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/delivery_ups_oca/README.rst +147 -0
- odoo/addons/delivery_ups_oca/__init__.py +3 -0
- odoo/addons/delivery_ups_oca/__manifest__.py +26 -0
- odoo/addons/delivery_ups_oca/data/stock_package_type_data.xml +120 -0
- odoo/addons/delivery_ups_oca/i18n/delivery_ups_oca.pot +399 -0
- odoo/addons/delivery_ups_oca/i18n/it.po +471 -0
- odoo/addons/delivery_ups_oca/models/__init__.py +5 -0
- odoo/addons/delivery_ups_oca/models/delivery_carrier.py +213 -0
- odoo/addons/delivery_ups_oca/models/stock_package_type.py +9 -0
- odoo/addons/delivery_ups_oca/models/stock_picking.py +14 -0
- odoo/addons/delivery_ups_oca/models/ups_request.py +415 -0
- odoo/addons/delivery_ups_oca/readme/CONFIGURE.md +17 -0
- odoo/addons/delivery_ups_oca/readme/CONTRIBUTORS.md +9 -0
- odoo/addons/delivery_ups_oca/readme/DESCRIPTION.md +10 -0
- odoo/addons/delivery_ups_oca/readme/ROADMAP.md +2 -0
- odoo/addons/delivery_ups_oca/readme/USAGE.md +12 -0
- odoo/addons/delivery_ups_oca/static/description/icon.png +0 -0
- odoo/addons/delivery_ups_oca/static/description/index.html +498 -0
- odoo/addons/delivery_ups_oca/tests/__init__.py +3 -0
- odoo/addons/delivery_ups_oca/tests/test_delivery_ups.py +165 -0
- odoo/addons/delivery_ups_oca/views/delivery_carrier_view.xml +84 -0
- odoo/addons/delivery_ups_oca/views/stock_picking_view.xml +24 -0
- odoo_addon_delivery_ups_oca-17.0.1.0.0.2.dist-info/METADATA +166 -0
- odoo_addon_delivery_ups_oca-17.0.1.0.0.2.dist-info/RECORD +26 -0
- odoo_addon_delivery_ups_oca-17.0.1.0.0.2.dist-info/WHEEL +5 -0
- odoo_addon_delivery_ups_oca-17.0.1.0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright 2020 Hunki Enterprises BV
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
from odoo import fields, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PackageType(models.Model):
|
|
7
|
+
_inherit = "stock.package.type"
|
|
8
|
+
|
|
9
|
+
package_carrier_type = fields.Selection(selection_add=[("ups", "UPS")])
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2022 Tecnativa - Víctor Martínez
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
from odoo import models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StockPicking(models.Model):
|
|
7
|
+
_inherit = "stock.picking"
|
|
8
|
+
|
|
9
|
+
def ups_get_label(self):
|
|
10
|
+
self.ensure_one()
|
|
11
|
+
tracking_ref = self.carrier_tracking_ref
|
|
12
|
+
if self.delivery_type != "ups" or not tracking_ref:
|
|
13
|
+
return
|
|
14
|
+
return self.carrier_id.ups_get_label(tracking_ref)
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# Copyright 2020 Hunki Enterprises BV
|
|
2
|
+
# Copyright 2021 Tecnativa - Víctor Martínez
|
|
3
|
+
# Copyright 2024 Sygel - Manuel Regidor
|
|
4
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from odoo import _
|
|
12
|
+
from odoo.exceptions import UserError
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UpsRequest:
|
|
18
|
+
def __init__(self, carrier):
|
|
19
|
+
self.carrier = carrier
|
|
20
|
+
self.default_packaging_id = self.carrier.ups_default_packaging_id
|
|
21
|
+
self.use_packages_from_picking = self.carrier.ups_use_packages_from_picking
|
|
22
|
+
self.shipper_number = self.carrier.ups_shipper_number
|
|
23
|
+
self.service_code = self.carrier.ups_service_code
|
|
24
|
+
self.file_format = self.carrier.ups_file_format
|
|
25
|
+
self.package_dimension_code = self.carrier.ups_package_dimension_code
|
|
26
|
+
self.package_weight_code = self.carrier.ups_package_weight_code
|
|
27
|
+
self.transaction_src = "Odoo (%s)" % self.carrier.name
|
|
28
|
+
self.client_id = self.carrier.ups_client_id
|
|
29
|
+
self.client_secret = self.carrier.ups_client_secret
|
|
30
|
+
self.token = self.carrier.ups_token
|
|
31
|
+
self.token_expiration_date = self.carrier.ups_token_expiration_date
|
|
32
|
+
self.url = "https://wwwcie.ups.com"
|
|
33
|
+
if self.carrier.prod_environment:
|
|
34
|
+
self.url = "https://onlinetools.ups.com"
|
|
35
|
+
|
|
36
|
+
def _raise_for_status(self, status, skip_errors=True):
|
|
37
|
+
errors = status.get("response", {}).get("errors")
|
|
38
|
+
if errors:
|
|
39
|
+
msg = _("Sending to UPS: {}").format(
|
|
40
|
+
"\n".join("{code} {message}".format(**error) for error in errors),
|
|
41
|
+
)
|
|
42
|
+
if skip_errors:
|
|
43
|
+
_logger.info(msg)
|
|
44
|
+
else:
|
|
45
|
+
raise UserError(msg)
|
|
46
|
+
|
|
47
|
+
def _send_request(
|
|
48
|
+
self, url, json=None, data=None, headers=None, method="post", auth=None
|
|
49
|
+
):
|
|
50
|
+
return getattr(requests, method)(
|
|
51
|
+
url, data=data, json=json, headers=headers, auth=auth
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _get_new_token(self):
|
|
55
|
+
if not (self.client_id and self.client_secret):
|
|
56
|
+
raise UserError(
|
|
57
|
+
_(
|
|
58
|
+
"Both Client ID and Client Secret"
|
|
59
|
+
" must be set in UPS delivery carriers."
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
url = "%s/security/v1/oauth/token" % self.url
|
|
63
|
+
headers = {"x-merchant-id": self.client_id}
|
|
64
|
+
data = {"grant_type": "client_credentials"}
|
|
65
|
+
status = self._send_request(
|
|
66
|
+
url, data=data, headers=headers, auth=(self.client_id, self.client_secret)
|
|
67
|
+
)
|
|
68
|
+
status = status.json()
|
|
69
|
+
self._raise_for_status(status, False)
|
|
70
|
+
token = status.get("access_token")
|
|
71
|
+
self.token = token
|
|
72
|
+
self.carrier.ups_token = token
|
|
73
|
+
self.carrier.ups_token_expiration_date = (
|
|
74
|
+
datetime.datetime.now()
|
|
75
|
+
+ datetime.timedelta(seconds=int(status.get("expires_in")))
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _process_reply(
|
|
79
|
+
self,
|
|
80
|
+
url,
|
|
81
|
+
json=None,
|
|
82
|
+
data=None,
|
|
83
|
+
method="post",
|
|
84
|
+
headers_extra=None,
|
|
85
|
+
):
|
|
86
|
+
if (
|
|
87
|
+
not self.token
|
|
88
|
+
or not self.token_expiration_date
|
|
89
|
+
or (self.token_expiration_date <= datetime.datetime.now())
|
|
90
|
+
):
|
|
91
|
+
self._get_new_token()
|
|
92
|
+
data = data or {}
|
|
93
|
+
headers = {
|
|
94
|
+
"Authorization": f"Bearer {self.token}",
|
|
95
|
+
}
|
|
96
|
+
if headers_extra:
|
|
97
|
+
headers = {**headers, **headers_extra}
|
|
98
|
+
status = self._send_request(url, json, data, headers, method)
|
|
99
|
+
# Generate a new token
|
|
100
|
+
if status.status_code == 401:
|
|
101
|
+
self._get_new_token()
|
|
102
|
+
status = self._send_request(url, json, data, headers, method)
|
|
103
|
+
status = status.json()
|
|
104
|
+
ups_last_request = f"URL: {self.url}\nData: {data}"
|
|
105
|
+
self.carrier.log_xml(ups_last_request, "ups_last_request")
|
|
106
|
+
self.carrier.log_xml(status or "", "ups_last_response")
|
|
107
|
+
return status
|
|
108
|
+
|
|
109
|
+
def _quant_package_data_from_picking(self, package, picking, is_package=False):
|
|
110
|
+
NumOfPieces = picking.number_of_packages
|
|
111
|
+
PackageWeight = picking.shipping_weight
|
|
112
|
+
if is_package:
|
|
113
|
+
NumOfPieces = sum(package.mapped("quant_ids.quantity"))
|
|
114
|
+
PackageWeight = max(package.shipping_weight, package.weight)
|
|
115
|
+
package = package.package_type_id
|
|
116
|
+
return {
|
|
117
|
+
"Description": package.name,
|
|
118
|
+
"NumOfPieces": str(NumOfPieces),
|
|
119
|
+
"Packaging": {
|
|
120
|
+
"Code": package.shipper_package_code,
|
|
121
|
+
"Description": package.name,
|
|
122
|
+
},
|
|
123
|
+
"Dimensions": {
|
|
124
|
+
"UnitOfMeasurement": {"Code": self.package_dimension_code},
|
|
125
|
+
"Length": str(package.packaging_length),
|
|
126
|
+
"Width": str(package.width),
|
|
127
|
+
"Height": str(package.height),
|
|
128
|
+
},
|
|
129
|
+
"PackageWeight": {
|
|
130
|
+
"UnitOfMeasurement": {"Code": self.package_weight_code},
|
|
131
|
+
"Weight": str(PackageWeight),
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def _partner_to_shipping_data(self, partner, **kwargs):
|
|
136
|
+
"""Return a dict describing a partner for the shipping request"""
|
|
137
|
+
return dict(
|
|
138
|
+
**kwargs,
|
|
139
|
+
Name=(partner.parent_id or partner).name,
|
|
140
|
+
AttentionName=partner.name,
|
|
141
|
+
TaxIdentificationNumber=partner.vat,
|
|
142
|
+
Phone=dict(Number=partner.phone or partner.mobile),
|
|
143
|
+
EMailAddress=partner.email,
|
|
144
|
+
Address=dict(
|
|
145
|
+
AddressLine=[partner.street, partner.street2 or ""],
|
|
146
|
+
City=partner.city,
|
|
147
|
+
StateProvinceCode=partner.state_id.code,
|
|
148
|
+
PostalCode=partner.zip,
|
|
149
|
+
CountryCode=partner.country_id.code,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _label_data(self):
|
|
154
|
+
res = {"LabelImageFormat": {"Code": self.file_format}}
|
|
155
|
+
# According to documentation, we need to specify sizes in some formats
|
|
156
|
+
if self.file_format != "GIF":
|
|
157
|
+
res["LabelStockSize"] = {"Height": "6", "Width": "4"}
|
|
158
|
+
return res
|
|
159
|
+
|
|
160
|
+
def _prepare_create_shipping(self, picking):
|
|
161
|
+
"""Return a dict that can be passed to the shipping endpoint of the UPS API"""
|
|
162
|
+
if self.use_packages_from_picking and picking.package_ids:
|
|
163
|
+
# modelo: stock.quant.package
|
|
164
|
+
packages = [
|
|
165
|
+
self._quant_package_data_from_picking(package, picking, True)
|
|
166
|
+
for package in picking.package_ids
|
|
167
|
+
]
|
|
168
|
+
else:
|
|
169
|
+
# modelo: stock.package.type
|
|
170
|
+
packages = []
|
|
171
|
+
package_info = self._quant_package_data_from_picking(
|
|
172
|
+
self.default_packaging_id, picking, False
|
|
173
|
+
)
|
|
174
|
+
package_weight = round(
|
|
175
|
+
(picking.shipping_weight / picking.number_of_packages), 2
|
|
176
|
+
)
|
|
177
|
+
for i in range(0, picking.number_of_packages):
|
|
178
|
+
package_item = package_info.copy()
|
|
179
|
+
package_name = f"{picking.name} ({i+1})"
|
|
180
|
+
package_item["Description"] = package_name
|
|
181
|
+
package_item["NumOfPieces"] = "1"
|
|
182
|
+
package_item["Packaging"]["Description"] = package_name
|
|
183
|
+
package_item["PackageWeight"]["Weight"] = str(package_weight)
|
|
184
|
+
packages.append(package_item)
|
|
185
|
+
vals = {
|
|
186
|
+
"ShipmentRequest": {
|
|
187
|
+
"Shipment": {
|
|
188
|
+
"Description": picking.name,
|
|
189
|
+
"Shipper": self._partner_to_shipping_data(
|
|
190
|
+
partner=picking.company_id.partner_id,
|
|
191
|
+
ShipperNumber=self.shipper_number,
|
|
192
|
+
),
|
|
193
|
+
"ShipTo": self._partner_to_shipping_data(picking.partner_id),
|
|
194
|
+
"ShipFrom": self._partner_to_shipping_data(
|
|
195
|
+
picking.picking_type_id.warehouse_id.partner_id
|
|
196
|
+
or picking.company_id.partner_id
|
|
197
|
+
),
|
|
198
|
+
"PaymentInformation": {
|
|
199
|
+
"ShipmentCharge": {
|
|
200
|
+
"Type": "01",
|
|
201
|
+
"BillShipper": {
|
|
202
|
+
# we ignore the alternatives paying per credit card or
|
|
203
|
+
# paypal for now
|
|
204
|
+
"AccountNumber": self.shipper_number,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"Service": {"Code": self.service_code},
|
|
209
|
+
"Package": packages,
|
|
210
|
+
},
|
|
211
|
+
"LabelSpecification": self._label_data(),
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if picking.carrier_id.ups_cash_on_delivery and picking.sale_id:
|
|
215
|
+
vals["ShipmentRequest"]["Shipment"]["ShipmentServiceOptions"] = (
|
|
216
|
+
{
|
|
217
|
+
"COD": {
|
|
218
|
+
"CODFundsCode": picking.carrier_id.ups_cod_funds_code,
|
|
219
|
+
"CODAmount": {
|
|
220
|
+
"CurrencyCode": picking.sale_id.currency_id.name,
|
|
221
|
+
"MonetaryValue": str(picking.sale_id.amount_total),
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
return vals
|
|
227
|
+
|
|
228
|
+
def _send_shipping(self, picking):
|
|
229
|
+
status = self._process_reply(
|
|
230
|
+
url="%s/api/shipments/v1/ship" % self.url,
|
|
231
|
+
json=self._prepare_create_shipping(picking),
|
|
232
|
+
)
|
|
233
|
+
self._raise_for_status(status, False)
|
|
234
|
+
res = status["ShipmentResponse"]["ShipmentResults"]
|
|
235
|
+
PackageResults = res["PackageResults"]
|
|
236
|
+
labels = []
|
|
237
|
+
if isinstance(PackageResults, dict):
|
|
238
|
+
labels.append(
|
|
239
|
+
{
|
|
240
|
+
"tracking_ref": PackageResults["TrackingNumber"],
|
|
241
|
+
"format_code": PackageResults["ShippingLabel"]["ImageFormat"][
|
|
242
|
+
"Code"
|
|
243
|
+
],
|
|
244
|
+
"datas": PackageResults["ShippingLabel"]["GraphicImage"],
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
if isinstance(PackageResults, list):
|
|
248
|
+
for label in PackageResults:
|
|
249
|
+
labels.append(
|
|
250
|
+
{
|
|
251
|
+
"tracking_ref": label["TrackingNumber"],
|
|
252
|
+
"format_code": label["ShippingLabel"]["ImageFormat"]["Code"],
|
|
253
|
+
"datas": label["ShippingLabel"]["GraphicImage"],
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
return {
|
|
257
|
+
"price": res["ShipmentCharges"]["TotalCharges"],
|
|
258
|
+
"ShipmentIdentificationNumber": res["ShipmentIdentificationNumber"],
|
|
259
|
+
"labels": labels,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def _quant_package_data_from_order(self, order):
|
|
263
|
+
PackageWeight = 0
|
|
264
|
+
for line in order.order_line.filtered(
|
|
265
|
+
lambda x: x.product_id and x.product_id.weight > 0
|
|
266
|
+
):
|
|
267
|
+
PackageWeight += line.product_id.weight * line.product_uom_qty
|
|
268
|
+
return {
|
|
269
|
+
"PackagingType": {"Code": self.default_packaging_id.shipper_package_code},
|
|
270
|
+
"Dimensions": {
|
|
271
|
+
"UnitOfMeasurement": {"Code": self.package_dimension_code},
|
|
272
|
+
"Length": str(self.default_packaging_id.packaging_length),
|
|
273
|
+
"Width": str(self.default_packaging_id.width),
|
|
274
|
+
"Height": str(self.default_packaging_id.height),
|
|
275
|
+
},
|
|
276
|
+
"PackageWeight": {
|
|
277
|
+
"UnitOfMeasurement": {"Code": self.package_weight_code},
|
|
278
|
+
"Weight": str(PackageWeight),
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def _prepare_rate_shipment(self, order):
|
|
283
|
+
packages = [self._quant_package_data_from_order(order)]
|
|
284
|
+
return {
|
|
285
|
+
"RateRequest": {
|
|
286
|
+
"Shipment": {
|
|
287
|
+
"Shipper": self._partner_to_shipping_data(
|
|
288
|
+
partner=order.company_id.partner_id,
|
|
289
|
+
ShipperNumber=self.shipper_number,
|
|
290
|
+
),
|
|
291
|
+
"ShipTo": self._partner_to_shipping_data(order.partner_shipping_id),
|
|
292
|
+
"ShipFrom": self._partner_to_shipping_data(
|
|
293
|
+
order.warehouse_id.partner_id or order.company_id.partner_id
|
|
294
|
+
),
|
|
295
|
+
"Service": {"Code": self.service_code},
|
|
296
|
+
"Package": packages,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
def _rate_shipment(self, order, skip_errors=False):
|
|
302
|
+
status = self._process_reply(
|
|
303
|
+
url="%s/api/rating/v1/Rate" % self.url,
|
|
304
|
+
json=self._prepare_rate_shipment(order),
|
|
305
|
+
)
|
|
306
|
+
self._raise_for_status(status, skip_errors)
|
|
307
|
+
return status
|
|
308
|
+
|
|
309
|
+
def rate_shipment(self, order):
|
|
310
|
+
status = self._rate_shipment(order)
|
|
311
|
+
return status["RateResponse"]["RatedShipment"]["TotalCharges"]
|
|
312
|
+
|
|
313
|
+
def _prepare_shipping_label(self, carrier_tracking_ref):
|
|
314
|
+
return {
|
|
315
|
+
"LabelRecoveryRequest": {
|
|
316
|
+
"LabelSpecification": self._label_data(),
|
|
317
|
+
"TrackingNumber": carrier_tracking_ref,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
def shipping_label(self, carrier_tracking_ref):
|
|
322
|
+
status = self._process_reply(
|
|
323
|
+
url="%s/api/labels/v1/recovery" % self.url,
|
|
324
|
+
json=self._prepare_shipping_label(carrier_tracking_ref),
|
|
325
|
+
)
|
|
326
|
+
self._raise_for_status(status, False)
|
|
327
|
+
labels = []
|
|
328
|
+
labels_data = status["LabelRecoveryResponse"]["LabelResults"]
|
|
329
|
+
if isinstance(labels_data, dict):
|
|
330
|
+
labels.append(
|
|
331
|
+
{
|
|
332
|
+
"tracking_ref": labels_data["TrackingNumber"],
|
|
333
|
+
"format_code": labels_data["LabelImage"]["LabelImageFormat"][
|
|
334
|
+
"Code"
|
|
335
|
+
],
|
|
336
|
+
"datas": labels_data["LabelImage"]["GraphicImage"],
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
elif isinstance(labels_data, list):
|
|
340
|
+
for label in labels_data:
|
|
341
|
+
labels.append(
|
|
342
|
+
{
|
|
343
|
+
"tracking_ref": label["TrackingNumber"],
|
|
344
|
+
"format_code": label["LabelImage"]["LabelImageFormat"]["Code"],
|
|
345
|
+
"datas": label["LabelImage"]["GraphicImage"],
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return labels
|
|
350
|
+
|
|
351
|
+
def cancel_shipment(self, picking):
|
|
352
|
+
url = "%s/api/shipments/v1/void/cancel" % self.url
|
|
353
|
+
url = f"{url}/{picking.carrier_tracking_ref}"
|
|
354
|
+
status = self._process_reply(url=url, method="delete")
|
|
355
|
+
self._raise_for_status(status, False)
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
def tracking_state_update(self, picking):
|
|
359
|
+
static_states = {
|
|
360
|
+
"I": "in_transit",
|
|
361
|
+
"D": "customer_delivered",
|
|
362
|
+
"E": "incidence",
|
|
363
|
+
"P": "customer_delivered",
|
|
364
|
+
"M": "in_transit",
|
|
365
|
+
}
|
|
366
|
+
status = self._process_reply(
|
|
367
|
+
url=f"{self.url}/api/track/v1/details/{picking.carrier_tracking_ref}",
|
|
368
|
+
method="get",
|
|
369
|
+
headers_extra={
|
|
370
|
+
"transId": f"{datetime.datetime.now().timestamp()}",
|
|
371
|
+
"transactionSrc": f"{picking.company_id.name} - Odoo",
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
self._raise_for_status(status, False)
|
|
375
|
+
states_list = []
|
|
376
|
+
delivery_state = "incidence"
|
|
377
|
+
try:
|
|
378
|
+
shipment = status["trackResponse"]["shipment"][0]
|
|
379
|
+
if not shipment.get("warnings"):
|
|
380
|
+
for activity in shipment["package"][0]["activity"]:
|
|
381
|
+
states_list.append(
|
|
382
|
+
"{} - {}".format(
|
|
383
|
+
datetime.datetime.strptime(
|
|
384
|
+
"{}{}".format(
|
|
385
|
+
activity.get("date"), activity.get("time")
|
|
386
|
+
),
|
|
387
|
+
"%Y%m%d%H%M%S",
|
|
388
|
+
),
|
|
389
|
+
activity.get("status").get("description"),
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
if shipment["package"][0]["activity"]:
|
|
393
|
+
delivery_state = static_states.get(
|
|
394
|
+
shipment["package"][0]["activity"][0]["status"]["type"],
|
|
395
|
+
"incidence",
|
|
396
|
+
)
|
|
397
|
+
else:
|
|
398
|
+
for warning in shipment.get("warnings"):
|
|
399
|
+
states_list.append(
|
|
400
|
+
_("{date} - Warning: {warn}").format(
|
|
401
|
+
date=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
402
|
+
warn=warning.get("message"),
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
except Exception:
|
|
407
|
+
states_list.append(
|
|
408
|
+
_("{} - Error retrieving the tracking information.").format(
|
|
409
|
+
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
return {
|
|
413
|
+
"delivery_state": delivery_state,
|
|
414
|
+
"tracking_state_history": "\n".join(states_list),
|
|
415
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
To configure this module, you need to:
|
|
2
|
+
|
|
3
|
+
1. Add a Shipping Method with Provider ``UPS`` and fill in your UPS credentials
|
|
4
|
+
(Client ID and Client Secret)
|
|
5
|
+
2. Configure in Odoo all required fields of the UPS tab with your
|
|
6
|
+
account data <https://wwwapps.ups.com/ppc/ppc.html> (Shipper number,
|
|
7
|
+
Default Packaging, Package Dimension Code, Package Weight Code and
|
|
8
|
+
File Format).
|
|
9
|
+
3. If yo have "Tracking state update sync" checked all delivery orders
|
|
10
|
+
state check will be done querying UPS services.
|
|
11
|
+
4. It is possible to create a UPS carrier for cash on delivery parcels.
|
|
12
|
+
Select the `ups` delivery type and check the "Cash on Delivery"
|
|
13
|
+
checkbox under the "UPS" tab. It is required to select the "UPS COD
|
|
14
|
+
Funds Code" when the "Cash on Delivery" option is selected.
|
|
15
|
+
|
|
16
|
+
**NOTE** You need to add an APP from <https://developer.ups.com/> for
|
|
17
|
+
using the webservice.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
- Holger Brunn \<<mail@hunki-enterprises.nl>\>
|
|
2
|
+
(<https://hunki-enterprises.nl>)
|
|
3
|
+
- [Tecnativa](https://www.tecnativa.com):
|
|
4
|
+
- Víctor Martínez
|
|
5
|
+
- Pedro M. Baeza
|
|
6
|
+
- [ForgeFlow](https://www.forgeflow.com):
|
|
7
|
+
- Jordi Ballester
|
|
8
|
+
- [Sygel](https://www.sygel.es):
|
|
9
|
+
- Manuel Regidor
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
This module adds [UPS](https://ups.com) to the available carriers.
|
|
2
|
+
|
|
3
|
+
It allows you to register shippings, generate labels, get rates from
|
|
4
|
+
order, read shipping states and cancel shipments using UPS webservice,
|
|
5
|
+
so no need of exchanging any kind of file.
|
|
6
|
+
|
|
7
|
+
When a sales order is created in Odoo and the UPS carrier is assigned,
|
|
8
|
+
the shipping price that will be obtained will be the price that the UPS
|
|
9
|
+
webservice estimates according to the order information (address and
|
|
10
|
+
products).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
You have to set the created shipping method in the delivery order to
|
|
2
|
+
ship:
|
|
3
|
+
|
|
4
|
+
- When the picking is 'Transferred', a *Create Shipping Label* button
|
|
5
|
+
appears. Just click on it, and if all went well, the label will be
|
|
6
|
+
'attached'.
|
|
7
|
+
- If the shipment creation process fails, a validation error will appear
|
|
8
|
+
displaying UPS error.
|
|
9
|
+
- When the delivery order is cancelled, it's automatically cancelled too
|
|
10
|
+
in UPS.
|
|
11
|
+
- If you have "Tracking state update sync" checked in the shipping
|
|
12
|
+
method, a periodical state check will be done querying UPS services.
|
|
Binary file
|