1
# -*- coding: utf-8 -*-
2
#################################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 Julius Network Solutions SARL <contact@julius.fr>
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#################################################################################
22
from datetime import datetime
23
from osv import fields, osv
24
from tools.translate import _
27
# ----------------------------------------------------
29
# ----------------------------------------------------
33
# location_dest_id is only used for predicting future stocks
35
class stock_move(osv.osv):
37
_inherit = 'stock.move'
40
'state': fields.selection([
42
('waiting', 'Waiting'),
43
('on_hold', 'Waiting for prepayment'),
44
('on_hold_billing', 'Waiting for billing'),
45
('on_hold_paym', 'Waiting for payment'),
46
('confirmed', 'Not Available'),
47
('assigned', 'Available'),
49
('cancel', 'Cancelled')
50
], 'State', readonly=True, select=True,
51
help="* When the stock move is created it is in the \'Draft\' state.\n"\
52
"* After that, it is set to \'Not Available\' state if the scheduler did not find the products.\n"\
53
"* When products are reserved it is set to \'Available\'.\n"\
54
"* When the picking is done the state is \'Done\'.\n"\
55
"* The state is \'Waiting\' if the move is waiting for another one.\n"\
56
"* The state is \'Waiting for prepayment\' if it's waiting for a prepayment of the sale order\n"\
57
"* The state is \'Waiting for billing\' if it's waiting for billing of the move\n"\
58
"* The state is \'Waiting for payment\' if it's waiting for the payment of the move"),
61
def action_on_hold(self, cr, uid, ids, context=None):
65
moves = self.browse(cr, uid, ids, context=context)
66
self.write(cr, uid, ids, {'state': 'on_hold'})
69
picking = moves[0].picking_id
71
moves = picking.move_lines
74
if move.state not in ['on_hold','done','cancel']:
78
self.pool.get('stock.picking').write(cr, uid, picking.id, {'state': state}, context)
81
def action_hold_to_confirm(self, cr, uid, ids, context=None):
82
""" Makes hold stock moves to the confirmed state.
85
moves = self.browse(cr, uid, ids, context=context)
86
self.write(cr, uid, ids, {'state': 'confirmed'})
89
picking = moves[0].picking_id
91
moves = picking.move_lines
94
if move.state in ['on_hold','draft']:
98
self.pool.get('stock.picking').write(cr, uid, picking.id, {'state': state}, context)
101
def action_confirm_waiting_bill(self, cr, uid, ids, context=None):
102
""" Makes hold stock moves to the wait for billing state.
103
@return: List of ids.
105
moves = self.browse(cr, uid, ids, context=context)
106
self.write(cr, uid, ids, {'state': 'on_hold_billing'})
109
picking = moves[0].picking_id
111
moves = picking.move_lines
112
state = 'on_hold_billing'
114
if move.state in ['on_hold','draft']:
118
self.pool.get('stock.picking').write(cr, uid, picking.id, {'state': state}, context)
121
def action_waiting_bill_to_unpaid(self, cr, uid, ids, context=None):
122
""" Makes hold stock moves to the wait for payment state.
123
@return: List of ids.
125
moves = self.browse(cr, uid, ids, context=context)
128
picking = moves[0].picking_id
130
moves = picking.move_lines
131
state = 'on_hold_paym'
133
if move.state in ['on_hold','draft']:
137
self.pool.get('stock.picking').write(cr, uid, picking.id, {'state': state}, context)
140
""" This part is replacing the usual one to be able to taking in account the new states. """
141
def action_assign(self, cr, uid, ids, *args):
142
""" Changes state to confirmed or waiting.
143
@return: List of values
146
for move in self.browse(cr, uid, ids):
147
if move.state in ('confirmed', 'waiting', 'on_hold_billing', 'on_hold_paym', 'assigned'):
149
res = self.check_assign(cr, uid, todo)
152
""" This part is replacing the usual one to be able to taking in account the new states. """
153
def check_assign(self, cr, uid, ids, context=None):
154
""" Checks the product type and accordingly writes the state.
155
@return: No. of moves done
163
for move in self.browse(cr, uid, ids, context=context):
164
if move.product_id.type == 'consu' or move.location_id.usage == 'supplier':
165
if move.state in ('confirmed', 'waiting'):
167
pickings[move.picking_id.id] = 1
169
if move.state in ('confirmed', 'waiting', 'on_hold_billing', 'on_hold_paym'):
170
if move.state in ('on_hold_billing', 'on_hold_paym'):
173
# Important: we must pass lock=True to _product_reserve() to avoid race conditions and double reservations
174
res = self.pool.get('stock.location')._product_reserve(cr, uid, [move.location_id.id], move.product_id.id, move.product_qty, {'uom': move.product_uom.id}, lock=True)
176
if move.sale_line_id and move.sale_line_id.order_id.order_policy in ['ship_prepayment','wait_prepayment']:
177
if not move.sale_line_id.invoice_lines:
178
self.write(cr, uid, [move.id], {'state':'on_hold_billing'})
179
pickings[move.picking_id.id] = 1
182
for line in move.sale_line_id.invoice_lines:
183
if line.invoice_id and line.invoice_id.state == 'cancel':
184
self.write(cr, uid, [move.id], {'state':'on_hold_billing'})
185
pickings[move.picking_id.id] = 1
187
elif line.invoice_id and line.invoice_id.state != 'paid':
188
self.write(cr, uid, [move.id], {'state':'on_hold_paym'})
189
pickings[move.picking_id.id] = 1
191
elif line.invoice_id and line.invoice_id.state == 'paid':
192
self.write(cr, uid, [move.id], {'state':'assigned'})
193
pickings[move.picking_id.id] = 1
195
#_product_available_test depends on the next status for correct functioning
196
#the test does not work correctly if the same product occurs multiple times
197
#in the same order. This is e.g. the case when using the button 'split in two' of
198
#the stock outgoing form
200
self.write(cr, uid, [move.id], {'state':'assigned'})
202
pickings[move.picking_id.id] = 1
204
cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
208
move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
212
self.write(cr, uid, done, {'state': 'assigned'})
215
for pick_id in pickings:
216
wf_service = netsvc.LocalService("workflow")
217
wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
222
class stock_picking(osv.osv):
224
_inherit = 'stock.picking'
227
'state': fields.selection([
230
('on_hold', 'Waiting for prepayment'),
231
('on_hold_billing', 'Waiting for billing'),
232
('on_hold_paym', 'Waiting for payment'),
233
('confirmed', 'Confirmed'),
234
('assigned', 'Available'),
236
('cancel', 'Cancelled'),
237
], 'State', readonly=True, select=True,
238
help="* Draft: not confirmed yet and will not be scheduled until confirmed\n"\
239
"* Confirmed: still waiting for the availability of products\n"\
240
"* Waiting: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n"\
241
"* On Hold: waiting for a payment, payment or billing\n"\
242
"* Waiting for billing: waiting for billing of the picking\n"\
243
"* Waiting for payment: waiting for the payment of the picking\n"\
244
"* Available: products reserved, simply waiting for confirmation.\n"\
245
"* Done: has been processed, can't be modified or cancelled anymore\n"\
246
"* Cancelled: has been cancelled, can't be confirmed anymore"),
249
def action_on_hold(self, cr, uid, ids, context=None):
253
self.write(cr, uid, ids, {'state': 'on_hold'})
255
for picking in self.browse(cr, uid, ids, context=context):
256
for r in picking.move_lines:
257
if r.state in ['draft','confirmed','waiting','assigned','on_hold_billing','on_hold_paym']:
260
self.log_picking(cr, uid, ids, context=context)
262
todo = self.action_explode(cr, uid, todo, context)
264
self.pool.get('stock.move').action_on_hold(cr, uid, todo, context=context)
267
def action_hold_to_confirm(self, cr, uid, ids, context=None):
268
""" Makes hold pickings to the confirmed state.
271
self.write(cr, uid, ids, {'state': 'confirmed'})
273
for picking in self.browse(cr, uid, ids, context=context):
274
for r in picking.move_lines:
275
if r.state in ['on_hold']:
278
self.log_picking(cr, uid, ids, context=context)
280
todo = self.action_explode(cr, uid, todo, context)
282
self.pool.get('stock.move').action_hold_to_confirm(cr, uid, todo, context=context)
285
def action_confirm_waiting_bill(self, cr, uid, ids, context=None):
286
""" Makes hold pickings to the wait for billing state.
289
self.write(cr, uid, ids, {'state': 'on_hold_billing', 'invoice_state': '2binvoiced'})
291
for picking in self.browse(cr, uid, ids, context=context):
292
for r in picking.move_lines:
293
if r.state in ['on_hold_billing']:
296
self.log_picking(cr, uid, ids, context=context)
298
todo = self.action_explode(cr, uid, todo, context)
300
self.pool.get('stock.move').action_confirm_waiting_bill(cr, uid, todo, context=context)
303
def action_waiting_bill_to_unpaid(self, cr, uid, ids, context=None):
304
""" Makes hold pickings to the wait for payment state.
307
self.write(cr, uid, ids, {'state': 'on_hold_paym'})
309
for picking in self.browse(cr, uid, ids, context=context):
310
for r in picking.move_lines:
311
if r.state in ['on_hold_paym']:
314
self.log_picking(cr, uid, ids, context=context)
316
todo = self.action_explode(cr, uid, todo, context)
318
self.pool.get('stock.move').action_waiting_bill_to_unpaid(cr, uid, todo, context=context)
321
""" This part is replacing the usual one to be able to taking in account the new states. """
322
def action_assign(self, cr, uid, ids, *args):
323
""" Changes state of picking to available if all moves are confirmed.
326
for pick in self.browse(cr, uid, ids):
327
move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed','on_hold_billing','on_hold_paym']]
329
raise osv.except_osv(_('Warning !'),_('Not enough stock, unable to reserve the products.'))
330
self.pool.get('stock.move').action_assign(cr, uid, move_ids)
333
def test_billed(self, cr, uid, ids):
334
""" Tests whether the move has been billed or not.
335
@return: True or False
338
for pick in self.browse(cr, uid, ids):
340
for move in pick.move_lines:
341
if (move.state in ('confirmed', 'draft')) and (mt == 'one'):
343
if (mt == 'direct') and (move.state == 'on_hold_billing') and (move.product_qty):
345
ok = ok and (move.state in ('cancel', 'done', 'assigned', 'on_hold_paym'))
348
def test_bill_and_paid(self, cr, uid, ids):
349
""" Tests whether the move has been billed and paid or not.
350
@return: True or False
353
for pick in self.browse(cr, uid, ids):
355
for move in pick.move_lines:
356
if (move.state in ('confirmed', 'draft', 'on_hold', 'on_hold_billing')) and (mt == 'one'):
358
if (mt == 'direct') and (move.state == 'on_hold_paym') and (move.product_qty):
360
ok = ok and (move.state in ('cancel', 'done', 'assigned'))
363
def test_paid(self, cr, uid, ids):
364
""" Tests whether the move has been paid.
365
@return: True or False
368
for pick in self.browse(cr, uid, ids):
370
for move in pick.move_lines:
371
if (move.state in ('confirmed', 'draft', 'on_hold', 'on_hold_billing', 'on_hold_paym')) and (mt == 'one'):
373
if (mt == 'direct') and (move.state == 'assigned') and (move.product_qty):
375
ok = ok and (move.state in ('cancel', 'done', 'assigned'))
378
""" This part is replacing the usual one to be able to taking in account the new states. """
379
def log_picking(self, cr, uid, ids, context=None):
380
""" This function will create log messages for picking.
381
@param cr: the database cursor
382
@param uid: the current user's ID for security checks,
383
@param ids: List of Picking Ids
384
@param context: A standard dictionary for contextual values
388
data_obj = self.pool.get('ir.model.data')
389
for pick in self.browse(cr, uid, ids, context=context):
391
if pick.auto_picking:
394
'out':_("Delivery Order"),
396
'internal': _('Internal picking'),
399
'out': 'view_picking_out_form',
400
'in': 'view_picking_in_form',
401
'internal': 'view_picking_form',
403
message = type_list.get(pick.type, _('Document')) + " '" + (pick.name or '?') + "' "
405
msg= _(' for the ')+ datetime.strptime(pick.min_date, '%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y')
407
'confirmed': _("is scheduled") + msg +'.',
408
'assigned': _('is ready to process.'),
409
'cancel': _('is cancelled.'),
410
'done': _('is done.'),
411
'draft': _('is in draft state.'),
412
'auto': _('is waiting.'),
413
'on_hold': _('is hold, waiting for prepayment.'),
414
'on_hold_billing': _('is ready to process but waiting for a billing.'),
415
'on_hold_paym': _('is ready to process but waiting for the payment.'),
417
res = data_obj.get_object_reference(cr, uid, 'stock', view_list.get(pick.type, 'view_picking_form'))
418
context.update({'view_id': res and res[1] or False})
419
message += state_list[pick.state]
420
self.log(cr, uid, pick.id, message, context=context)
423
def already_invoiced(self, cr, uid, ids, pick_invoice, context=None):
424
""" Looking for what have already been invoice to deduce it on the new invoice """
425
invoice_obj = self.pool.get('account.invoice')
426
sale_obj = self.pool.get('sale.order')
427
picking_ids = self.browse(cr, uid, ids, context)
429
for pick in picking_ids:
430
sale_id = pick.sale_id and pick.sale_id.id
432
amount_prepaid = 0.00
434
cr.execute("""SELECT invoice_id FROM sale_order_invoice_rel WHERE order_id = %s """, (sale_id,))
435
invoice_ids = map(lambda x: x[0], cr.fetchall())
436
invoice_open_paid = invoice_obj.search(cr, uid, [('id', 'in', invoice_ids),('state', 'in', ['open','paid'])])
437
invoice_id = pick_invoice[pick.id]
438
res[invoice_id] = invoice_open_paid
439
for invoice in invoice_obj.browse(cr, uid, invoice_open_paid):
440
if invoice.is_advance:
441
amount_prepaid += invoice.amount_untaxed or 0.00
443
for line in invoice.invoice_line:
444
if not line.is_advance:
445
amount_total += line.price_subtotal or 0.00
446
sale_obj.write(cr, uid, [sale_id], {'amount_prepaid': amount_prepaid, 'amount_shipped': amount_total})
449
def create_advance_line(self, cr, uid, pick_invoice, invoice_done, context=None):
450
invoice_obj = self.pool.get('account.invoice')
451
account_line_obj = self.pool.get('account.invoice.line')
452
for pick in pick_invoice:
453
picking_id = self.browse(cr, uid, pick, context)
454
sale_id = picking_id.sale_id
455
total_amount = sale_id.amount_untaxed or 0.00
456
prepaid_amount = sale_id.amount_prepaid or 0.00
457
amount_shipped = sale_id.amount_shipped or 0.00
458
invoice_id = pick_invoice[pick]
459
invoice_id = invoice_obj.browse(cr, uid, invoice_id)
460
invoice_amount = invoice_id.amount_untaxed
461
if prepaid_amount >= amount_shipped:
462
if (invoice_amount + amount_shipped) > prepaid_amount:
463
line_amount = - (prepaid_amount - amount_shipped)
465
line_amount = - invoice_amount
466
res = account_line_obj.product_id_change(cr, uid, [], 7624, False, 1, partner_id=invoice_id.partner_id.id, fposition_id=invoice_id.fiscal_position.id, price_unit=line_amount, address_invoice_id=invoice_id.address_invoice_id.id, currency_id=invoice_id.currency_id.id, context=context)
467
account_line_obj = self.pool.get('account.invoice.line')
469
vals.update({'invoice_id': invoice_id.id, 'invoice_line_tax_id': [(6, 0, vals['invoice_line_tax_id'])], 'note':'', 'price_unit': line_amount, 'is_advance': True})
470
if vals['price_unit'] <> 0:
471
account_line_obj.create(cr, uid, vals)
472
invoice_amount = invoice_id.amount_untaxed
474
self.pool.get('sale.order').write(cr, uid, [sale_id.id], {'amount_shipped': amount_shipped + invoice_amount})
475
invoice_obj.button_reset_taxes(cr, uid, [invoice_id.id], context=context)
478
def associate_lines(self, cr, uid, ids, pick_invoice, context):
479
inv_line_obj = self.pool.get('account.invoice.line')
480
move_line_obj = self.pool.get('stock.move')
481
for pick in pick_invoice:
482
picking_id = self.browse(cr, uid, pick, context)
483
invoice_id = pick_invoice[pick]
484
move_lines = picking_id.move_lines
485
for line in move_lines:
486
invoice_line_ids = inv_line_obj(cr, uid, [
487
('invoice_id', '=', invoice_id),
488
('product_id', '=', line.product_id.id),
489
('quantity', '=', line.product_qty),
492
move_line_obj.write(cr, uid, [line.id], {'invoice_line_id': invoice_line_ids[0]})
494
def action_invoice_create(self, cr, uid, ids, journal_id=False,
495
group=False, type='out_invoice', context=None):
498
if context.get('before_shipping', False):
499
picking_ids = self.browse(cr, uid, ids)
501
sequence_obj = self.pool.get('ir.sequence')
502
for pick in picking_ids:
503
line_ids = [x.id for x in pick.move_lines if x.state == 'on_hold_billing']
504
old_lines_ids = [x.id for x in pick.move_lines if x.id not in line_ids]
506
wf_service = netsvc.LocalService("workflow")
508
new_picking = self.copy(cr, uid, pick.id,
510
'name': sequence_obj.get(cr, uid, 'stock.picking.%s'%(pick.type)),
513
'backorder_id': pick.id,
515
self.pool.get('stock.move').write(cr, uid, old_lines_ids, {'picking_id': new_picking}),
516
wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_confirm', cr)
517
self.pool.get('stock.move').write(cr, uid, line_ids, {'state': 'on_hold_paym'}),
520
res = super(stock_picking, self).action_invoice_create(cr, uid, ids, journal_id, group, type, context)
521
invoice_done = self.already_invoiced(cr, uid, ids, res, context)
522
self.create_advance_line(cr, uid, res, invoice_done, context)
527
class stock_location(osv.osv):
529
_inherit = "stock.location"
531
def _product_reserve(self, cr, uid, ids, product_id, product_qty, context=None, lock=False):
532
result = super(stock_location, self)._product_reserve(cr, uid, ids, product_id, product_qty, context, lock)
536
for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
537
cr.execute("""SELECT product_uom, sum(product_qty) AS product_qty
539
WHERE location_dest_id=%s AND
545
(id, id, product_id))
546
results = cr.dictfetchall()
547
cr.execute("""SELECT product_uom,-sum(product_qty) AS product_qty
549
WHERE location_id=%s AND
550
location_dest_id<>%s AND
552
state in ('done', 'assigned', 'on_hold_billing', 'on_hold_paym')
555
(id, id, product_id))
556
results += cr.dictfetchall()
560
amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
569
if amount > min(total, product_qty):
570
amount = min(product_qty, total)
571
result.append((amount, id))
572
product_qty -= amount
574
if product_qty <= 0.0:
582
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: