1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 TeMPO Consulting, MSF
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Affero General Public License as
9
# published by the Free Software Foundation, either version 3 of the
10
# License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Affero General Public License for more details.
17
# You should have received a copy of the GNU Affero General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
22
from osv import osv, fields
28
from tools.translate import _
29
from dateutil.relativedelta import relativedelta
30
from datetime import datetime
31
from decimal import Decimal, ROUND_UP
37
define getter for date / time / datetime formats
41
def _get_format(self, cr, uid, type, context=None):
47
type = type + '_format'
48
assert type in self._columns, 'Specified format field does not exist'
49
user_obj = self.pool.get('res.users')
50
# get user context lang
51
user_lang = user_obj.read(cr, uid, uid, ['context_lang'], context=context)['context_lang']
53
lang_id = self.search(cr, uid, [('code','=',user_lang)])
54
# return format value or from default function if not exists
55
format = lang_id and self.read(cr, uid, lang_id[0], [type], context=context)[type] or getattr(self, '_get_default_%s'%type)(cr, uid, context=context)
58
def _get_db_format(self, cr, uid, type, context=None):
60
generic function - for now constant values
73
class date_tools(osv.osv):
75
date related tools for msf project
79
def get_date_format(self, cr, uid, context=None):
81
get the date format for the uid specified user
83
from msf_order_date module
85
lang_obj = self.pool.get('res.lang')
86
return lang_obj._get_format(cr, uid, 'date', context=context)
88
def get_db_date_format(self, cr, uid, context=None):
92
lang_obj = self.pool.get('res.lang')
93
return lang_obj._get_db_format(cr, uid, 'date', context=context)
95
def get_time_format(self, cr, uid, context=None):
97
get the time format for the uid specified user
99
from msf_order_date module
101
lang_obj = self.pool.get('res.lang')
102
return lang_obj._get_format(cr, uid, 'time', context=context)
104
def get_db_time_format(self, cr, uid, context=None):
106
return constant value
108
lang_obj = self.pool.get('res.lang')
109
return lang_obj._get_db_format(cr, uid, 'time', context=context)
111
def get_datetime_format(self, cr, uid, context=None):
113
get the datetime format for the uid specified user
115
return self.get_date_format(cr, uid, context=context) + ' ' + self.get_time_format(cr, uid, context=context)
117
def get_db_datetime_format(self, cr, uid, context=None):
119
return constant value
121
return self.get_db_date_format(cr, uid, context=context) + ' ' + self.get_db_time_format(cr, uid, context=context)
123
def get_date_formatted(self, cr, uid, d_type='date', datetime=None, context=None):
125
Return the datetime in the format of the user
126
@param d_type: 'date' or 'datetime' : determines which is the out format
127
@param datetime: date to format
129
assert d_type in ('date', 'datetime'), 'Give only \'date\' or \'datetime\' as type parameter'
132
datetime = time.strftime('%Y-%m-%d')
135
d_format = self.get_date_format(cr, uid)
136
date = time.strptime(datetime, '%Y-%m-%d')
137
return time.strftime(d_format, date)
138
elif d_type == 'datetime':
139
d_format = self.get_datetime_format(cr, uid)
140
date = time.strptime(datetime, '%Y-%m-%d %H:%M:%S')
141
return time.strftime(d_format, date)
146
class fields_tools(osv.osv):
148
date related tools for msf project
150
_name = 'fields.tools'
152
def get_field_from_company(self, cr, uid, object=False, field=False, context=None):
154
return the value for field from company for object
156
# field is required for value
160
company_obj = self.pool.get('res.company')
161
# corresponding company
162
company_id = company_obj._company_default_get(cr, uid, object, context=context)
164
res = company_obj.read(cr, uid, [company_id], [field], context=context)[0][field]
167
def get_selection_name(self, cr, uid, object=False, field=False, key=False, context=None):
169
return the name from the key of selection field
171
if not object or not field or not key:
173
# get the selection values list
174
if isinstance(object, str):
175
object = self.pool.get(object)
176
list = object._columns[field].selection
177
name = [x[1] for x in list if x[0] == key][0]
180
def get_ids_from_browse_list(self, cr, uid, browse_list=False, context=None):
182
return the list of ids corresponding to browse list in parameter
187
result = [x.id for x in browse_list]
193
class data_tools(osv.osv):
195
data related tools for msf project
199
def load_common_data(self, cr, uid, ids, context=None):
201
load common data into context
205
context.setdefault('common', {})
207
date_tools = self.pool.get('date.tools')
208
obj_data = self.pool.get('ir.model.data')
209
comp_obj = self.pool.get('res.company')
211
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
212
context['common']['db_date_format'] = db_date_format
213
date_format = date_tools.get_date_format(cr, uid, context=context)
214
context['common']['date_format'] = date_format
216
date = time.strftime(db_date_format)
217
context['common']['date'] = date
219
company_id = comp_obj._company_default_get(cr, uid, 'stock.picking', context=context)
220
context['common']['company_id'] = company_id
223
stock_id = obj_data.get_object_reference(cr, uid, 'stock', 'stock_location_stock')[1]
224
context['common']['stock_id'] = stock_id
226
kitting_id = obj_data.get_object_reference(cr, uid, 'stock', 'location_production')[1]
227
context['common']['kitting_id'] = kitting_id
229
input_id = obj_data.get_object_reference(cr, uid, 'msf_cross_docking', 'stock_location_input')[1]
230
context['common']['input_id'] = input_id
232
quarantine_anal = obj_data.get_object_reference(cr, uid, 'stock_override', 'stock_location_quarantine_analyze')[1]
233
context['common']['quarantine_anal'] = quarantine_anal
234
# quarantine before scrap
235
quarantine_scrap = obj_data.get_object_reference(cr, uid, 'stock_override', 'stock_location_quarantine_scrap')[1]
236
context['common']['quarantine_scrap'] = quarantine_scrap
238
log = obj_data.get_object_reference(cr, uid, 'stock_override', 'stock_location_logistic')[1]
239
context['common']['log'] = log
241
cross_docking = obj_data.get_object_reference(cr, uid, 'msf_cross_docking', 'stock_location_cross_docking')[1]
242
context['common']['cross_docking'] = cross_docking
245
reason_type_id = obj_data.get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_kit')[1]
246
context['common']['reason_type_id'] = reason_type_id
247
# reason type goods return
248
rt_goods_return = obj_data.get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_goods_return')[1]
249
context['common']['rt_goods_return'] = rt_goods_return
250
# reason type goods replacement
251
rt_goods_replacement = obj_data.get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_goods_replacement')[1]
252
context['common']['rt_goods_replacement'] = rt_goods_replacement
253
# reason type internal supply
254
rt_internal_supply = obj_data.get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
255
context['common']['rt_internal_supply'] = rt_internal_supply
262
class sequence_tools(osv.osv):
266
_name = 'sequence.tools'
268
def reorder_sequence_number(self, cr, uid, base_object, base_seq_field, dest_object, foreign_field, foreign_ids, seq_field, context=None):
270
receive a browse list corresponding to one2many lines
271
recompute numbering corresponding to specified field
272
compute next number of sequence
274
we must make sure we reorder in conservative way according to original order
281
if isinstance(foreign_ids, (int, long)):
282
foreign_ids = [foreign_ids]
285
base_obj = self.pool.get(base_object)
286
dest_obj = self.pool.get(dest_object)
287
seq_obj = self.pool.get('ir.sequence')
289
for foreign_id in foreign_ids:
290
# will be ordered by default according to db id, it's what we want according to user sequence
291
item_ids = dest_obj.search(cr, uid, [(foreign_field, '=', foreign_id)], context=context)
293
# read line number and id from items
294
item_data = dest_obj.read(cr, uid, item_ids, [seq_field], context=context)
295
# check the line number: data are ordered according to db id, so line number must be equal to index+1
296
for i in range(len(item_data)):
297
if item_data[i][seq_field] != i+1:
298
dest_obj.write(cr, uid, [item_data[i]['id']], {seq_field: i+1}, context=context)
299
# reset sequence to length + 1 all time, checking if needed would take much time
300
# get the sequence id
301
seq_id = base_obj.read(cr, uid, foreign_id, [base_seq_field], context=context)[base_seq_field][0]
302
# we reset the sequence to length+1
303
self.reset_next_number(cr, uid, [seq_id], value=len(item_ids)+1, context=context)
307
def reorder_sequence_number_from_unlink(self, cr, uid, ids, base_object, base_seq_field, dest_object, foreign_field, seq_field, context=None):
309
receive a browse list corresponding to one2many lines
310
recompute numbering corresponding to specified field
311
compute next number of sequence
313
for unlink, only items with id > min(deleted id) are resequenced + reset the sequence value
315
we must make sure we reorder in conservative way according to original order
317
this method is called from methods of **destination object**
322
# if no ids as parameter return Tru
327
base_obj = self.pool.get(base_object)
328
dest_obj = self.pool.get(dest_object)
329
seq_obj = self.pool.get('ir.sequence')
331
# find the corresponding base ids
332
base_ids = [x[foreign_field][0] for x in dest_obj.read(cr, uid, ids, [foreign_field], context=context) if x[foreign_field]]
333
# simulate unique sql
334
foreign_ids = set(base_ids)
336
for foreign_id in foreign_ids:
337
# will be ordered by default according to db id, it's what we want according to user sequence
338
# reorder only ids bigger than min deleted + do not select deleted ones
339
item_ids = dest_obj.search(cr, uid, [('id', '>', min(ids)), (foreign_field, '=', foreign_id), ('id', 'not in', ids)], context=context)
340
# start numbering sequence
342
# if deleted object is not the first one, we find the numbering value of previous one
343
before_ids = dest_obj.search(cr, uid, [('id', '<', min(ids)), (foreign_field, '=', foreign_id)], context=context)
345
# we read the numbering value of previous value (biggest id)
346
start_num = dest_obj.read(cr, uid, max(before_ids), [seq_field], context=context)[seq_field]
348
# read line number and id from items
349
item_data = dest_obj.read(cr, uid, item_ids, [seq_field], context=context)
350
# check the line number: data are ordered according to db id, so line number must be equal to index+1
351
for i in range(len(item_data)):
353
start_num = start_num+1
354
if item_data[i][seq_field] != start_num:
355
cr.execute("update "+dest_obj._table+" set "+seq_field+"=%s where id=%s", (start_num, item_data[i]['id']))
356
#dest_obj.write(cr, uid, [item_data[i]['id']], {seq_field: start_num}, context=context)
358
# reset sequence to start_num + 1 all time, checking if needed would take much time
359
# get the sequence id
360
seq_id = base_obj.read(cr, uid, foreign_id, [base_seq_field], context=context)[base_seq_field][0]
361
# we reset the sequence to length+1, whether or not items
362
self.reset_next_number(cr, uid, [seq_id], value=start_num+1, context=context)
366
def reset_next_number(self, cr, uid, seq_ids, value=1, context=None):
368
reset the next number of the sequence to value, default value 1
373
if isinstance(seq_ids, (int, long)):
377
seq_obj = self.pool.get('ir.sequence')
378
seq_obj.write(cr, uid, seq_ids, {'number_next': value}, context=context)
381
def create_sequence(self, cr, uid, vals, name, code, prefix='', padding=0, context=None):
383
create a new sequence
385
seq_pool = self.pool.get('ir.sequence')
386
seq_typ_pool = self.pool.get('ir.sequence.type')
388
assert name, 'create sequence: missing name'
389
assert code, 'create sequence: missing code'
391
types = {'name': name,
394
seq_typ_pool.create(cr, uid, types)
401
return seq_pool.create(cr, uid, seq)
406
class picking_tools(osv.osv):
408
picking related tools
410
_name = 'picking.tools'
412
def confirm(self, cr, uid, ids, context=None):
419
if isinstance(ids, (int, long)):
423
pick_obj = self.pool.get('stock.picking')
424
pick_obj.draft_force_assign(cr, uid, ids, context)
427
def check_assign(self, cr, uid, ids, context=None):
429
check assign the picking
434
if isinstance(ids, (int, long)):
438
pick_obj = self.pool.get('stock.picking')
439
pick_obj.action_assign(cr, uid, ids, context)
442
def force_assign(self, cr, uid, ids, context=None):
444
force assign the picking
449
if isinstance(ids, (int, long)):
453
pick_obj = self.pool.get('stock.picking')
454
pick_obj.force_assign(cr, uid, ids, context)
457
def validate(self, cr, uid, ids, context=None):
464
if isinstance(ids, (int, long)):
468
pick_obj = self.pool.get('stock.picking')
469
wf_service = netsvc.LocalService("workflow")
470
# trigger standard workflow for validated picking ticket
472
pick_obj.action_move(cr, uid, [id])
473
wf_service.trg_validate(uid, 'stock.picking', id, 'button_done', cr)
476
def all(self, cr, uid, ids, context=None):
478
confirm - check - validate
480
self.confirm(cr, uid, ids, context=context)
481
self.check_assign(cr, uid, ids, context=context)
482
self.validate(cr, uid, ids, context=context)
488
class ir_translation(osv.osv):
489
_name = 'ir.translation'
490
_inherit = 'ir.translation'
492
def tr_view(self, cr, name, context):
493
if not context or not context.get('lang'):
495
tr = self._get_source(cr, 1, False, 'view', context['lang'], name, True)
497
# sometimes de view name is empty and so the action name is used as view name
498
tr2 = self._get_source(cr, 1, 'ir.actions.act_window,name', 'model', context['lang'], name)
508
class uom_tools(osv.osv_memory):
510
This osv_memory class helps to check certain consistency related to the UOM.
514
def check_uom(self, cr, uid, product_id, uom_id, context=None):
516
Check the consistency between the category of the UOM of a product and the category of a UOM.
517
Return a boolean value (if false, it will raise an error).
518
:param cr: database cursor
519
:param product_id: takes the id of a product
520
:param product_id: takes the id of a uom
521
Note that this method is not consistent with the onchange method that returns a dictionary.
525
uom_obj = self.pool.get('product.uom')
526
product_obj = self.pool.get('product.product')
527
if product_id and uom_id:
528
if isinstance(product_id, (int, long)):
529
product_id = [product_id]
530
if isinstance(uom_id, (int, long)):
532
if not product_obj.browse(cr, uid, product_id, context)[0].uom_id.category_id.id == uom_obj.browse(cr, uid, uom_id, context)[0].category_id.id:
539
class product_uom(osv.osv):
540
_inherit = 'product.uom'
542
def _compute_round_up_qty(self, cr, uid, uom_id, qty, context=None):
544
Round up the qty according to the UoM
546
uom = self.browse(cr, uid, uom_id, context=context)
547
rounding_value = Decimal(str(uom.rounding).rstrip('0'))
549
return float(Decimal(str(qty)).quantize(rounding_value, rounding=ROUND_UP))
551
def _change_round_up_qty(self, cr, uid, uom_id, qty, fields=[], result=None, context=None):
553
Returns the error message and the rounded value
556
result = {'value': {}, 'warning': {}}
558
if isinstance(fields, str):
561
message = {'title': _('Bad rounding'),
562
'message': _('The quantity entered is not valid according to the rounding value of the UoM. The product quantity has been rounded to the highest good value.')}
565
new_qty = self._compute_round_up_qty(cr, uid, uom_id, qty, context=context)
568
result.setdefault('value', {}).update({f: new_qty})
569
result.setdefault('warning', {}).update(message)