-
Notifications
You must be signed in to change notification settings - Fork 18
/
shipment.py
382 lines (320 loc) · 12.7 KB
/
shipment.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
# -*- coding: utf-8 -*-
"""
shipment.py
"""
import simplejson as json
from decimal import Decimal
from trytond.model import fields, ModelView
from trytond.pool import PoolMeta, Pool
from trytond.wizard import Wizard, StateView, Button, StateTransition
from trytond.pyson import Eval, Bool
from trytond.transaction import Transaction
from .mixin import ShipmentCarrierMixin
__metaclass__ = PoolMeta
__all__ = [
'ShipmentOut', 'GenerateShippingLabelMessage',
'GenerateShippingLabel', 'ShippingCarrierSelector',
'SelectShippingRate'
]
class ShipmentOut(ShipmentCarrierMixin):
__metaclass__ = PoolMeta
__name__ = 'stock.shipment.out'
@property
def carrier_cost_moves(self):
return filter(
lambda m: (m.state != 'cancel' and m.quantity), self.outgoing_moves
)
def on_change_inventory_moves(self):
with Transaction().set_context(ignore_carrier_computation=True):
super(ShipmentOut, self).on_change_inventory_moves()
@classmethod
def get_weight(cls, records, name=None):
res = {}
for shipment in records:
weight_uom = shipment.weight_uom
if shipment.packages or (shipment.state in ('packed', 'done')):
res.update(super(ShipmentOut, cls).get_weight([shipment], name))
continue
inventory_moves = filter(
lambda m: (m.state != 'cancel' and m.quantity),
shipment.inventory_moves
)
res[shipment.id] = sum([
move.get_weight(weight_uom, silent=True)
for move in inventory_moves
])
return res
@classmethod
def pack(cls, shipments):
super(ShipmentOut, cls).pack(shipments)
for shipment in shipments:
if not shipment.packages:
# No package, create a default package
shipment._create_default_package()
else:
if (len(shipment.carrier_cost_moves) !=
sum(len(p.moves) for p in shipment.packages)):
cls.raise_user_error(
"Not all the items are packaged for shipment #%s", (
shipment.number, )
)
class ShippingCarrierSelector(ModelView):
'View To Select Carrier'
__name__ = 'shipping.label.start'
carrier = fields.Many2One("carrier", "Carrier", required=True)
override_weight = fields.Float("Override Weight", digits=(16, 2))
no_of_packages = fields.Integer('Number of packages', readonly=True)
box_type = fields.Many2One(
"carrier.box_type", "Box Type", domain=[
('id', 'in', Eval("available_box_types"))
], depends=["available_box_types"]
)
shipping_instructions = fields.Text('Shipping Instructions', readonly=True)
carrier_service = fields.Many2One(
"carrier.service", "Carrier Service", domain=[
('id', 'in', Eval("available_carrier_services"))
], depends=["available_carrier_services"]
)
available_box_types = fields.Function(
fields.One2Many("carrier.box_type", None, 'Available Box Types'),
getter="on_change_with_available_box_types"
)
available_carrier_services = fields.Function(
fields.One2Many("carrier.service", None, 'Available Carrier Services'),
getter="on_change_with_available_carrier_services"
)
@fields.depends('carrier', 'carrier_service', 'box_type')
def on_change_carrier(self):
self.carrier_service = None
self.box_type = None
@fields.depends("carrier")
def on_change_with_available_box_types(self, name=None):
if self.carrier:
return map(int, self.carrier.box_types)
return []
@fields.depends("carrier")
def on_change_with_available_carrier_services(self, name=None):
if self.carrier:
return map(int, self.carrier.services)
return []
@classmethod
def view_attributes(cls):
return super(ShippingCarrierSelector, cls).view_attributes() + [
('//label[@name="no_of_packages"]', 'states', {
'invisible': Bool(Eval('no_of_packages')),
})
]
class SelectShippingRate(ModelView):
'Select Shipping Rate'
__name__ = 'shipping.label.select_rate'
rate = fields.Selection([], 'Select Rate')
class GenerateShippingLabelMessage(ModelView):
'Generate Labels Message'
__name__ = 'shipping.label.end'
tracking_number = fields.Many2One(
"shipment.tracking", "Tracking number", readonly=True
)
message = fields.Text("Message", readonly=True)
attachments = fields.One2Many(
'ir.attachment', None, 'Attachments', readonly=True
)
cost = fields.Numeric("Cost", digits=(16, 2), readonly=True)
cost_currency = fields.Many2One(
'currency.currency', 'Cost Currency', readonly=True
)
class GenerateShippingLabel(Wizard):
'Generate Labels'
__name__ = 'shipping.label'
#: This is the first state of wizard to generate shipping label.
#: It asks for carrier, carrier_service, box_type and override weight,
#: once entered, it move to `next` transition where it saves all the
#: values on shipment.
start = StateView(
'shipping.label.start',
'shipping.select_carrier_view_form',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Continue', 'next', 'tryton-go-next'),
]
)
#: Transition saves values from `start` state to the shipment.
next = StateTransition()
#: Select shipping rates
select_rate = StateView(
'shipping.label.select_rate',
'shipping.select_rate_view_form',
[
Button('Cancel', 'end', 'tryton-cancel'),
Button('Continue', 'generate_labels', 'tryton-go-next', default=True), # noqa
]
)
#: Transition generates shipping labels.
generate_labels = StateTransition()
#: State shows the generated label, tracking number, cost and cost
#: currency.
generate = StateView(
'shipping.label.end',
'shipping.generate_shipping_label_message_view_form',
[
Button('Ok', 'end', 'tryton-ok'),
]
)
@property
def shipment(self):
"Gives the active shipment."
Shipment = Pool().get(Transaction().context.get('active_model'))
return Shipment(Transaction().context.get('active_id'))
@classmethod
def __setup__(cls):
super(GenerateShippingLabel, cls).__setup__()
cls._error_messages.update({
'tracking_number_already_present':
'Tracking Number is already present for this shipment.',
'invalid_state': (
'Labels can only be generated when the shipment is in Packed or'
' Done states only'
),
'no_packages': 'Shipment %s has no packages',
})
def default_start(self, data):
"""Fill the default values for `start` state.
"""
UOM = Pool().get('product.uom')
if self.shipment.allow_label_generation():
values = {
'no_of_packages': len(self.shipment.packages),
'shipping_instructions': self.shipment.shipping_instructions,
}
if self.shipment.carrier:
values.update({
'carrier': self.shipment.carrier.id,
})
if self.shipment.packages:
package_weights = []
for package in self.shipment.packages:
if not package.override_weight:
continue
package_weights.append(
UOM.compute_qty(
package.override_weight_uom,
package.override_weight,
self.shipment.weight_uom
)
)
values['override_weight'] = sum(package_weights)
if self.shipment.carrier_service:
values['carrier_service'] = self.shipment.carrier_service.id
return values
def transition_next(self):
Company = Pool().get('company.company')
shipment = self.shipment
company = Company(Transaction().context['company'])
shipment.carrier = self.start.carrier
shipment.cost_currency = company.currency
shipment.carrier_service = self.start.carrier_service
shipment.save()
if not shipment.packages:
shipment._create_default_package(self.start.box_type)
default_values = self.default_start({})
per_package_weight = None
if self.start.override_weight and \
default_values['override_weight'] != self.start.override_weight:
# Distribute weight equally
per_package_weight = (
self.start.override_weight / len(shipment.packages)
)
for package in shipment.packages:
if per_package_weight:
package.override_weight = per_package_weight
package.override_weight_uom = shipment.weight_uom
if self.start.box_type and self.start.box_type != package.box_type:
package.box_type = self.start.box_type
package.save()
# Fetch rates, and fill selection field with result list
rates = self.shipment.get_shipping_rate(
self.start.carrier, self.start.carrier_service
)
result = []
for rate in rates:
json_safe_rate = rate.copy()
json_safe_rate.update({
'carrier': json_safe_rate['carrier'].id,
'carrier_service': json_safe_rate['carrier_service'] and
json_safe_rate['carrier_service'].id,
'cost_currency': json_safe_rate['cost_currency'].id,
})
# Update when delivery date is not None
if json_safe_rate.get('delivery_date'):
json_safe_rate.update({
'delivery_date': json_safe_rate[
'delivery_date'].isoformat()
})
# Update when delivery time is not None
if json_safe_rate.get('delivery_time'):
json_safe_rate.update({
'delivery_time': json_safe_rate[
'delivery_time'].isoformat()
})
result.append((
json.dumps(json_safe_rate), '%s %s %s' % (
rate['display_name'],
rate['cost'],
rate['cost_currency'].code,
)
))
self.select_rate.__class__.rate.selection = result
return 'select_rate'
def default_select_rate(self, field):
rate = None
if self.select_rate.__class__.rate.selection:
rate = self.select_rate.__class__.rate.selection[0][0]
return {
'rate': rate
}
def transition_generate_labels(self):
"Generates shipping labels from data provided by earlier states"
Carrier = Pool().get('carrier')
CarrierService = Pool().get('carrier.service')
Currency = Pool().get('currency.currency')
if self.select_rate.rate:
rate = json.loads(self.select_rate.rate)
rate.update({
'carrier': Carrier(rate['carrier']),
'carrier_service': rate['carrier_service'] and
CarrierService(rate['carrier_service']),
'cost': Decimal(rate['cost']),
'cost_currency': Currency(rate['cost_currency'])
})
self.shipment.apply_shipping_rate(rate)
self.shipment.generate_shipping_labels()
return "generate"
def get_attachments(self): # pragma: no cover
"""
Returns list of attachments corresponding to shipment.
"""
Attachment = Pool().get('ir.attachment')
return map(
int,
Attachment.search([
('resource.origin.id', 'in', map(int, self.shipment.packages),
'shipment.tracking', 'stock.package')
])
)
def get_message(self):
"""
Returns message to be displayed on wizard
"""
message = 'Shipment labels have been generated via %s and saved as ' \
'attachments for the tracking number' % (
self.shipment.carrier.carrier_cost_method.upper()
)
return message
def default_generate(self, data):
return {
'tracking_number': self.shipment.tracking_number and
self.shipment.tracking_number.id,
'message': self.get_message(),
'attachments': self.get_attachments(),
'cost': self.shipment.cost,
'cost_currency': self.shipment.cost_currency.id,
}