31
31
import decimal_precision as dp
35
from msf_partner import PARTNER_TYPE
37
#----------------------------------------------------------
39
#----------------------------------------------------------
40
class procurement_order(osv.osv):
41
_name = 'procurement.order'
42
_inherit = 'procurement.order'
44
def create(self, cr, uid, vals, context=None):
46
create method for filling flag from yml tests
50
if context.get('update_mode') in ['init', 'update'] and 'from_yml_test' not in vals:
51
logging.getLogger('init').info('PRO: set from yml test to True')
52
vals['from_yml_test'] = True
53
return super(procurement_order, self).create(cr, uid, vals, context=context)
55
def action_confirm(self, cr, uid, ids, context=None):
56
""" Confirms procurement and writes exception message if any.
59
move_obj = self.pool.get('stock.move')
60
for procurement in self.browse(cr, uid, ids, context=context):
61
if procurement.product_qty <= 0.00:
62
raise osv.except_osv(_('Data Insufficient !'),
63
_('Please check the Quantity in Procurement Order(s), it should not be less than 1!'))
64
if procurement.product_id.type in ('product', 'consu'):
65
if not procurement.move_id:
66
source = procurement.location_id.id
67
reason_type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_other')[1]
68
if procurement.procure_method == 'make_to_order':
69
reason_type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_external_supply')[1]
70
source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
71
id = move_obj.create(cr, uid, {
72
'name': procurement.name,
73
'location_id': source,
74
'location_dest_id': procurement.location_id.id,
75
'product_id': procurement.product_id.id,
76
'product_qty': procurement.product_qty,
77
'product_uom': procurement.product_uom.id,
78
'date_expected': procurement.date_planned,
80
'company_id': procurement.company_id.id,
81
'auto_validate': True,
82
'reason_type_id': reason_type_id,
84
move_obj.action_confirm(cr, uid, [id], context=context)
85
self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move': 1})
86
self.write(cr, uid, ids, {'state': 'confirmed', 'message': ''})
89
def copy_data(self, cr, uid, id, default=None, context=None):
91
reset link to purchase order from update of on order purchase order
95
default.update({'so_back_update_dest_po_id_procurement_order': False,
96
'so_back_update_dest_pol_id_procurement_order': False})
97
return super(procurement_order, self).copy_data(cr, uid, id, default, context=context)
99
_columns = {'from_yml_test': fields.boolean('Only used to pass addons unit test', readonly=True, help='Never set this field to true !'),
100
# this field is used when the po is modified during on order process, and the so must be modified accordingly
101
# the resulting new purchase order line will be merged in specified po_id
102
'so_back_update_dest_po_id_procurement_order': fields.many2one('purchase.order', string='Destination of new purchase order line', readonly=True),
103
'so_back_update_dest_pol_id_procurement_order': fields.many2one('purchase.order.line', string='Original purchase order line', readonly=True),
106
_defaults = {'from_yml_test': lambda *a: False,
36
112
#----------------------------------------------------------
40
116
_inherit = "stock.picking"
41
117
_description = "Picking List"
44
def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
46
hook to update defaults data
119
def _hook_state_list(self, cr, uid, *args, **kwargs):
121
Change terms into states list
123
state_list = kwargs['state_list']
125
state_list['done'] = _('is closed.')
129
def _get_stock_picking_from_partner_ids(self, cr, uid, ids, context=None):
131
ids represents the ids of res.partner objects for which values have changed
133
return the list of ids of stock.picking objects which need to get their fields updated
135
self is res.partner object
140
if isinstance(ids, (int, long)):
143
pick_obj = self.pool.get('stock.picking')
144
result = pick_obj.search(cr, uid, [('partner_id2', 'in', ids)], context=context)
147
def _vals_get_stock_ov(self, cr, uid, ids, fields, arg, context=None):
149
multi fields function method
154
if isinstance(ids, (int, long)):
158
for obj in self.browse(cr, uid, ids, context=context):
161
result[obj.id].update({f:False})
163
result[obj.id].update({'partner_type_stock_picking': obj.partner_id2.partner_type})
167
def _get_inactive_product(self, cr, uid, ids, field_name, args, context=None):
169
for pick in self.browse(cr, uid, ids, context=context):
171
for line in pick.move_lines:
172
if line.inactive_product:
179
'state': fields.selection([
182
('confirmed', 'Confirmed'),
183
('assigned', 'Available'),
185
('cancel', 'Cancelled'),
186
], 'State', readonly=True, select=True,
187
help="* Draft: not confirmed yet and will not be scheduled until confirmed\n"\
188
"* Confirmed: still waiting for the availability of products\n"\
189
"* Available: products reserved, simply waiting for confirmation.\n"\
190
"* Waiting: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n"\
191
"* Closed: has been processed, can't be modified or cancelled anymore\n"\
192
"* Cancelled: has been cancelled, can't be confirmed anymore"),
193
'from_yml_test': fields.boolean('Only used to pass addons unit test', readonly=True, help='Never set this field to true !'),
194
'address_id': fields.many2one('res.partner.address', 'Delivery address', help="Address of partner", readonly=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain="[('partner_id', '=', partner_id)]"),
195
'partner_id2': fields.many2one('res.partner', 'Partner', required=False),
196
'from_wkf': fields.boolean('From wkf'),
197
'update_version_from_in_stock_picking': fields.integer(string='Update version following IN processing'),
198
'partner_type_stock_picking': fields.function(_vals_get_stock_ov, method=True, type='selection', selection=PARTNER_TYPE, string='Partner Type', multi='get_vals_stock_ov', readonly=True, select=True,
199
store= {'stock.picking': (lambda self, cr, uid, ids, c=None: ids, ['partner_id2'], 10),
200
'res.partner': (_get_stock_picking_from_partner_ids, ['partner_type'], 10),}),
201
'inactive_product': fields.function(_get_inactive_product, method=True, type='boolean', string='Product is inactive', store=False),
202
'fake_type': fields.selection([('out', 'Sending Goods'), ('in', 'Getting Goods'), ('internal', 'Internal')], 'Shipping Type', required=True, select=True, help="Shipping type specify, goods coming in or going out."),
205
_defaults = {'from_yml_test': lambda *a: False,
206
'from_wkf': lambda *a: False,
207
'update_version_from_in_stock_picking': 0,
211
def _check_active_product(self, cr, uid, ids, context=None):
213
Check if the stock picking contains a line with an inactive products
215
inactive_lines = self.pool.get('stock.move').search(cr, uid, [('product_id.active', '=', False),
216
('picking_id', 'in', ids),
217
('picking_id.state', 'not in', ['draft', 'cancel', 'done'])], context=context)
220
plural = len(inactive_lines) == 1 and _('A product has') or _('Some products have')
221
l_plural = len(inactive_lines) == 1 and _('line') or _('lines')
222
p_plural = len(inactive_lines) == 1 and _('this inactive product') or _('those inactive products')
223
raise osv.except_osv(_('Error'), _('%s been inactivated. If you want to validate this document you have to remove/correct the %s containing %s (see red %s of the document)') % (plural, l_plural, p_plural, l_plural))
228
(_check_active_product, "You cannot validate this document because it contains a line with an inactive product", ['order_line', 'state'])
231
def create(self, cr, uid, vals, context=None):
233
create method for filling flag from yml tests
238
if not context.get('active_id',False):
239
vals['from_wkf'] = True
240
# in case me make a copy of a stock.picking coming from a workflow
241
if context.get('not_workflow', False):
242
vals['from_wkf'] = False
244
if context.get('update_mode') in ['init', 'update'] and 'from_yml_test' not in vals:
245
logging.getLogger('init').info('PICKING: set from yml test to True')
246
vals['from_yml_test'] = True
248
if not vals.get('partner_id2') and vals.get('address_id'):
249
addr = self.pool.get('res.partner.address').browse(cr, uid, vals.get('address_id'), context=context)
250
vals['partner_id2'] = addr.partner_id and addr.partner_id.id or False
252
if not vals.get('address_id') and vals.get('partner_id2'):
253
addr = self.pool.get('res.partner').address_get(cr, uid, vals.get('partner_id2'), ['delivery', 'default'])
254
if not addr.get('delivery'):
255
vals['address_id'] = addr.get('default')
257
vals['address_id'] = addr.get('delivery')
259
return super(stock_picking, self).create(cr, uid, vals, context=context)
261
def write(self, cr, uid, ids, vals, context=None):
263
Update the partner or the address according to the other
265
if isinstance(ids,(int, long)):
268
if not vals.get('address_id') and vals.get('partner_id2'):
269
for pick in self.browse(cr, uid, ids, context=context):
270
if pick.partner_id.id != vals.get('partner_id2'):
271
addr = self.pool.get('res.partner').address_get(cr, uid, vals.get('partner_id2'), ['delivery', 'default'])
272
if not addr.get('delivery'):
273
vals['address_id'] = addr.get('default')
275
vals['address_id'] = addr.get('delivery')
277
if not vals.get('partner_id2') and vals.get('address_id'):
278
for pick in self.browse(cr, uid, ids, context=context):
279
if pick.address_id.id != vals.get('address_id'):
280
addr = self.pool.get('res.partner.address').browse(cr, uid, vals.get('address_id'), context=context)
281
vals['partner_id2'] = addr.partner_id and addr.partner_id.id or False
283
return super(stock_picking, self).write(cr, uid, ids, vals, context=context)
285
def on_change_partner(self, cr, uid, ids, partner_id, address_id, context=None):
287
Change the delivery address when the partner change.
293
v.update({'address_id': False})
295
d.update({'address_id': [('partner_id', '=', partner_id)]})
299
addr = self.pool.get('res.partner.address').browse(cr, uid, address_id, context=context)
301
if not address_id or addr.partner_id.id != partner_id:
302
addr = self.pool.get('res.partner').address_get(cr, uid, partner_id, ['delivery', 'default'])
303
if not addr.get('delivery'):
304
addr = addr.get('default')
306
addr = addr.get('delivery')
308
v.update({'address_id': addr})
314
def set_manually_done(self, cr, uid, ids, all_doc=True, context=None):
316
Set the picking to done
320
if isinstance(ids, (int, long)):
323
for pick in self.browse(cr, uid, ids, context=context):
324
for move in pick.move_lines:
325
if move.state not in ('cancel', 'done'):
326
move_ids.append(move.id)
328
#Set all stock moves to done
329
self.pool.get('stock.move').set_manually_done(cr, uid, move_ids, all_doc=all_doc, context=context)
333
def _do_partial_hook(self, cr, uid, ids, context=None, *args, **kwargs):
335
Please copy this to your module's method also.
336
This hook belongs to the do_partial method from stock_override>stock.py>stock_picking
338
- allow to modify the defaults data for move creation and copy
48
340
defaults = kwargs.get('defaults')
49
341
assert defaults is not None, 'missing defaults'
345
def _picking_done_cond(self, cr, uid, ids, context=None, *args, **kwargs):
347
Please copy this to your module's method also.
348
This hook belongs to the do_partial method from stock_override>stock.py>stock_picking
350
- allow to conditionally execute the picking processing to done
354
def _custom_code(self, cr, uid, ids, context=None, *args, **kwargs):
356
Please copy this to your module's method also.
357
This hook belongs to the do_partial method from stock_override>stock.py>stock_picking
359
- allow to execute specific custom code before processing picking to done
360
- no supposed to modify partial_datas
54
364
# @@@override stock>stock.py>stock_picking>do_partial
55
365
def do_partial(self, cr, uid, ids, partial_datas, context=None):
210
545
# @@@override end
547
# @@@override stock>stock.py>stock_picking>_get_invoice_type
548
def _get_invoice_type(self, pick):
549
src_usage = dest_usage = None
551
if pick.invoice_state == '2binvoiced':
553
src_usage = pick.move_lines[0].location_id.usage
554
dest_usage = pick.move_lines[0].location_dest_id.usage
555
if pick.type == 'out' and dest_usage == 'supplier':
556
inv_type = 'in_refund'
557
elif pick.type == 'out' and dest_usage == 'customer':
558
inv_type = 'out_invoice'
559
elif (pick.type == 'in' and src_usage == 'supplier') or (pick.type == 'internal'):
560
inv_type = 'in_invoice'
561
elif pick.type == 'in' and src_usage == 'customer':
562
inv_type = 'out_refund'
564
inv_type = 'out_invoice'
567
def _hook_get_move_ids(self, cr, uid, *args, **kwargs):
568
move_obj = self.pool.get('stock.move')
569
pick = kwargs['pick']
570
move_ids = move_obj.search(cr, uid, [('picking_id', '=', pick.id),
571
('state', 'in', ('waiting', 'confirmed'))], order='product_qty desc')
575
def is_invoice_needed(self, cr, uid, sp=None):
577
Check if invoice is needed. Cases where we do not need invoice:
578
- OUT from scratch (without purchase_id and sale_id) AND stock picking type in internal, external or esc
579
- OUT from FO AND stock picking type in internal, external or esc
580
So all OUT that have internel, external or esc should return FALSE from this method.
581
This means to only accept intermission and intersection invoicing on OUT with reason type "Deliver partner".
588
rt_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_deliver_partner')[1]
591
# type out and partner_type in internal, external or esc
592
if sp.type == 'out' and not sp.purchase_id and not sp.sale_id and sp.partner_id.partner_type in ['external', 'internal', 'esc']:
594
if sp.type == 'out' and not sp.purchase_id and not sp.sale_id and rt_id and sp.partner_id.partner_type in ['intermission', 'section']:
595
# Search all stock moves attached to this one. If one of them is deliver partner, then is_invoice_needed is ok
597
sm_ids = self.pool.get('stock.move').search(cr, uid, [('picking_id', '=', sp.id)])
599
for sm in self.pool.get('stock.move').browse(cr, uid, sm_ids):
600
if sm.reason_type_id.id == rt_id:
602
# partner is itself (those that own the company)
603
company_partner_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.partner_id
604
if sp.partner_id.id == company_partner_id.id:
608
def action_done(self, cr, uid, ids, context=None):
610
Create automatically invoice or NOT (regarding some criteria in is_invoice_needed)
612
res = super(stock_picking, self).action_done(cr, uid, ids, context=context)
614
if isinstance(ids, (int, long)):
616
for sp in self.browse(cr, uid, ids):
618
inv_type = self._get_invoice_type(sp)
619
# Check if no invoice needed
620
is_invoice_needed = self.is_invoice_needed(cr, uid, sp)
621
if not is_invoice_needed:
623
# we do not create invoice for procurement_request (Internal Request)
624
if not sp.sale_id.procurement_request and sp.subtype == 'standard':
625
if sp.type == 'in' or sp.type == 'internal':
626
if inv_type == 'out_refund':
627
sp_type = 'sale_refund'
630
elif sp.type == 'out':
631
if inv_type == 'in_refund':
632
sp_type = 'purchase_refund'
636
journal_type = sp_type
637
# Disturb journal for invoice only on intermission partner type
638
if sp.partner_id.partner_type == 'intermission':
639
journal_type = 'intermission'
640
journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', journal_type),
641
('is_current_instance', '=', True)])
643
raise osv.except_osv(_('Warning'), _('No %s journal found!') % (journal_type,))
645
self.action_invoice_create(cr, uid, [sp.id], journal_ids[0], False, inv_type, {})
648
def action_invoice_create(self, cr, uid, ids, journal_id=False, group=False, type='out_invoice', context=None):
650
Attach an intermission journal to the Intermission Voucher IN/OUT if partner type is intermission from the picking.
651
Prepare intermission voucher IN/OUT
652
Change invoice purchase_list field to TRUE if this picking come from a PO which is 'purchase_list'
654
if isinstance(ids,(int, long)):
659
res = super(stock_picking, self).action_invoice_create(cr, uid, ids, journal_id, group, type, context)
660
intermission_journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'intermission'),
661
('is_current_instance', '=', True)])
662
company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
663
intermission_default_account = company.intermission_default_counterpart
664
for pick in self.browse(cr, uid, [x for x in res]):
665
# Check if PO and PO is purchase_list
666
if pick.purchase_id and pick.purchase_id.order_type and pick.purchase_id.order_type == 'purchase_list':
667
inv_id = res[pick.id]
668
self.pool.get('account.invoice').write(cr, uid, [inv_id], {'purchase_list': True})
670
if pick.partner_id.partner_type == 'intermission':
671
inv_id = res[pick.id]
672
if not intermission_journal_ids:
673
raise osv.except_osv(_('Error'), _('No Intermission journal found!'))
674
if not intermission_default_account or not intermission_default_account.id:
675
raise osv.except_osv(_('Error'), _('Please configure a default intermission account in Company configuration.'))
676
self.pool.get('account.invoice').write(cr, uid, [inv_id], {'journal_id': intermission_journal_ids[0],
677
'is_intermission': True, 'account_id': intermission_default_account.id,})
678
# Change currency for this invoice
679
company_currency = company.currency_id and company.currency_id.id or False
680
if not company_currency:
681
raise osv.except_osv(_('Warning'), _('No company currency found!'))
682
wiz_account_change = self.pool.get('account.change.currency').create(cr, uid, {'currency_id': company_currency}, context=context)
683
self.pool.get('account.change.currency').change_currency(cr, uid, [wiz_account_change], context={'active_id': inv_id})
686
def action_confirm(self, cr, uid, ids, context=None):
688
stock.picking: action confirm
689
if INCOMING picking: confirm and check availability
691
super(stock_picking, self).action_confirm(cr, uid, ids, context=context)
692
move_obj = self.pool.get('stock.move')
694
if isinstance(ids, (int, long)):
696
for pick in self.browse(cr, uid, ids):
697
if pick.move_lines and pick.type == 'in':
698
not_assigned_move = [x.id for x in pick.move_lines if x.state == 'confirmed']
699
if not_assigned_move:
700
move_obj.action_assign(cr, uid, not_assigned_move)
703
def _hook_action_assign_batch(self, cr, uid, ids, context=None):
705
Please copy this to your module's method also.
706
This hook belongs to the action_assign method from stock>stock.py>stock_picking class
708
- when product is Expiry date mandatory, we "pre-assign" batch numbers regarding the available quantity
709
and location logic in addition to FEFO logic (First expired first out).
711
if isinstance(ids,(int, long)):
715
move_obj = self.pool.get('stock.move')
716
if not context.get('already_checked'):
717
for pick in self.browse(cr, uid, ids, context=context):
718
# perishable for perishable or batch management
719
move_obj.fefo_update(cr, uid, [move.id for move in pick.move_lines if move.product_id.perishable], context) # FEFO
720
context['already_checked'] = True
721
return super(stock_picking, self)._hook_action_assign_batch(cr, uid, ids, context=context)
225
735
_inherit = "stock.move"
226
736
_description = "Stock Move with hook"
738
def set_manually_done(self, cr, uid, ids, all_doc=True, context=None):
740
Set the stock move to manually done
742
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
744
def _get_from_dpo(self, cr, uid, ids, field_name, args, context=None):
746
Return True if the move has a dpo_id
749
if isinstance(ids,(int, long)):
752
for move in self.browse(cr, uid, ids, context=context):
759
def _search_from_dpo(self, cr, uid, obj, name, args, context=None):
761
Returns the list of moves from or not from DPO
764
if arg[0] == 'from_dpo' and arg[1] == '=':
765
return [('dpo_id', '!=', False)]
766
elif arg[0] == 'from_dpo' and arg[1] in ('!=', '<>'):
767
return [('dpo_id', '=', False)]
771
def _default_location_destination(self, cr, uid, context=None):
774
if context.get('picking_type') == 'out':
775
wh_ids = self.pool.get('stock.warehouse').search(cr, uid, [])
777
return self.pool.get('stock.warehouse').browse(cr, uid, wh_ids[0]).lot_output_id.id
781
def _get_inactive_product(self, cr, uid, ids, field_name, args, context=None):
783
Fill the error message if the product of the line is inactive
786
if isinstance(ids,(int, long)):
789
for line in self.browse(cr, uid, ids, context=context):
790
res[line.id] = {'inactive_product': False,
791
'inactive_error': ''}
792
if line.picking_id and line.picking_id.state not in ('cancel', 'done') and line.product_id and not line.product_id.active:
793
res[line.id] = {'inactive_product': True,
794
'inactive_error': _('The product in line is inactive !')}
799
'price_unit': fields.float('Unit Price', digits_compute=dp.get_precision('Picking Price Computation'), help="Technical field used to record the product cost set by the user during a picking confirmation (when average price costing method is used)"),
800
'state': fields.selection([('draft', 'Draft'), ('waiting', 'Waiting'), ('confirmed', 'Not Available'), ('assigned', 'Available'), ('done', 'Closed'), ('cancel', 'Cancelled')], 'State', readonly=True, select=True,
801
help='When the stock move is created it is in the \'Draft\' state.\n After that, it is set to \'Not Available\' state if the scheduler did not find the products.\n When products are reserved it is set to \'Available\'.\n When the picking is done the state is \'Closed\'.\
802
\nThe state is \'Waiting\' if the move is waiting for another one.'),
803
'address_id': fields.many2one('res.partner.address', 'Delivery address', help="Address of partner", readonly=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain="[('partner_id', '=', partner_id)]"),
804
'partner_id2': fields.many2one('res.partner', 'Partner', required=False),
805
'already_confirmed': fields.boolean(string='Already confirmed'),
806
'dpo_id': fields.many2one('purchase.order', string='Direct PO', help='PO from where this stock move is sourced.'),
807
'from_dpo': fields.function(_get_from_dpo, fnct_search=_search_from_dpo, type='boolean', method=True, store=False, string='From DPO ?'),
808
'from_wkf_line': fields.related('picking_id', 'from_wkf', type='boolean', string='Internal use: from wkf'),
809
'fake_state': fields.related('state', type='char', store=False, string="Internal use"),
810
'processed_stock_move': fields.boolean(string='Processed Stock Move'),
811
'inactive_product': fields.function(_get_inactive_product, method=True, type='boolean', string='Product is inactive', store=False, multi='inactive'),
812
'inactive_error': fields.function(_get_inactive_product, method=True, type='char', string='Error', store=False, multi='inactive'),
816
'location_dest_id': _default_location_destination,
817
'processed_stock_move': False, # to know if the stock move has already been partially or completely processed
818
'inactive_product': False,
819
'inactive_error': lambda *a: '',
822
def create(self, cr, uid, vals, context=None):
824
Update the partner or the address according to the other
826
if not vals.get('partner_id2') and vals.get('address_id'):
827
addr = self.pool.get('res.partner.address').browse(cr, uid, vals.get('address_id'), context=context)
828
vals['partner_id2'] = addr.partner_id and addr.partner_id.id or False
829
elif not vals.get('partner_id2'):
830
vals['partner_id2'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.partner_id.id
832
if not vals.get('address_id') and vals.get('partner_id2'):
833
addr = self.pool.get('res.partner').address_get(cr, uid, vals.get('partner_id2'), ['delivery', 'default'])
834
if not addr.get('delivery'):
835
vals['address_id'] = addr.get('default')
837
vals['address_id'] = addr.get('delivery')
839
type = vals.get('type')
840
if not type and vals.get('picking_id'):
841
type = self.pool.get('stock.picking').browse(cr, uid, vals.get('picking_id'), context=context).type
843
if type == 'in' and not vals.get('date_expected'):
844
vals['date_expected'] = time.strftime('%Y-%m-%d %H:%M:%S')
846
if vals.get('date_expected'):
847
vals['date'] = vals.get('date_expected')
849
return super(stock_move, self).create(cr, uid, vals, context=context)
851
def write(self, cr, uid, ids, vals, context=None):
853
Update the partner or the address according to the other
856
if isinstance(ids, (int, long)):
859
if not vals.get('address_id') and vals.get('partner_id2'):
860
for move in self.browse(cr, uid, ids, context=context):
861
if move.partner_id.id != vals.get('partner_id'):
862
addr = self.pool.get('res.partner').address_get(cr, uid, vals.get('partner_id2'), ['delivery', 'default'])
863
if not addr.get('delivery'):
864
vals['address_id'] = addr.get('default')
866
vals['address_id'] = addr.get('delivery')
868
if not vals.get('partner_id2') and vals.get('address_id'):
869
for move in self.browse(cr, uid, ids, context=context):
870
if move.address_id.id != vals.get('address_id'):
871
addr = self.pool.get('res.partner.address').browse(cr, uid, vals.get('address_id'), context=context)
872
vals['partner_id2'] = addr.partner_id and addr.partner_id.id or False
874
if vals.get('date_expected'):
875
for move in self.browse(cr, uid, ids, context=context):
876
if vals.get('state', move.state) not in ('done', 'cancel'):
877
vals['date'] = vals.get('date_expected')
879
return super(stock_move, self).write(cr, uid, ids, vals, context=context)
881
def on_change_partner(self, cr, uid, ids, partner_id, address_id, context=None):
883
Change the delivery address when the partner change.
889
v.update({'address_id': False})
891
d.update({'address_id': [('partner_id', '=', partner_id)]})
895
addr = self.pool.get('res.partner.address').browse(cr, uid, address_id, context=context)
897
if not address_id or addr.partner_id.id != partner_id:
898
addr = self.pool.get('res.partner').address_get(cr, uid, partner_id, ['delivery', 'default'])
899
if not addr.get('delivery'):
900
addr = addr.get('default')
902
addr = addr.get('delivery')
904
v.update({'address_id': addr})
910
def copy(self, cr, uid, id, default=None, context=None):
912
Remove the already confirmed flag
916
default.update({'already_confirmed':False})
918
return super(stock_move, self).copy(cr, uid, id, default, context=context)
920
def fefo_update(self, cr, uid, ids, context=None):
922
Update batch, Expiry Date, Location according to FEFO logic
924
if isinstance(ids, (int, long)):
929
loc_obj = self.pool.get('stock.location')
930
prodlot_obj = self.pool.get('stock.production.lot')
931
for move in self.browse(cr, uid, ids, context):
933
if move.state == 'assigned': # a check_availability has already been done in action_assign, so we take only the 'assigned' lines
934
needed_qty = move.product_qty
935
res = loc_obj.compute_availability(cr, uid, [move.location_id.id], True, move.product_id.id, move.product_uom.id, context=context)
937
# We need to have the value like below because we need to have the id of the m2o (which is not possible if we do self.read(cr, uid, move.id))
938
values = {'name': move.name,
939
'picking_id': move.picking_id.id,
940
'product_uom': move.product_uom.id,
941
'product_id': move.product_id.id,
942
'date_expected': move.date_expected,
945
'location_dest_id': move.location_dest_id.id,
946
'reason_type_id': move.reason_type_id.id,
948
for loc in res['fefo']:
949
# if source == destination, the state becomes 'done', so we don't do fefo logic in that case
950
if not move.location_dest_id.id == loc['location_id']:
951
# we ignore the batch that are outdated
952
expired_date = prodlot_obj.read(cr, uid, loc['prodlot_id'], ['life_date'], context)['life_date']
953
if datetime.strptime(expired_date, "%Y-%m-%d") >= datetime.today():
954
# as long all needed are not fulfilled
956
# if the batch already exists and qty is enough, it is available (assigned)
957
if needed_qty <= loc['qty']:
958
if move.prodlot_id.id == loc['prodlot_id']:
959
self.write(cr, uid, move.id, {'state': 'assigned'}, context)
961
self.write(cr, uid, move.id, {'product_qty': needed_qty, 'product_uom': loc['uom_id'],
962
'location_id': loc['location_id'], 'prodlot_id': loc['prodlot_id']}, context)
965
# we take all available
966
selected_qty = loc['qty']
967
needed_qty -= selected_qty
969
dict_for_create = values.copy()
970
dict_for_create.update({'product_uom': loc['uom_id'], 'product_qty': selected_qty, 'location_id': loc['location_id'], 'prodlot_id': loc['prodlot_id']})
971
self.create(cr, uid, dict_for_create, context)
972
self.write(cr, uid, move.id, {'product_qty': needed_qty})
973
# if the batch is outdated, we remove it
974
if not context.get('yml_test', False):
975
if move.expired_date and not datetime.strptime(move.expired_date, "%Y-%m-%d") >= datetime.today():
976
self.write(cr, uid, move.id, {'prodlot_id': False}, context)
977
elif move.state == 'confirmed':
978
# we remove the prodlot_id in case that the move is not available
979
self.write(cr, uid, move.id, {'prodlot_id': False}, context)
982
def action_confirm(self, cr, uid, ids, context=None):
984
Set the bool already confirmed to True
986
res = super(stock_move, self).action_confirm(cr, uid, ids, context=context)
988
self.write(cr, uid, ids, {'already_confirmed': True}, context=context)
992
def _hook_confirmed_move(self, cr, uid, *args, **kwargs):
996
move = kwargs['move']
997
if not move.already_confirmed:
998
self.action_confirm(cr, uid, [move.id])
1001
def _hook_move_cancel_state(self, cr, uid, *args, **kwargs):
1003
Change the state of the chained move
1005
if kwargs.get('context'):
1006
kwargs['context'].update({'call_unlink': True})
1007
return {'state': 'cancel'}, kwargs.get('context', {})
1009
def _hook_write_state_stock_move(self, cr, uid, done, notdone, count):
1013
#If source location == dest location THEN stock move is done.
1014
for line in self.read(cr,uid,done,['location_id','location_dest_id']):
1015
if line.get('location_id') and line.get('location_dest_id') and line.get('location_id') == line.get('location_dest_id'):
1016
self.write(cr, uid, [line['id']], {'state': 'done'})
1018
self.write(cr, uid, [line['id']], {'state': 'assigned'})
1021
self.write(cr, uid, notdone, {'state': 'confirmed'})
1024
def _hook_check_assign(self, cr, uid, *args, **kwargs):
1026
kwargs['move'] is the current move
1028
move = kwargs['move']
1029
return move.location_id.usage == 'supplier'
1031
def _hook_cancel_assign_batch(self, cr, uid, ids, context=None):
1033
Please copy this to your module's method also.
1034
This hook belongs to the cancel_assign method from stock>stock.py>stock_move class
1036
- it erases the batch number associated if any and reset the source location to the original one.
1038
if isinstance(ids,(int, long)):
1041
for line in self.browse(cr, uid, ids, context):
1043
self.write(cr, uid, ids, {'prodlot_id': False, 'expired_date': False})
1044
if line.location_id.location_id and line.location_id.location_id.usage != 'view':
1045
self.write(cr, uid, ids, {'location_id': line.location_id.location_id.id})
1048
def _hook_copy_stock_move(self, cr, uid, res, move, done, notdone):
1051
move_id = self.copy(cr, uid, move.id, {'line_number': move.line_number, 'product_qty': r[0],'product_uos_qty': r[0] * move.product_id.uos_coeff,'location_id': r[1]})
1053
done.append(move_id)
1055
notdone.append(move_id)
1056
return done, notdone
229
1058
def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
360
1196
return [move.id for move in complete]
361
1197
# @@@override end
1199
def _get_destruction_products(self, cr, uid, ids, product_ids=False, context=None, recursive=False):
1200
""" Finds the product quantity and price for particular location.
1204
if isinstance(ids,(int, long)):
1208
for move in self.browse(cr, uid, ids, context=context):
1209
# add this move into the list of result
1217
sub_total = move.product_qty * move.product_id.standard_price
1220
if move.purchase_line_id and move.purchase_line_id.currency_id:
1221
currency = move.purchase_line_id.currency_id.name
1222
elif move.sale_line_id and move.sale_line_id.currency_id:
1223
currency = move.sale_line_id.currency_id.name
1226
'prod_name': move.product_id.name,
1227
'prod_code': move.product_id.code,
1228
'prod_price': move.product_id.standard_price,
1229
'sub_total': sub_total,
1230
'currency': currency,
1231
'origin': move.origin,
1232
'expired_date': move.expired_date,
1233
'prodlot_id': move.prodlot_id.name,
1234
'dg_check': dg_check_flag,
1235
'np_check': np_check_flag,
1236
'uom': move.product_uom.name,
1237
'prod_qty': move.product_qty,
1241
def in_action_confirm(self, cr, uid, ids, context=None):
1243
Incoming: draft or confirmed: validate and assign
1245
if isinstance(ids, (int, long)):
1247
self.action_confirm(cr, uid, ids, context)
1248
self.action_assign(cr, uid, ids, context)
1253
#-----------------------------------------
1255
#-----------------------------------------
1256
class stock_location(osv.osv):
1257
_name = 'stock.location'
1258
_inherit = 'stock.location'
1264
if hasattr(super(stock_location, self), 'init'):
1265
super(stock_location, self).init(cr)
1267
mod_obj = self.pool.get('ir.module.module')
1268
logging.getLogger('init').info('HOOK: module stock_override: loading stock_data.xml')
1269
pathname = path.join('stock_override', 'stock_data.xml')
1270
file = tools.file_open(pathname)
1271
tools.convert_xml_import(cr, 'stock_override', file, {}, mode='init', noupdate=False)
1273
def _product_value(self, cr, uid, ids, field_names, arg, context=None):
1274
"""Computes stock value (real and virtual) for a product, as well as stock qty (real and virtual).
1275
@param field_names: Name of field
1276
@return: Dictionary of values
1278
result = super(stock_location, self)._product_value(cr, uid, ids, field_names, arg, context=context)
1280
product_product_obj = self.pool.get('product.product')
1281
currency_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
1282
currency_obj = self.pool.get('res.currency')
1283
currency = currency_obj.browse(cr, uid, currency_id, context=context)
1284
if context.get('product_id'):
1285
view_ids = self.search(cr, uid, [('usage', '=', 'view')], context=context)
1286
result.update(dict([(i, {}.fromkeys(field_names, 0.0)) for i in list(set([aaa for aaa in view_ids]))]))
1287
for loc_id in view_ids:
1288
c = (context or {}).copy()
1289
c['location'] = loc_id
1290
c['compute_child'] = True
1291
for prod in product_product_obj.browse(cr, uid, [context.get('product_id')], context=c):
1292
for f in field_names:
1293
if f == 'stock_real':
1294
if loc_id not in result:
1296
result[loc_id][f] += prod.qty_available
1297
elif f == 'stock_virtual':
1298
result[loc_id][f] += prod.virtual_available
1299
elif f == 'stock_real_value':
1300
amount = prod.qty_available * prod.standard_price
1301
amount = currency_obj.round(cr, uid, currency, amount)
1302
result[loc_id][f] += amount
1303
elif f == 'stock_virtual_value':
1304
amount = prod.virtual_available * prod.standard_price
1305
amount = currency_obj.round(cr, uid, currency, amount)
1306
result[loc_id][f] += amount
1310
def _fake_get(self, cr, uid, ids, fields, arg, context=None):
1312
if isinstance(ids, (int, long)):
1318
def _prod_loc_search(self, cr, uid, ids, fields, arg, context=None):
1319
if not arg or not arg[0] or not arg[0][2] or not arg[0][2][0]:
1323
id_nonstock = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock_override', 'stock_location_non_stockable')[1]
1324
id_cross = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_cross_docking', 'stock_location_cross_docking')[1]
1325
prod_obj = self.pool.get('product.product').browse(cr, uid, arg[0][2][0])
1326
if prod_obj and prod_obj.type == 'consu':
1327
if arg[0][2][1] == 'in':
1328
id_virt = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock','stock_location_locations_virtual')[1]
1329
ids_child = self.pool.get('stock.location').search(cr,uid,[('location_id', 'child_of', id_virt)])
1330
return [('id', 'in', [id_nonstock, id_cross]+ids_child)]
1332
return [('id', 'in', [id_cross])]
1334
elif prod_obj and prod_obj.type != 'consu':
1335
if arg[0][2][1] == 'in':
1336
return [('id', 'in', ids_child)]
1338
return [('id', 'not in', [id_nonstock]), ('usage', '=', 'internal')]
1340
return [('id', 'in', [])]
1342
def _cd_search(self, cr, uid, ids, fields, arg, context=None):
1343
id_cross = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_cross_docking', 'stock_location_cross_docking')[1]
1347
obj_pol = arg[0][2][0] and self.pool.get('purchase.order.line').browse(cr, uid, arg[0][2][0]) or False
1348
if (obj_pol and obj_pol.order_id.cross_docking_ok) or arg[0][2][1]:
1349
return [('id', 'in', [id_cross])]
1352
def _check_usage(self, cr, uid, ids, fields, arg, context=None):
1353
if not arg or not arg[0][2]:
1357
prod_obj = self.pool.get('product.product').browse(cr, uid, arg[0][2])
1358
if prod_obj.type == 'service_recep':
1359
ids = self.pool.get('stock.location').search(cr, uid, [('usage','=', 'inventory')])
1360
return [('id', 'in', ids)]
1361
elif prod_obj.type == 'consu':
1364
ids = self.pool.get('stock.location').search(cr, uid, [('usage','=', 'internal')])
1365
return [('id', 'in', ids)]
1370
'chained_location_type': fields.selection([('none', 'None'), ('customer', 'Customer'), ('fixed', 'Fixed Location'), ('nomenclature', 'Nomenclature')],
1371
'Chained Location Type', required=True,
1372
help="Determines whether this location is chained to another location, i.e. any incoming product in this location \n" \
1373
"should next go to the chained location. The chained location is determined according to the type :"\
1374
"\n* None: No chaining at all"\
1375
"\n* Customer: The chained location will be taken from the Customer Location field on the Partner form of the Partner that is specified in the Picking list of the incoming products." \
1376
"\n* Fixed Location: The chained location is taken from the next field: Chained Location if Fixed." \
1377
"\n* Nomenclature: The chained location is taken from the options field: Chained Location is according to the nomenclature level of product."\
1379
'chained_options_ids': fields.one2many('stock.location.chained.options', 'location_id', string='Chained options'),
1380
'optional_loc': fields.boolean(string='Is an optional location ?'),
1381
'stock_real': fields.function(_product_value, method=True, type='float', string='Real Stock', multi="stock"),
1382
'stock_virtual': fields.function(_product_value, method=True, type='float', string='Virtual Stock', multi="stock"),
1383
'stock_real_value': fields.function(_product_value, method=True, type='float', string='Real Stock Value', multi="stock", digits_compute=dp.get_precision('Account')),
1384
'stock_virtual_value': fields.function(_product_value, method=True, type='float', string='Virtual Stock Value', multi="stock", digits_compute=dp.get_precision('Account')),
1385
'check_prod_loc': fields.function(_fake_get, method=True, type='many2one', string='zz', fnct_search=_prod_loc_search),
1386
'check_cd': fields.function(_fake_get, method=True, type='many2one', string='zz', fnct_search=_cd_search),
1387
'check_usage': fields.function(_fake_get, method=True, type='many2one', string='zz', fnct_search=_check_usage),
1388
'virtual_location': fields.boolean(string='Virtual location'),
1393
# Chained location on nomenclature level
1395
def _hook_chained_location_get(self, cr, uid, context=None, *args, **kwargs):
1397
Return the location according to nomenclature level
1399
location = kwargs['location']
1400
product = kwargs['product']
1401
result = kwargs['result']
1403
if location.chained_location_type == 'nomenclature':
1404
for opt in location.chained_options_ids:
1405
if opt.nomen_id.id == product.nomen_manda_0.id:
1406
return opt.dest_location_id
1410
def _hook_proct_reserve(self, cr, uid, product_qty, result, amount, id, ids ):
1411
result.append((amount, id, True))
1412
product_qty -= amount
1413
if product_qty <= 0.0:
1417
result.append((amount, id, True))
1419
result.append((product_qty, ids[0], False))
1421
result.append((product_qty, id, False))
1425
def on_change_location_type(self, cr, uid, ids, chained_location_type, context=None):
1427
If the location type is changed to 'Nomenclature', set some other fields values
1429
if chained_location_type and chained_location_type == 'nomenclature':
1430
return {'value': {'chained_auto_packing': 'transparent',
1431
'chained_picking_type': 'internal',
1432
'chained_delay': 0}}
1439
class stock_location_chained_options(osv.osv):
1440
_name = 'stock.location.chained.options'
1441
_rec_name = 'location_id'
1444
'dest_location_id': fields.many2one('stock.location', string='Destination Location', required=True),
1445
'nomen_id': fields.many2one('product.nomenclature', string='Nomenclature Level', required=True),
1446
'location_id': fields.many2one('stock.location', string='Location', required=True),
1449
stock_location_chained_options()
1452
class ir_values(osv.osv):
1454
_inherit = 'ir.values'
1456
def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
1459
values = super(ir_values, self).get(cr, uid, key, key2, models, meta, context, res_id_req, without_user, key2_req)
1460
trans_obj = self.pool.get('ir.translation')
1462
move_accepted_values = {'client_action_multi': [],
1463
'client_print_multi': [],
1464
'client_action_relate': ['act_relate_picking'],
1465
'tree_but_action': [],
1466
'tree_but_open': []}
1468
incoming_accepted_values = {'client_action_multi': ['act_stock_return_picking', 'action_stock_invoice_onshipping'],
1469
'client_print_multi': ['Reception'],
1470
'client_action_relate': ['View_log_stock.picking'],
1471
'tree_but_action': [],
1472
'tree_but_open': []}
1474
internal_accepted_values = {'client_action_multi': [],
1475
'client_print_multi': ['Labels'],
1476
'client_action_relate': [],
1477
'tree_but_action': [],
1478
'tree_but_open': []}
1480
delivery_accepted_values = {'client_action_multi': [],
1481
'client_print_multi': ['Labels'],
1482
'client_action_relate': [''],
1483
'tree_but_action': [],
1484
'tree_but_open': []}
1486
picking_accepted_values = {'client_action_multi': [],
1487
'client_print_multi': ['Picking Ticket', 'Pre-Packing List', 'Labels'],
1488
'client_action_relate': [''],
1489
'tree_but_action': [],
1490
'tree_but_open': []}
1492
if 'stock.move' in [x[0] for x in models]:
1494
Destruction_Report = trans_obj.tr_view(cr, 'Destruction Report', context)
1496
if key == 'action' and v[1] in move_accepted_values[key2]:
1497
new_values.append(v)
1498
elif context.get('_terp_view_name', False) == Destruction_Report:
1499
new_values.append(v)
1500
elif context.get('picking_type', False) == 'incoming_shipment' and 'stock.picking' in [x[0] for x in models]:
1503
if key == 'action' and v[1] in incoming_accepted_values[key2]:
1504
new_values.append(v)
1505
elif context.get('picking_type', False) == 'internal_move' and 'stock.picking' in [x[0] for x in models]:
1508
if key == 'action' and v[1] in internal_accepted_values[key2]:
1509
new_values.append(v)
1510
elif context.get('picking_type', False) == 'delivery_order' and 'stock.picking' in [x[0] for x in models]:
1513
if key == 'action' and v[1] in delivery_accepted_values[key2]:
1514
new_values.append(v)
1515
elif context.get('picking_type', False) == 'picking_ticket' and 'stock.picking' in [x[0] for x in models]:
1518
if key == 'action' and v[1] in picking_accepted_values[key2]:
1519
new_values.append(v)