forked from OCA/delivery-carrier
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstock_quant_package.py
357 lines (295 loc) · 10.8 KB
/
stock_quant_package.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
# coding: utf-8
# @author Raphael Reverdy <raphael.reverdy@akretion.com>
# David BEAL <david.beal@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
import base64
from odoo import models, api, fields
from odoo.tools.translate import _
from odoo.exceptions import UserError
from ..decorator import implemented_by_carrier
_logger = logging.getLogger(__name__)
try:
from roulier import roulier
from roulier.exception import (
InvalidApiInput,
CarrierError
)
except ImportError:
_logger.debug('Cannot `import roulier`.')
class StockQuantPackage(models.Model):
_inherit = 'stock.quant.package'
carrier_id = fields.Many2one("delivery.carrier", string="Carrier")
# helper : move it to base ?
@api.multi
def get_operations(self):
"""Get operations of the package.
Usefull for having products and quantities
"""
self.ensure_one()
return self.env['stock.pack.operation'].search([
('result_package_id', '=', self.id),
('product_id', '!=', False),
])
# API
# Each method in this class have at least picking arg to directly
# deal with stock.picking if required by your carrier use case
@implemented_by_carrier
def _before_call(self, picking, payload):
pass
@implemented_by_carrier
def _after_call(self, picking, response):
pass
@implemented_by_carrier
def _get_customs(self, picking):
pass
@implemented_by_carrier
def _should_include_customs(self, picking):
pass
@implemented_by_carrier
def _get_parcel(self, picking):
pass
@implemented_by_carrier
def _carrier_error_handling(self, payload, response):
pass
@implemented_by_carrier
def _invalid_api_input_handling(self, payload, response):
pass
@implemented_by_carrier
def _prepare_label(self, label, picking):
pass
@implemented_by_carrier
def _handle_attachments(self, label, response):
pass
@implemented_by_carrier
def _handle_tracking(self, label, response):
pass
@implemented_by_carrier
def _get_tracking_link(self):
pass
@implemented_by_carrier
def _generate_labels(self, picking):
pass
@implemented_by_carrier
def _get_parcels(self, picking):
pass
# end of API
# Core functions
@api.multi
def _roulier_generate_labels(self, picking):
# by default, only one pack per call
for package in self:
response = package._call_roulier_api(picking)
package._handle_tracking(picking, response)
package._handle_attachments(picking, response)
@api.multi
def _roulier_get_parcels(self, picking):
# by default, only one pack per call
self.ensure_one()
return [self._get_parcel(picking)]
@api.multi
def open_website_url(self):
"""Open website for parcel tracking.
Each carrier should implement _get_tracking_link
There is low chance you need to override this method.
returns:
action
"""
self.ensure_one()
url = self._get_tracking_link()
client_action = {
'type': 'ir.actions.act_url',
'name': "Shipment Tracking Page",
'target': 'new',
'url': url,
}
return client_action
@api.multi
def _call_roulier_api(self, picking):
"""Create a label for a given package_id (self)."""
# There is low chance you need to override it.
# Don't forget to implement _a-carrier_before_call
# and _a-carrier_after_call
self.write({'carrier_id': picking.carrier_id.id})
roulier_instance = roulier.get(picking.carrier_type)
payload = roulier_instance.api()
sender = picking._get_sender(self)
receiver = picking._get_receiver(self)
payload['auth'] = picking._get_auth(self)
payload['from_address'] = picking._convert_address(sender)
payload['to_address'] = picking._convert_address(receiver)
if self._should_include_customs(picking):
payload['customs'] = self._get_customs(picking)
payload['service'] = picking._get_service(self)
payload['parcels'] = self._get_parcels(picking)
# hook to override request / payload
payload = self._before_call(picking, payload)
try:
# api call
ret = roulier_instance.get_label(payload)
except InvalidApiInput as e:
raise UserError(self._invalid_api_input_handling(payload, e))
except CarrierError as e:
raise UserError(self._carrier_error_handling(payload, e))
# give result to someone else
return self._after_call(picking, ret)
# default implementations
@api.multi
def _roulier_get_parcel(self, picking):
self.ensure_one()
weight = self.weight
parcel = {
'weight': weight,
'reference': self.name
}
return parcel
def _roulier_before_call(self, picking, payload):
"""Add stuff to payload just before api call.
Put here what you can't put in other methods
(like _get_parcel, _get_service...)
It's totally ok to do nothing here.
returns:
dict
"""
return payload
def _roulier_after_call(self, picking, response):
"""Do stuff just after api call.
It's totally ok to do nothing here.
"""
return response
def _roulier_get_tracking_link(self):
"""Build a tracking url.
You have to implement it for your carrier.
It's like :
'https://the-carrier.com/?track=%s' % self.parcel_tracking
returns:
string (url)
"""
_logger.warning("not implemented")
pass
def _roulier_should_include_customs(self, picking):
"""Choose if custom docs should be sent.
Really dumb implementation.
You may improve this for your carrier.
"""
sender = picking._get_sender(self)
receiver = picking._get_receiver(self)
return sender.country_id.code != receiver.country_id.code
def _roulier_carrier_error_handling(self, payload, exception):
"""Build exception message for carrier error.
It's happen when the carrier WS returns something unexpected.
You may improve this for your carrier.
returns:
string
"""
try:
_logger.debug(exception.response.text)
_logger.debug(exception.response.request.body)
except AttributeError:
_logger.debug('No request available')
return _(u'Sent data:\n%s\n\nException raised:\n%s\n' % (
payload, exception.message))
def _roulier_invalid_api_input_handling(self, payload, exception):
"""Build exception message for bad input.
It's happend when your data is not valid, like a missing value
in the payload.
You may improve this for your carrier.
returns:
string
"""
return _(u'Bad input: %s\n' % exception.message)
@api.multi
def _roulier_get_customs(self, picking):
"""Format customs infos for each product in the package.
The decision whether to include these infos or not is
taken in _should_include_customs()
Returns:
dict.'articles' : list with qty, weight, hs_code
int category: gift 1, sample 2, commercial 3, ...
"""
self.ensure_one()
articles = []
for operation in self.get_operations():
article = {}
articles.append(article)
product = operation.product_id
# stands for harmonized_system
hs = product.product_tmpl_id.get_hs_code_recursively()
article['quantity'] = '%.f' % operation.product_qty
article['weight'] = (
operation.get_weight() / operation.product_qty)
article['originCountry'] = product.origin_country_id.code
article['description'] = hs.description
article['hs'] = hs.hs_code
article['value'] = product.list_price # unit price is expected
category = picking.customs_category
return {
"articles": articles,
"category": category,
}
# There is low chance you need to override the following methods.
@api.multi
def _roulier_handle_attachments(self, picking, response):
labels = []
parcels = iter(response.get('parcels', []))
for rec in self:
parcel_rep = parcels.next()
main_label = rec._roulier_prepare_label(picking, parcel_rep)
labels.append(
self.env['shipping.label'].create(main_label)
)
attachments = [
self.env['ir.attachment'].create(attachment)
for attachment in
self[0]._roulier_prepare_attachments(picking, response)
] # do it once for all
return {
'labels': labels,
'attachments': attachments,
}
@api.multi
def _roulier_prepare_label(self, picking, response):
"""Prepare a dict for building a shipping.label.
The shipping label is what you stick on your packages.
returns:
dict
"""
self.ensure_one()
label = response.get('label')
return {
'res_id': picking.id,
'res_model': 'stock.picking',
'package_id': self.id,
'name': "%s %s" % (self.name, label['name']),
'datas': base64.b64encode(label['data']),
'type': 'binary',
'datas_fname': "%s-%s.%s" % (
self.name, label['name'], label['type']),
}
@api.multi
def _roulier_prepare_attachments(self, picking, response):
"""Prepare a list of dicts for building ir.attachemens.
Attachements are annexes like customs declarations, summary
etc.
returns:
list
"""
self.ensure_one()
attachments = response.get('annexes')
return [{
'res_id': picking.id,
'res_model': 'stock.picking',
'name': "%s %s" % (self.name, attachment['name']),
'datas': base64.b64encode(attachment['data']),
'type': 'binary',
'datas_fname': "%s-%s.%s" % (
self.name, attachment['name'], attachment['type']),
} for attachment in attachments]
@api.multi
def _roulier_handle_tracking(self, picking, response):
number = False
tracking = response.get('tracking')
if tracking:
number = tracking.get('number')
for rec in self:
rec.parcel_tracking = number