1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# Copyright (C) 2011 MSF, TeMPO Consulting
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU Affero General Public License as
8
# published by the Free Software Foundation, either version 3 of the
9
# License, or (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU Affero General Public License for more details.
16
# You should have received a copy of the GNU Affero General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
##############################################################################
22
from datetime import datetime, timedelta, date
23
from dateutil.relativedelta import relativedelta, relativedelta
24
from osv import osv, fields
25
from osv.orm import browse_record, browse_null
26
from tools.translate import _
28
import decimal_precision as dp
34
from mx import DateTime
37
SHORT_SHELF_LIFE_MESS = 'Product with Short Shelf Life, check the accuracy of the order quantity, frequency and mode of transport.'
40
class sale_order_line(osv.osv):
42
override to add message at sale order creation and update
44
_inherit = 'sale.order.line'
46
def _kc_dg(self, cr, uid, ids, name, arg, context=None):
48
return 'KC' if cold chain or 'DG' if dangerous goods
54
for sol in self.browse(cr, uid, ids, context=context):
56
if sol.product_id.heat_sensitive_item:
58
elif sol.product_id.dangerous_goods:
63
_columns = {'kc_dg': fields.function(_kc_dg, method=True, string='KC/DG', type='char'),}
65
def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
66
uom=False, qty_uos=0, uos=False, name='', partner_id=False,
67
lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
69
if the product is short shelf life we display a warning
72
result = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty,
73
uom, qty_uos, uos, name, partner_id, lang, update_tax, date_order, packaging, fiscal_position, flag)
75
# if the product is short shelf life, display a warning
77
prod_obj = self.pool.get('product.product')
78
if prod_obj.browse(cr, uid, product).short_shelf_life:
80
'title': 'Short Shelf Life product',
81
'message': _(SHORT_SHELF_LIFE_MESS)
83
result.update(warning=warning)
90
class sale_order(osv.osv):
92
add message when so is written, i.e when we add new so lines
94
_inherit = 'sale.order'
96
def write(self, cr, uid, ids, vals, context=None):
98
display message if contains short shelf life
100
if isinstance(ids, (int, long)):
103
for obj in self.browse(cr, uid, ids, context=context):
104
for line in obj.order_line:
106
if line.product_id.short_shelf_life:
108
self.log(cr, uid, obj.id, _(SHORT_SHELF_LIFE_MESS))
110
return super(sale_order, self).write(cr, uid, ids, vals, context=context)
115
class purchase_order_line(osv.osv):
117
override to add message at purchase order creation and update
119
_inherit = 'purchase.order.line'
121
def _kc_dg(self, cr, uid, ids, name, arg, context=None):
123
return 'KC' if cold chain or 'DG' if dangerous goods
129
for pol in self.browse(cr, uid, ids, context=context):
131
if pol.product_id.heat_sensitive_item:
132
result[pol.id] = 'KC'
133
elif pol.product_id.dangerous_goods:
134
result[pol.id] = 'DG'
138
_columns = {'kc_dg': fields.function(_kc_dg, method=True, string='KC/DG', type='char'),}
140
def product_id_change(self, cr, uid, ids, pricelist, product, qty, uom,
141
partner_id, date_order=False, fiscal_position=False, date_planned=False,
142
name=False, price_unit=False, notes=False):
144
if the product is short shelf life we display a warning
147
result = super(purchase_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty, uom,
148
partner_id, date_order, fiscal_position, date_planned,
149
name, price_unit, notes)
151
# if the product is short shelf life, display a warning
153
prod_obj = self.pool.get('product.product')
154
if prod_obj.browse(cr, uid, product).short_shelf_life:
156
'title': 'Short Shelf Life product',
157
'message': _(SHORT_SHELF_LIFE_MESS)
159
result.update(warning=warning)
163
purchase_order_line()
166
class purchase_order(osv.osv):
168
add message when po is written, i.e when we add new po lines
170
no need to modify the wkf_confirm_order as the wrtie method is called during the workflow
172
_inherit = 'purchase.order'
174
def write(self, cr, uid, ids, vals, context=None):
176
display message if contains short shelf life
178
if isinstance(ids, (int, long)):
181
for obj in self.browse(cr, uid, ids, context=context):
182
for line in obj.order_line:
184
if line.product_id.short_shelf_life:
186
self.log(cr, uid, obj.id, _(SHORT_SHELF_LIFE_MESS))
188
return super(purchase_order, self).write(cr, uid, ids, vals, context=context)
193
class stock_warehouse_orderpoint(osv.osv):
197
_inherit = 'stock.warehouse.orderpoint'
200
'name': fields.char('Reference', size=128, required=True, select=True),
201
'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade",
202
domain="[('is_replenishment', '=', warehouse_id)]"),
205
def _check_product_uom(self, cr, uid, ids, context=None):
207
Check if the UoM has the same category as the product standard UoM
212
for rule in self.browse(cr, uid, ids, context=context):
213
if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
219
(_check_product_uom, 'You have to select a product UOM in the same category than the purchase UOM of the product', ['product_id', 'product_uom']),
222
def default_get(self, cr, uid, fields, context=None):
224
Get the default values for the replenishment rule
226
res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context=context)
228
company_id = res.get('company_id')
229
warehouse_id = res.get('warehouse_id')
231
if not 'company_id' in res:
232
company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.automatic.supply', context=context)
233
res.update({'company_id': company_id})
235
if not 'warehouse_id' in res:
236
warehouse_id = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)[0]
237
res.update({'warehouse_id': warehouse_id})
239
if not 'location_id' in res:
240
location_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_stock_id.id
241
res.update({'location_id': location_id})
245
def create(self, cr, uid, vals, context=None):
249
new_id = super(stock_warehouse_orderpoint, self).create(cr, uid, vals, context=context)
251
product_obj = self.pool.get('product.product')
252
product_id = vals.get('product_id', False)
254
if product_obj.browse(cr, uid, product_id, context=context).short_shelf_life:
255
self.log(cr, uid, new_id, _(SHORT_SHELF_LIFE_MESS))
259
def write(self, cr, uid, ids, vals, context=None):
263
result = super(stock_warehouse_orderpoint, self).write(cr, uid, ids, vals, context=context)
265
if isinstance(ids, (int, long)):
268
product_obj = self.pool.get('product.product')
269
product_id = vals.get('product_id', False)
271
if product_obj.browse(cr, uid, product_id, context=context).short_shelf_life:
272
for obj in self.browse(cr, uid, ids, context=context):
273
self.log(cr, uid, obj.id, _(SHORT_SHELF_LIFE_MESS))
277
def onchange_product_id(self, cr, uid, ids, product_id, context=None):
279
Add domain on UoM to have only UoM on the same category of the
282
product_obj = self.pool.get('product.product')
284
res = super(stock_warehouse_orderpoint, self).onchange_product_id(cr, uid, ids, product_id, context=context)
287
# Get the product UoM category
289
product = product_obj.browse(cr, uid, product_id, context=context)
290
domain = {'product_uom': [('category_id', '=', product.uom_id.category_id.id)]}
292
domain = {'product_uom': []}
294
res['value'].update({'product_uom': False})
296
res.update({'value': {'product_uom': False}})
298
# Apply the domain in res
300
res['domain'].update(domain)
302
res.update({'domain': domain})
306
def onchange_uom(self, cr, uid, ids, product_id, uom_id, context=None):
308
Check if the UoM is convertible to product standard UoM
310
if uom_id and product_id:
311
product_obj = self.pool.get('product.product')
312
uom_obj = self.pool.get('product.uom')
314
product = product_obj.browse(cr, uid, product_id, context=context)
315
uom = uom_obj.browse(cr, uid, uom_id, context=context)
317
if product.uom_id.category_id.id != uom.category_id.id:
318
raise osv.except_osv(_('Wrong Product UOM !'), _('You have to select a product UOM in the same category than the purchase UOM of the product'))
323
stock_warehouse_orderpoint()
326
class product_uom(osv.osv):
327
_name = 'product.uom'
328
_inherit = 'product.uom'
330
def _get_uom_by_product(self, cr, uid, ids, field_name, args, context=None):
333
def _search_uom_by_product(self, cr, uid, obj, name, args, context=None):
337
if arg[0] == 'uom_by_product' and arg[1] != '=':
338
raise osv.except_osv(_('Error'), _('Bad comparison operator in domain'))
339
elif arg[0] == 'uom_by_product':
341
if isinstance(product_id, (int, long)):
342
product_id = [product_id]
343
product = self.pool.get('product.product').browse(cr, uid, product_id[0], context=context)
344
dom.append(('category_id', '=', product.uom_id.category_id.id))
349
'uom_by_product': fields.function(_get_uom_by_product, fnct_search=_search_uom_by_product, string='UoM by Product',
350
help='Field used to filter the UoM for a specific product'),
356
class stock_warehouse_automatic_supply(osv.osv):
360
_inherit = 'stock.warehouse.automatic.supply'
362
def create(self, cr, uid, vals, context=None):
366
new_id = super(stock_warehouse_automatic_supply, self).create(cr, uid, vals, context=context)
368
product_obj = self.pool.get('product.product')
369
product_id = vals.get('product_id', False)
371
if product_obj.browse(cr, uid, product_id, context=context).short_shelf_life:
372
self.log(cr, uid, new_id, _(SHORT_SHELF_LIFE_MESS))
376
def write(self, cr, uid, ids, vals, context=None):
380
result = super(stock_warehouse_automatic_supply, self).write(cr, uid, ids, vals, context=context)
382
if isinstance(ids, (int, long)):
385
product_obj = self.pool.get('product.product')
386
product_id = vals.get('product_id', False)
388
if product_obj.browse(cr, uid, product_id, context=context).short_shelf_life:
389
for obj in self.browse(cr, uid, ids, context=context):
390
self.log(cr, uid, obj.id, _(SHORT_SHELF_LIFE_MESS))
394
stock_warehouse_automatic_supply()
397
class stock_warehouse_order_cycle(osv.osv):
401
_inherit = 'stock.warehouse.order.cycle'
403
def create(self, cr, uid, vals, context=None):
407
new_id = super(stock_warehouse_order_cycle, self).create(cr, uid, vals, context=context)
409
product_obj = self.pool.get('product.product')
410
product_id = vals.get('product_id', False)
412
if product_obj.browse(cr, uid, product_id, context=context).short_shelf_life:
413
self.log(cr, uid, new_id, _(SHORT_SHELF_LIFE_MESS))
417
def write(self, cr, uid, ids, vals, context=None):
424
result = super(stock_warehouse_order_cycle, self).write(cr, uid, ids, vals, context=context)
426
if isinstance(ids, (int, long)):
429
product_obj = self.pool.get('product.product')
430
product_id = vals.get('product_id', False)
432
if product_obj.browse(cr, uid, product_id, context=context).short_shelf_life:
433
for obj in self.browse(cr, uid, ids, context=context):
434
self.log(cr, uid, obj.id, _(SHORT_SHELF_LIFE_MESS))
438
stock_warehouse_order_cycle()
441
class stock_picking(osv.osv):
445
_inherit = 'stock.picking'
447
def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
449
hook to update defaults data
451
# variable parameters
452
move = kwargs.get('move')
453
assert move, 'missing move'
454
partial_datas = kwargs.get('partial_datas')
455
assert partial_datas, 'missing partial_datas'
457
# calling super method
458
defaults = super(stock_picking, self)._do_partial_hook(cr, uid, ids, context, *args, **kwargs)
459
assetId = partial_datas.get('move%s'%(move.id), {}).get('asset_id')
461
defaults.update({'asset_id': assetId})
470
class stock_move(osv.osv):
474
_inherit = 'stock.move'
476
def create(self, cr, uid, vals, context=None):
478
complete info normally generated by javascript on_change function
480
id_cross = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_cross_docking', 'stock_location_cross_docking')[1]
481
prod_obj = self.pool.get('product.product')
482
if vals.get('product_id', False):
483
# complete hidden flags - needed if not created from GUI
484
product = prod_obj.browse(cr, uid, vals.get('product_id'), context=context)
486
if vals.get('picking_id') and product.type == 'consu' and vals.get('location_dest_id') != id_cross:
487
pick_bro = self.pool.get('stock.picking').browse(cr, uid, vals.get('picking_id'))
488
id_nonstock = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock_override', 'stock_location_non_stockable')
489
if vals.get('sale_line_id'):
490
id_pack = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'stock_location_packing')
491
if pick_bro.type == 'out':
492
vals.update(location_id=id_cross)
494
vals.update(location_id=id_nonstock[1])
495
vals.update(location_dest_id=id_pack[1])
497
if pick_bro.type != 'out':
498
vals.update(location_dest_id=id_nonstock[1])
500
if product.batch_management:
501
vals.update(hidden_batch_management_mandatory=True)
502
elif product.perishable:
503
vals.update(hidden_perishable_mandatory=True)
505
vals.update(hidden_batch_management_mandatory=False,
506
hidden_perishable_mandatory=False,
509
result = super(stock_move, self).create(cr, uid, vals, context=context)
512
def write(self, cr, uid, ids, vals, context=None):
514
complete info normally generated by javascript on_change function
516
id_cross = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_cross_docking', 'stock_location_cross_docking')[1]
517
prod_obj = self.pool.get('product.product')
519
if vals.get('product_id', False):
520
# complete hidden flags - needed if not created from GUI
521
product = prod_obj.browse(cr, uid, vals.get('product_id'), context=context)
523
if vals.get('picking_id') and product.type == 'consu' and vals.get('location_dest_id') != id_cross:
524
pick_bro = self.pool.get('stock.picking').browse(cr, uid, vals.get('picking_id'))
525
id_nonstock = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock_override', 'stock_location_non_stockable')
526
if vals.get('sale_line_id'):
527
id_pack = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'stock_location_packing')
528
if pick_bro.type == 'out':
529
vals.update(location_id=id_cross)
531
vals.update(location_id=id_nonstock[1])
532
vals.update(location_dest_id=id_pack[1])
534
if pick_bro.type != 'out':
535
vals.update(location_dest_id=id_nonstock[1])
537
if product.batch_management:
538
vals.update(hidden_batch_management_mandatory=True)
539
elif product.perishable:
540
vals.update(hidden_perishable_mandatory=True)
542
vals.update(hidden_batch_management_mandatory=False,
543
hidden_perishable_mandatory=False,
546
result = super(stock_move, self).write(cr, uid, ids, vals, context=context)
549
def _kc_dg(self, cr, uid, ids, name, arg, context=None):
551
return 'KC' if cold chain or 'DG' if dangerous goods
557
for move in self.browse(cr, uid, ids, context=context):
559
if move.product_id.heat_sensitive_item:
560
result[move.id] = 'KC'
561
elif move.product_id.dangerous_goods:
562
result[move.id] = 'DG'
566
def _check_batch_management(self, cr, uid, ids, context=None):
568
check for batch management
569
@return: True or False
571
for move in self.browse(cr, uid, ids, context=context):
572
if move.state == 'done' and move.location_id.id != move.location_dest_id.id:
573
if move.product_id.batch_management:
574
if not move.prodlot_id and move.product_qty:
575
raise osv.except_osv(_('Error!'), _('You must assign a Batch Number for this product (Batch Number Mandatory).'))
578
def _check_perishable(self, cr, uid, ids, context=None):
581
@return: True or False
583
for move in self.browse(cr, uid, ids, context=context):
584
if move.state == 'done' and move.location_id.id != move.location_dest_id.id:
585
if move.product_id.perishable:
586
if not move.prodlot_id and move.product_qty:
587
raise osv.except_osv(_('Error!'), _('You must assign an Expiry Date for this product (Expiry Date Mandatory).'))
590
def _check_prodlot_need(self, cr, uid, ids, context=None):
592
If the move has a prodlot but does not need one, return False.
594
for move in self.browse(cr, uid, ids, context=context):
596
if not move.product_id.perishable and not move.product_id.batch_management:
597
raise osv.except_osv(_('Error!'), _('The selected product is neither Batch Number Mandatory nor Expiry Date Mandatory.'))
600
def _check_prodlot_need_batch_management(self, cr, uid, ids, context=None):
602
If the product is batch management while the selected prodlot is 'internal'.
604
for move in self.browse(cr, uid, ids, context=context):
606
if move.prodlot_id.type == 'internal' and move.product_id.batch_management:
607
raise osv.except_osv(_('Error!'), _('The selected product is Batch Number Mandatory while the selected Batch number corresponds to Expiry Date Mandatory.'))
610
def _check_prodlot_need_perishable(self, cr, uid, ids, context=None):
612
If the product is perishable ONLY while the selected prodlot is 'standard'.
614
for move in self.browse(cr, uid, ids, context=context):
616
if move.prodlot_id.type == 'standard' and not move.product_id.batch_management and move.product_id.perishable:
617
raise osv.except_osv(_('Error!'), _('The selected product is Expiry Date Mandatory while the selected Batch number corresponds to Batch Number Mandatory.'))
620
def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, address_id=False, parent_type=False, purchase_line_id=False, out=False,):
622
the product changes, set the hidden flag if necessary
624
result = super(stock_move, self).onchange_product_id(cr, uid, ids, prod_id, loc_id,
625
loc_dest_id, address_id)
627
# product changes, prodlot is always cleared
628
result.setdefault('value', {})['prodlot_id'] = False
629
# reset the hidden flag
630
result.setdefault('value', {})['hidden_batch_management_mandatory'] = False
631
result.setdefault('value', {})['hidden_perishable_mandatory'] = False
633
product = self.pool.get('product.product').browse(cr, uid, prod_id)
635
if product.batch_management:
636
result.setdefault('value', {})['hidden_batch_management_mandatory'] = True
637
result['warning'] = {'title': _('Info'),
638
'message': _('The selected product is Batch Management.')}
640
elif product.perishable:
641
result.setdefault('value', {})['hidden_perishable_mandatory'] = True
642
result['warning'] = {'title': _('Info'),
643
'message': _('The selected product is Perishable.')}
644
# quantities are set to False
645
result.setdefault('value', {}).update({'product_qty': 0.00,
646
'product_uos_qty': 0.00,
651
def _get_checks_all(self, cr, uid, ids, name, arg, context=None):
653
function for KC/SSL/DG/NP products
656
kit_obj = self.pool.get('composition.kit')
661
result[id].update({f: False})
663
for obj in self.browse(cr, uid, ids, context=context):
665
if obj.product_id.heat_sensitive_item:
666
result[obj.id]['kc_check'] = True
668
if obj.product_id.short_shelf_life:
669
result[obj.id]['ssl_check'] = True
671
if obj.product_id.dangerous_goods:
672
result[obj.id]['dg_check'] = True
674
if obj.product_id.narcotic:
675
result[obj.id]['np_check'] = True
677
if obj.product_id.batch_management:
678
result[obj.id]['lot_check'] = True
679
# expiry date management
680
if obj.product_id.perishable:
681
result[obj.id]['exp_check'] = True
682
# contains a kit and allow the creation of a new composition LIst
683
# will be false if the kit is batch management and a composition list already uses this batch number
684
# only one composition list can use a given batch number for a given product
685
if obj.product_id.type == 'product' and obj.product_id.subtype == 'kit':
687
# search if composition list already use this batch number
688
kit_ids = kit_obj.search(cr, uid, [('composition_lot_id', '=', obj.prodlot_id.id)], context=context)
690
result[obj.id]['kit_check'] = True
692
# not batch management, we can create as many composition list as we want
693
result[obj.id]['kit_check'] = True
697
def _check_tracking(self, cr, uid, ids, context=None):
698
""" Checks if production lot is assigned to stock move or not.
699
@return: True or False
701
for move in self.browse(cr, uid, ids, context=context):
702
if not move.prodlot_id and move.product_qty and \
703
(move.state == 'done' and \
705
(move.product_id.track_production and move.location_id.usage == 'production') or \
706
(move.product_id.track_production and move.location_dest_id.usage == 'production') or \
707
(move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
708
(move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') \
710
raise osv.except_osv(_('Error!'), _('You must assign a batch number for this product.'))
714
'kc_dg': fields.function(_kc_dg, method=True, string='KC/DG', type='char'),
715
# if prodlot needs to be mandatory, add 'required': ['|', ('hidden_batch_management_mandatory','=',True), ('hidden_perishable_mandatory','=',True)] in attrs
716
'hidden_batch_management_mandatory': fields.boolean(string='Hidden Flag for Batch Management product',),
717
'hidden_perishable_mandatory': fields.boolean(string='Hidden Flag for Perishable product',),
718
'kc_check': fields.function(_get_checks_all, method=True, string='KC', type='boolean', readonly=True, multi="m"),
719
'ssl_check': fields.function(_get_checks_all, method=True, string='SSL', type='boolean', readonly=True, multi="m"),
720
'dg_check': fields.function(_get_checks_all, method=True, string='DG', type='boolean', readonly=True, multi="m"),
721
'np_check': fields.function(_get_checks_all, method=True, string='NP', type='boolean', readonly=True, multi="m"),
722
'lot_check': fields.function(_get_checks_all, method=True, string='B.Num', type='boolean', readonly=True, multi="m"),
723
'exp_check': fields.function(_get_checks_all, method=True, string='Exp', type='boolean', readonly=True, multi="m"),
724
'kit_check': fields.function(_get_checks_all, method=True, string='Kit', type='boolean', readonly=True, multi="m"),
725
'prodlot_id': fields.many2one('stock.production.lot', 'Batch', states={'done': [('readonly', True)]}, help="Batch number is used to put a serial number on the production", select=True),
728
_constraints = [(_check_batch_management,
729
'You must assign a Batch Number for this product (Batch Number Mandatory).',
732
'You must assign an Expiry Date for this product (Expiry Date Mandatory).',
734
(_check_prodlot_need,
735
'The selected product is neither Batch Number Mandatory nor Expiry Date Mandatory.',
737
(_check_prodlot_need_batch_management,
738
'The selected product is Batch Number Mandatory while the selected Batch number corresponds to Expiry Date Mandatory.',
740
(_check_prodlot_need_perishable,
741
'The selected product is Expiry Date Mandatory while the selected Batch number corresponds to Batch Number Mandatory.',
744
'You must assign a batch number for this product.',
751
class stock_production_lot(osv.osv):
753
productin lot modifications
755
_inherit = 'stock.production.lot'
757
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
759
Correct fields in order to have those from account_statement_from_invoice_lines (in case where account_statement_from_invoice is used)
764
# warehouse wizards or inventory screen
765
if view_type == 'tree' and ((context.get('expiry_date_check', False) and not context.get('batch_number_check', False)) or context.get('hidden_perishable_mandatory', False)):
766
view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'specific_rules', 'view_production_lot_expiry_date_tree')
769
result = super(stock_production_lot, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
772
def copy(self, cr, uid, id, default=None, context=None):
774
increase the batch number
775
create a new sequence
781
lot_name = self.read(cr, uid, id, ['name'])['name']
782
default.update(name='%s (copy)'%lot_name, date=time.strftime('%Y-%m-%d'))
784
return super(stock_production_lot, self).copy(cr, uid, id, default, context=context)
786
def copy_data(self, cr, uid, id, default=None, context=None):
792
default.update(revisions=[])
793
return super(stock_production_lot, self).copy_data(cr, uid, id, default, context=context)
795
def create_sequence(self, cr, uid, vals, context=None):
797
Create new entry sequence for every new order
798
@param cr: cursor to database
799
@param user: id of current user
800
@param ids: list of record ids to be process
801
@param context: context arguments, like lang, time zone
802
@return: return a result
804
seq_pool = self.pool.get('ir.sequence')
805
seq_typ_pool = self.pool.get('ir.sequence.type')
807
name = 'Batch number'
808
code = 'stock.production.lot'
814
seq_typ_pool.create(cr, uid, types)
822
return seq_pool.create(cr, uid, seq)
824
def create(self, cr, uid, vals, context=None):
826
create the sequence for the version management
831
sequence = self.create_sequence(cr, uid, vals, context=context)
832
vals.update({'sequence_id': sequence,})
834
if context.get('update_mode') in ['init', 'update']:
835
if not vals.get('life_date'):
836
# default value to today
837
vals.update(life_date=time.strftime('%Y-%m-%d'))
839
return super(stock_production_lot, self).create(cr, uid, vals, context=context)
841
def write(self, cr, uid, ids, vals, context=None):
843
update the sequence for the version management
845
if isinstance(ids, (int, long)):
848
revision_obj = self.pool.get('stock.production.lot.revision')
850
for lot in self.browse(cr, uid, ids, context=context):
851
# create revision object for each lot
852
version_number = lot.sequence_id.get_id(test='id', context=context)
853
values = {'name': 'Auto Revision Logging',
854
'description': 'The batch number has been modified, this revision log has been created automatically.',
855
'date': time.strftime('%Y-%m-%d'),
856
'indice': version_number,
859
revision_obj.create(cr, uid, values, context=context)
861
return super(stock_production_lot, self).write(cr, uid, ids, vals, context=context)
863
def remove_flag(self, flag, _list):
865
if we do not remove the flag, we fall into an infinite loop
873
def search_check_type(self, cr, uid, obj, name, args, context=None):
875
modify the query to take the type of prodlot into account according to product's attributes
876
'Batch Number mandatory' and 'Expiry Date Mandatory'
878
if batch management: display only 'standard' lot
879
if expiry and not batch management: display only 'internal' lot
880
else: display normally
882
product_obj = self.pool.get('product.product')
883
product_id = context.get('product_id', False)
885
# remove flag avoid infinite loop
886
args = self.remove_flag('check_type', args)
892
product = product_obj.browse(cr, uid, product_id, context=context)
894
if product.batch_management:
896
args.append(('type', '=', 'standard'))
897
elif product.perishable:
899
args.append(('type', '=', 'internal'))
903
def _get_false(self, cr, uid, ids, field_name, arg, context=None):
905
return false for each id
907
if isinstance(ids,(long, int)):
915
def _stock_search_virtual(self, cr, uid, obj, name, args, context=None):
916
""" Searches Ids of products
917
@return: Ids of locations
921
# when the location_id = False results now in showing stock for all internal locations
922
# *previously*, was showing the location of no location (= 0.0 for all prodlot)
923
if 'location_id' not in context or not context['location_id']:
924
locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
926
locations = context['location_id'] and [context['location_id']] or []
928
ids = [('id', 'in', [])]
934
stock_report_prodlots_virtual
936
location_id IN %s group by prodlot_id
937
having sum(qty) '''+ str(args[0][1]) + str(args[0][2]),(tuple(locations),))
939
ids = [('id', 'in', map(lambda x: x[0], res))]
942
def _stock_search(self, cr, uid, obj, name, args, context=None):
944
call super method, as fields.function does not work with inheritance
946
return super(stock_production_lot, self)._stock_search(cr, uid, obj, name, args, context=context)
948
def _get_stock_virtual(self, cr, uid, ids, field_name, arg, context=None):
949
""" Gets stock of products for locations
950
@return: Dictionary of values
954
# when the location_id = False results now in showing stock for all internal locations
955
# *previously*, was showing the location of no location (= 0.0 for all prodlot)
956
if 'location_id' not in context or not context['location_id']:
957
locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
959
locations = context['location_id'] and [context['location_id']] or []
961
if isinstance(ids, (int, long)):
964
res = {}.fromkeys(ids, 0.0)
970
stock_report_prodlots_virtual
972
location_id IN %s and prodlot_id IN %s group by prodlot_id''',(tuple(locations),tuple(ids),))
973
res.update(dict(cr.fetchall()))
977
def _get_stock(self, cr, uid, ids, field_name, arg, context=None):
979
call super method, as fields.function does not work with inheritance
981
return super(stock_production_lot, self)._get_stock(cr, uid, ids, field_name, arg, context=context)
983
def _get_checks_all(self, cr, uid, ids, name, arg, context=None):
985
function for KC/SSL/DG/NP products
991
result[id].update({f: False})
993
for obj in self.browse(cr, uid, ids, context=context):
995
if obj.product_id.heat_sensitive_item:
996
result[obj.id]['kc_check'] = True
998
if obj.product_id.short_shelf_life:
999
result[obj.id]['ssl_check'] = True
1001
if obj.product_id.dangerous_goods:
1002
result[obj.id]['dg_check'] = True
1004
if obj.product_id.narcotic:
1005
result[obj.id]['np_check'] = True
1007
if obj.product_id.batch_management:
1008
result[obj.id]['lot_check'] = True
1009
# expiry date management
1010
if obj.product_id.perishable:
1011
result[obj.id]['exp_check'] = True
1015
def _check_batch_type_integrity(self, cr, uid, ids, context=None):
1017
Check if the type of the batch is consistent with the product attributes
1019
for obj in self.browse(cr, uid, ids, context=context):
1020
if obj.type == 'standard' and not obj.product_id.batch_management:
1025
def _check_perishable_type_integrity(self, cr, uid, ids, context=None):
1027
Check if the type of the batch is consistent with the product attributes
1029
for obj in self.browse(cr, uid, ids, context=context):
1030
if obj.type == 'internal' and (obj.product_id.batch_management or not obj.product_id.perishable):
1035
def _get_delete_ok(self, cr, uid, ids, field_name, args, context=None):
1037
Returns if the batch is deletable
1040
for batch_id in ids:
1041
res[batch_id] = True
1042
move_ids = self.pool.get('stock.move').search(cr, uid, [('prodlot_id', '=', batch_id)], context=context)
1044
res[batch_id] = False
1048
_columns = {'check_type': fields.function(_get_false, fnct_search=search_check_type, string='Check Type', type="boolean", readonly=True, method=True),
1049
# readonly is True, the user is only allowed to create standard lots - internal lots are system-created
1050
'type': fields.selection([('standard', 'Standard'),('internal', 'Internal'),], string="Type", readonly=True),
1051
#'expiry_date': fields.date('Expiry Date'),
1052
'name': fields.char('Batch Number', size=1024, required=True, help="Unique batch number, will be displayed as: PREFIX/SERIAL [INT_REF]"),
1053
'date': fields.datetime('Auto Creation Date', required=True),
1054
'sequence_id': fields.many2one('ir.sequence', 'Batch Sequence', required=True,),
1055
'stock_virtual': fields.function(_get_stock_virtual, method=True, type="float", string="Available Stock", select=True,
1056
help="Current available quantity of products with this Batch Numbre Number in company warehouses",
1057
digits_compute=dp.get_precision('Product UoM'), readonly=True,
1058
fnct_search=_stock_search_virtual,),
1059
'stock_available': fields.function(_get_stock, fnct_search=_stock_search, method=True, type="float", string="Real Stock", select=True,
1060
help="Current real quantity of products with this Batch Number in company warehouses",
1061
digits_compute=dp.get_precision('Product UoM')),
1062
'kc_check': fields.function(_get_checks_all, method=True, string='KC', type='boolean', readonly=True, multi="m"),
1063
'ssl_check': fields.function(_get_checks_all, method=True, string='SSL', type='boolean', readonly=True, multi="m"),
1064
'dg_check': fields.function(_get_checks_all, method=True, string='DG', type='boolean', readonly=True, multi="m"),
1065
'np_check': fields.function(_get_checks_all, method=True, string='NP', type='boolean', readonly=True, multi="m"),
1066
'lot_check': fields.function(_get_checks_all, method=True, string='B.Num', type='boolean', readonly=True, multi="m"),
1067
'exp_check': fields.function(_get_checks_all, method=True, string='Exp', type='boolean', readonly=True, multi="m"),
1068
'delete_ok': fields.function(_get_delete_ok, method=True, string='Possible deletion ?', type='boolean', readonly=True),
1071
_defaults = {'type': 'standard',
1072
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'stock.production.lot', context=c),
1077
_sql_constraints = [('name_uniq', 'unique (product_id,name)', 'For a given product, the batch number must be unique.'),
1080
_constraints = [(_check_batch_type_integrity,
1081
'You can\'t create a standard batch number for a product which is not batch mandatory. If the product is perishable, the system will create automatically an internal batch number on reception/inventory.',
1082
['Type', 'Product']),
1083
(_check_perishable_type_integrity,
1084
'You can\'t create an internal Batch Number for a product which is batch managed or which is not perishable. If the product is batch managed, please create a standard batch number.',
1085
['Type', 'Product']),
1088
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1090
search function of production lot
1092
result = super(stock_production_lot, self).search(cr, uid, args=args, offset=offset, limit=limit, order=order, context=context, count=count)
1096
def name_get(self, cr, uid, ids, context=None):
1102
reads = self.read(cr, uid, ids, ['name', 'prefix', 'ref', 'life_date'], context)
1104
# TODO replace by _get_format in uf-651
1105
if context.get('with_expiry'):
1106
user_obj = self.pool.get('res.users')
1107
lang_obj = self.pool.get('res.lang')
1108
user_lang = user_obj.read(cr, uid, uid, ['context_lang'], context=context)['context_lang']
1109
lang_id = lang_obj.search(cr, uid, [('code','=',user_lang)])
1110
date_format = lang_id and lang_obj.read(cr, uid, lang_id[0], ['date_format'], context=context)['date_format'] or '%m/%d/%Y'
1112
for record in reads:
1113
if context.get('with_expiry') and record['life_date']:
1114
name = '%s - %s'%(record['name'], DateTime.strptime(record['life_date'],'%Y-%m-%d').strftime(date_format).decode('utf-8'))
1116
name = record['name']
1117
res.append((record['id'], name))
1120
def unlink(self, cr, uid, ids, context=None):
1124
for batch in self.browse(cr, uid, ids, context=context):
1125
if not batch.delete_ok:
1126
raise osv.except_osv(_('Error'), _('You cannot remove a batch number which has stock !'))
1128
return super(stock_production_lot, self).unlink(cr, uid, batch.id, context=context)
1131
stock_production_lot()
1134
class stock_location(osv.osv):
1136
override stock location to add:
1140
_inherit = 'stock.location'
1142
def replace_field_key(self, fieldsDic, search, replace):
1144
will replace 'stock_real' by 'stock_real_specific'
1145
and 'stock_virtual' by 'stock_virtual_specific'
1147
and return a new dictionary
1149
return dict((replace if key == search else key, (self.replace_field_key(value, search, replace) if isinstance(value, dict) else value)) for key, value in fieldsDic.items())
1151
def _product_value_specific_rules(self, cr, uid, ids, field_names, arg, context=None):
1153
add two fields for custom stock computation, if no product selected, both stock are set to 0.0
1161
for f in field_names:
1162
result[id].update({f: False,})
1163
# if product is set to False, it does not make sense to return a stock value, return False for each location
1164
if 'product_id' in context and not context['product_id']:
1167
result = super(stock_location, self)._product_value(cr, uid, ids, ['stock_real', 'stock_virtual'], arg, context=context)
1168
# replace stock real
1169
result = self.replace_field_key(result, 'stock_real', 'stock_real_specific')
1170
# replace stock virtual
1171
result = self.replace_field_key(result, 'stock_virtual', 'stock_virtual_specific')
1174
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1176
display the modified stock values (stock_real_specific, stock_virtual_specific) if needed
1180
# warehouse wizards or inventory screen
1181
if view_type == 'tree' and context.get('specific_rules_tree_view', False):
1182
view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'specific_rules', 'view_location_tree2')
1185
result = super(osv.osv, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
1188
_columns = {'stock_real_specific': fields.function(_product_value_specific_rules, method=True, type='float', string='Real Stock', multi="get_vals_specific_rules"),
1189
'stock_virtual_specific': fields.function(_product_value_specific_rules, method=True, type='float', string='Virtual Stock', multi="get_vals_specific_rules"),
1195
class stock_production_lot_revision(osv.osv):
1196
_inherit = 'stock.production.lot.revision'
1197
_order = 'indice desc'
1199
stock_production_lot_revision()
1202
class stock_inventory(osv.osv):
1204
override the action_confirm to create the production lot if needed
1206
_inherit = 'stock.inventory'
1208
def _check_line_data(self, cr, uid, ids, context=None):
1209
for inv in self.browse(cr, uid, ids, context=context):
1210
if inv.state not in ('draft', 'cancel'):
1211
for line in inv.inventory_line_id:
1212
if line.product_qty != 0.00 and not line.location_id:
1217
def copy(self, cr, uid, inventory_id, defaults, context=None):
1219
Set the creation date of the document to the current date
1224
defaults.update({'date': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': False})
1225
return super(stock_inventory, self).copy(cr, uid, inventory_id, defaults, context=context)
1228
'sublist_id': fields.many2one('product.list', string='List/Sublist'),
1229
'nomen_manda_0': fields.many2one('product.nomenclature', 'Main Type'),
1230
'nomen_manda_1': fields.many2one('product.nomenclature', 'Group'),
1231
'nomen_manda_2': fields.many2one('product.nomenclature', 'Family'),
1232
'nomen_manda_3': fields.many2one('product.nomenclature', 'Root'),
1236
(_check_line_data, "You must define a stock location for each line", ['state']),
1239
def onChangeSearchNomenclature(self, cr, uid, ids, position, n_type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=True, context=None):
1240
return self.pool.get('product.product').onChangeSearchNomenclature(cr, uid, 0, position, n_type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, False, context={'withnum': 1})
1242
def fill_lines(self, cr, uid, ids, context=None):
1244
Fill all lines according to defined nomenclature level and sublist
1246
line_obj = self.pool.get('stock.inventory.line')
1247
product_obj = self.pool.get('product.product')
1252
discrepancy_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_discrepancy')[1]
1254
for inv in self.browse(cr, uid, ids, context=context):
1259
# Get all products for the defined nomenclature
1260
if inv.nomen_manda_3:
1261
nom = inv.nomen_manda_3.id
1262
field = 'nomen_manda_3'
1263
elif inv.nomen_manda_2:
1264
nom = inv.nomen_manda_2.id
1265
field = 'nomen_manda_2'
1266
elif inv.nomen_manda_1:
1267
nom = inv.nomen_manda_1.id
1268
field = 'nomen_manda_1'
1269
elif inv.nomen_manda_0:
1270
nom = inv.nomen_manda_0.id
1271
field = 'nomen_manda_0'
1273
product_ids.extend(self.pool.get('product.product').search(cr, uid, [(field, '=', nom)], context=context))
1275
# Get all products for the defined list
1277
for line in inv.sublist_id.product_ids:
1278
product_ids.append(line.name.id)
1280
for product in product_obj.browse(cr, uid, product_ids, context=context):
1281
# Check if the product is not already in the list
1282
if product.type not in ('consu', 'service', 'service_recep') and\
1283
not line_obj.search(cr, uid, [('inventory_id', '=', inv.id),
1284
('product_id', '=', product.id),
1285
('product_uom', '=', product.uom_id.id)], context=context):
1286
line_obj.create(cr, uid, {'inventory_id': inv.id,
1287
'product_id': product.id,
1288
'reason_type_id': discrepancy_id,
1289
'product_uom': product.uom_id.id}, context=context)
1293
def get_nomen(self, cr, uid, ids, field):
1294
return self.pool.get('product.nomenclature').get_nomen(cr, uid, self, ids, field, context={'withnum': 1})
1296
def _hook_dont_move(self, cr, uid, *args, **kwargs):
1297
res = super(stock_inventory, self)._hook_dont_move(cr, uid, *args, **kwargs)
1298
if 'line' in kwargs:
1299
return res and not kwargs['line'].dont_move
1303
def action_confirm(self, cr, uid, ids, context=None):
1305
if the line is perishable without prodlot, we create the prodlot
1307
prodlot_obj = self.pool.get('stock.production.lot')
1308
product_obj = self.pool.get('product.product')
1309
# treat the needed production lot
1310
for obj in self.browse(cr, uid, ids, context=context):
1311
for line in obj.inventory_line_id:
1312
if self._name == 'initial.stock.inventory' and line.product_qty == 0.00:
1313
line.write({'dont_move': True})
1315
if line.hidden_perishable_mandatory and not line.expiry_date:
1316
raise osv.except_osv(_('Error'), _('The product %s is perishable but the line with this product has no expiry date') % product_obj.name_get(cr, uid, [line.product_id.id])[0][1])
1317
if line.hidden_batch_management_mandatory and not line.prod_lot_id:
1318
raise osv.except_osv(_('Error'), _('The product %s is batch mandatory but the line with this product has no batch') % product_obj.name_get(cr, uid, [line.product_id.id])[0][1])
1319
# if perishable product
1320
if line.hidden_perishable_mandatory and not line.hidden_batch_management_mandatory:
1322
assert line.product_id.perishable, 'product is not perishable but line is'
1323
assert line.expiry_date, 'expiry date is not set'
1324
# if no production lot, we create a new one
1325
if not line.prod_lot_id:
1326
# double check to find the corresponding prodlot
1327
prodlot_ids = prodlot_obj.search(cr, uid, [('life_date', '=', line.expiry_date),
1328
('type', '=', 'internal'),
1329
('product_id', '=', line.product_id.id)], context=context)
1330
# no prodlot, create a new one
1332
vals = {'product_id': line.product_id.id,
1333
'life_date': line.expiry_date,
1334
'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.serial'),
1337
prodlot_id = prodlot_obj.create(cr, uid, vals, context=context)
1339
prodlot_id = prodlot_ids[0]
1341
line.write({'prod_lot_id': prodlot_id,},)
1343
# super function after production lot creation - production lot are therefore taken into account at stock move creation
1344
result = super(stock_inventory, self).action_confirm(cr, uid, ids, context=context)
1350
class stock_inventory_line(osv.osv):
1352
add mandatory or readonly behavior to prodlot
1354
_inherit = 'stock.inventory.line'
1355
_rec_name = 'product_id'
1357
def common_on_change(self, cr, uid, ids, location_id, product, prod_lot_id, uom=False, to_date=False, result=None):
1359
commmon qty computation
1365
product_obj = self.pool.get('product.product').browse(cr, uid, product)
1366
uom = uom or product_obj.uom_id.id
1367
stock_context = {'uom': uom, 'to_date': to_date,
1368
'prodlot_id':prod_lot_id,}
1370
# if a location is specified, we do not list the children locations, otherwise yes
1371
stock_context.update({'compute_child': False,})
1372
amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], stock_context)[product]
1373
result.setdefault('value', {}).update({'product_qty': amount, 'product_uom': uom})
1376
def change_lot(self, cr, uid, ids, location_id, product, prod_lot_id, uom=False, to_date=False,):
1378
prod lot changes, update the expiry date
1380
prodlot_obj = self.pool.get('stock.production.lot')
1381
result = {'value':{}}
1382
# reset expiry date or fill it
1384
result['value'].update(expiry_date=prodlot_obj.browse(cr, uid, prod_lot_id).life_date)
1386
result['value'].update(expiry_date=False)
1388
result = self.common_on_change(cr, uid, ids, location_id, product, prod_lot_id, uom, to_date, result=result)
1391
def change_expiry(self, cr, uid, id, expiry_date, product_id, type_check, context=None):
1393
expiry date changes, find the corresponding internal prod lot
1395
prodlot_obj = self.pool.get('stock.production.lot')
1396
result = {'value':{}}
1398
if expiry_date and product_id:
1399
prod_ids = prodlot_obj.search(cr, uid, [('life_date', '=', expiry_date),
1400
('type', '=', 'internal'),
1401
('product_id', '=', product_id)], context=context)
1403
if type_check == 'in':
1404
# the corresponding production lot will be created afterwards
1405
result['warning'] = {'title': _('Info'),
1406
'message': _('The selected Expiry Date does not exist in the system. It will be created during validation process.')}
1408
result['value'].update(prod_lot_id=False)
1411
result['warning'] = {'title': _('Error'),
1412
'message': _('The selected Expiry Date does not exist in the system.')}
1414
result['value'].update(expiry_date=False, prod_lot_id=False)
1416
# return first prodlot
1417
result['value'].update(prod_lot_id=prod_ids[0])
1419
# clear expiry date, we clear production lot
1420
result['value'].update(prod_lot_id=False,
1425
def on_change_location_id(self, cr, uid, ids, location_id, product, prod_lot_id, uom=False, to_date=False,):
1426
""" Changes UoM and name if product_id changes.
1427
@param location_id: Location id
1428
@param product: Changed product_id
1429
@param uom: UoM product
1430
@return: Dictionary of changed values
1435
result.setdefault('value', {}).update({'product_qty': 0.0,})
1438
result = self.common_on_change(cr, uid, ids, location_id, product, prod_lot_id, uom, to_date, result=result)
1441
def on_change_product_id_specific_rules(self, cr, uid, ids, location_id, product, prod_lot_id, uom=False, to_date=False,):
1443
the product changes, set the hidden flag if necessary
1445
result = super(stock_inventory_line, self).on_change_product_id(cr, uid, ids, location_id, product, uom, to_date)
1446
# product changes, prodlot is always cleared
1447
result.setdefault('value', {})['prod_lot_id'] = False
1448
result.setdefault('value', {})['expiry_date'] = False
1449
# reset the hidden flags
1450
result.setdefault('value', {})['hidden_batch_management_mandatory'] = False
1451
result.setdefault('value', {})['hidden_perishable_mandatory'] = False
1453
product_obj = self.pool.get('product.product').browse(cr, uid, product)
1454
if product_obj.batch_management:
1455
result.setdefault('value', {})['hidden_batch_management_mandatory'] = True
1456
elif product_obj.perishable:
1457
result.setdefault('value', {})['hidden_perishable_mandatory'] = True
1458
# if not product, result is 0.0 by super
1460
result = self.common_on_change(cr, uid, ids, location_id, product, prod_lot_id, uom, to_date, result=result)
1463
def create(self, cr, uid, vals, context=None):
1465
complete info normally generated by javascript on_change function
1467
prod_obj = self.pool.get('product.product')
1468
if vals.get('product_id', False):
1469
# complete hidden flags - needed if not created from GUI
1470
product = prod_obj.browse(cr, uid, vals.get('product_id'), context=context)
1471
if product.batch_management:
1472
vals.update(hidden_batch_management_mandatory=True)
1473
elif product.perishable:
1474
vals.update(hidden_perishable_mandatory=True)
1476
vals.update(hidden_batch_management_mandatory=False,
1477
hidden_perishable_mandatory=False,
1479
# complete expiry date from production lot - needed if not created from GUI
1480
prodlot_obj = self.pool.get('stock.production.lot')
1481
if vals.get('prod_lot_id', False):
1482
vals.update(expiry_date=prodlot_obj.browse(cr, uid, vals.get('prod_lot_id'), context=context).life_date)
1484
result = super(stock_inventory_line, self).create(cr, uid, vals, context=context)
1487
def write(self, cr, uid, ids, vals, context=None):
1489
complete info normally generated by javascript on_change function
1491
prod_obj = self.pool.get('product.product')
1492
if vals.get('product_id', False):
1493
# complete hidden flags - needed if not created from GUI
1494
product = prod_obj.browse(cr, uid, vals.get('product_id'), context=context)
1495
if product.batch_management:
1496
vals.update(hidden_batch_management_mandatory=True)
1497
elif product.perishable:
1498
vals.update(hidden_perishable_mandatory=True)
1500
vals.update(hidden_batch_management_mandatory=False,
1501
hidden_perishable_mandatory=False,
1503
# complete expiry date from production lot - needed if not created from GUI
1504
prodlot_obj = self.pool.get('stock.production.lot')
1505
if vals.get('prod_lot_id', False):
1506
vals.update(expiry_date=prodlot_obj.browse(cr, uid, vals.get('prod_lot_id'), context=context).life_date)
1509
result = super(stock_inventory_line, self).write(cr, uid, ids, vals, context=context)
1512
def _get_checks_all(self, cr, uid, ids, name, arg, context=None):
1514
function for KC/SSL/DG/NP products
1520
result[id].update({f: False,})
1522
for obj in self.browse(cr, uid, ids, context=context):
1524
if obj.product_id.heat_sensitive_item:
1525
result[obj.id]['kc_check'] = True
1527
if obj.product_id.short_shelf_life:
1528
result[obj.id]['ssl_check'] = True
1530
if obj.product_id.dangerous_goods:
1531
result[obj.id]['dg_check'] = True
1533
if obj.product_id.narcotic:
1534
result[obj.id]['np_check'] = True
1536
if obj.product_id.batch_management:
1537
result[obj.id]['lot_check'] = True
1538
# expiry date management
1539
if obj.product_id.perishable:
1540
result[obj.id]['exp_check'] = True
1543
# Line will be displayed in red if it's not correct
1544
result[obj.id]['has_problem'] = False
1545
if not obj.location_id \
1546
or not self._check_perishable(cr, uid, [obj.id]) \
1547
or not self._check_batch_management(cr, uid, [obj.id]):
1548
result[obj.id]['has_problem'] = True
1552
def _check_batch_management(self, cr, uid, ids, context=None):
1554
check for batch management
1556
for obj in self.browse(cr, uid, ids, context=context):
1557
if obj.inventory_id.state not in ('draft', 'cancel') and obj.product_id.batch_management:
1558
if not obj.prod_lot_id or obj.prod_lot_id.type != 'standard':
1562
def _check_perishable(self, cr, uid, ids, context=None):
1564
check for perishable ONLY
1566
for obj in self.browse(cr, uid, ids, context=context):
1567
if obj.inventory_id.state not in ('draft', 'cancel') and obj.product_id.perishable and not obj.product_id.batch_management:
1568
if (not obj.prod_lot_id and not obj.expiry_date) or (obj.prod_lot_id and obj.prod_lot_id.type != 'internal'):
1572
def _check_prodlot_need(self, cr, uid, ids, context=None):
1574
If the inv line has a prodlot but does not need one, return False.
1576
for obj in self.browse(cr, uid, ids, context=context):
1578
if not obj.product_id.perishable and not obj.product_id.batch_management:
1583
'hidden_perishable_mandatory': fields.boolean(string='Hidden Flag for Perishable product',),
1584
'hidden_batch_management_mandatory': fields.boolean(string='Hidden Flag for Batch Management product',),
1585
# Remove the 'required' attribute on location_id to allow the possiblity to fill lines with list or nomenclature
1586
# The required attribute is True on the XML view
1587
'location_id': fields.many2one('stock.location', 'Location'),
1588
'prod_lot_id': fields.many2one('stock.production.lot', 'Batch', domain="[('product_id','=',product_id)]"),
1589
'expiry_date': fields.date(string='Expiry Date'),
1590
'type_check': fields.char(string='Type Check', size=1024,),
1591
'kc_check': fields.function(_get_checks_all, method=True, string='KC', type='boolean', readonly=True, multi="m"),
1592
'ssl_check': fields.function(_get_checks_all, method=True, string='SSL', type='boolean', readonly=True, multi="m"),
1593
'dg_check': fields.function(_get_checks_all, method=True, string='DG', type='boolean', readonly=True, multi="m"),
1594
'np_check': fields.function(_get_checks_all, method=True, string='NP', type='boolean', readonly=True, multi="m"),
1595
'lot_check': fields.function(_get_checks_all, method=True, string='B.Num', type='boolean', readonly=True, multi="m"),
1596
'exp_check': fields.function(_get_checks_all, method=True, string='Exp', type='boolean', readonly=True, multi="m"),
1597
'has_problem': fields.function(_get_checks_all, method=True, string='Has problem', type='boolean', readonly=True, multi="m"),
1598
'dont_move': fields.boolean(string='Don\'t create stock.move for this line'),
1601
_defaults = {# in is used, meaning a new prod lot will be created if the specified expiry date does not exist
1603
'dont_move': lambda *a: False,
1606
_constraints = [(_check_batch_management,
1607
'You must assign a Batch Number which corresponds to Batch Number Mandatory Products.',
1610
'You must assign a Batch Numbre which corresponds to Expiry Date Mandatory Products.',
1612
(_check_prodlot_need,
1613
'The selected product is neither Batch Number Mandatory nor Expiry Date Mandatory',
1617
stock_inventory_line()
1619
class report_stock_inventory(osv.osv):
1621
UF-565: add group by expired_date
1623
_inherit = "report.stock.inventory"
1626
tools.drop_view_if_exists(cr, 'report_stock_inventory')
1628
CREATE OR REPLACE view report_stock_inventory AS (
1630
min(m.id) as id, m.date as date,
1631
m.expired_date as expired_date,
1632
m.address_id as partner_id, m.location_id as location_id,
1633
m.product_id as product_id, pt.categ_id as product_categ_id, l.usage as location_type,
1635
m.state as state, m.prodlot_id as prodlot_id,
1636
coalesce(sum(-pt.standard_price * m.product_qty)::decimal, 0.0) as value,
1637
CASE when pt.uom_id = m.product_uom
1639
coalesce(sum(-m.product_qty)::decimal, 0.0)
1641
coalesce(sum(-m.product_qty * pu.factor)::decimal, 0.0) END as product_qty
1644
LEFT JOIN stock_picking p ON (m.picking_id=p.id)
1645
LEFT JOIN product_product pp ON (m.product_id=pp.id)
1646
LEFT JOIN product_template pt ON (pp.product_tmpl_id=pt.id)
1647
LEFT JOIN product_uom pu ON (pt.uom_id=pu.id)
1648
LEFT JOIN product_uom u ON (m.product_uom=u.id)
1649
LEFT JOIN stock_location l ON (m.location_id=l.id)
1651
m.id, m.product_id, m.product_uom, pt.categ_id, m.address_id, m.location_id, m.location_dest_id,
1652
m.prodlot_id, m.expired_date, m.date, m.state, l.usage, m.company_id,pt.uom_id
1655
-m.id as id, m.date as date,
1656
m.expired_date as expired_date,
1657
m.address_id as partner_id, m.location_dest_id as location_id,
1658
m.product_id as product_id, pt.categ_id as product_categ_id, l.usage as location_type,
1660
m.state as state, m.prodlot_id as prodlot_id,
1661
coalesce(sum(pt.standard_price * m.product_qty )::decimal, 0.0) as value,
1662
CASE when pt.uom_id = m.product_uom
1664
coalesce(sum(m.product_qty)::decimal, 0.0)
1666
coalesce(sum(m.product_qty * pu.factor)::decimal, 0.0) END as product_qty
1669
LEFT JOIN stock_picking p ON (m.picking_id=p.id)
1670
LEFT JOIN product_product pp ON (m.product_id=pp.id)
1671
LEFT JOIN product_template pt ON (pp.product_tmpl_id=pt.id)
1672
LEFT JOIN product_uom pu ON (pt.uom_id=pu.id)
1673
LEFT JOIN product_uom u ON (m.product_uom=u.id)
1674
LEFT JOIN stock_location l ON (m.location_dest_id=l.id)
1676
m.id, m.product_id, m.product_uom, pt.categ_id, m.address_id, m.location_id, m.location_dest_id,
1677
m.prodlot_id, m.expired_date, m.date, m.state, l.usage, m.company_id,pt.uom_id
1683
'prodlot_id': fields.many2one('stock.production.lot', 'Batch', readonly=True),
1684
'expired_date': fields.date(string='Expiry Date',),
1687
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1692
context['with_expiry'] = 1
1693
return super(report_stock_inventory, self).read(cr, uid, ids, fields, context, load)
1695
def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1697
UF-1546: This method is to remove the lines that have quantity = 0 from the list view
1699
res = super(report_stock_inventory, self).read_group(cr, uid, domain, fields, groupby, offset, limit, context, orderby)
1700
if self._name == 'report.stock.inventory' and res:
1701
return [data for data in res if data.get('product_qty', 10) != 0.0]
1704
report_stock_inventory()
1706
class product_product(osv.osv):
1707
_inherit = 'product.product'
1708
def open_stock_by_location(self, cr, uid, ids, context=None):
1712
ctx = {'product_id': context.get('active_id') , 'compute_child': False}
1713
if context.get('lang'):
1714
ctx['lang'] = context['lang']
1716
name = _('Stock by Location')
1718
prod = self.pool.get('product.product').read(cr, uid, ids[0], ['name', 'code'], context=ctx)
1719
name = "%s: [%s] %s"%(name, prod['code'], prod['name'])
1720
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock_override', 'view_location_tree_tree')[1]
1723
'type': 'ir.actions.act_window',
1724
'res_model': 'stock.location',
1725
'view_type': 'tree',
1726
'view_id': [view_id],
1727
'domain': [('location_id','=',False)],
1728
'view_mode': 'tree',
1730
'target': 'current',