1
# -*- encoding: utf-8 -*-
2
##############################################################################
4
# Product serial module for OpenERP
5
# Copyright (C) 2008 Raphaël Valyi
6
# Copyright (C) 2011 Anevia S.A. - Ability to group invoice lines
7
# written by Alexis Demeaulte <alexis.demeaulte@anevia.com>
8
# Copyright (C) 2011 Akretion - Ability to split lines on logistical units
9
# written by Emmanuel Samyn
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU Affero General Public License as
13
# published by the Free Software Foundation, either version 3 of the
14
# License, or (at your option) any later version.
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
# GNU Affero General Public License for more details.
21
# You should have received a copy of the GNU Affero General Public License
22
# along with this program. If not, see <http://www.gnu.org/licenses/>.
24
##############################################################################
26
from osv import fields, osv
28
from tools.translate import _
31
class stock_move(osv.osv):
32
_inherit = "stock.move"
33
# We order by product name because otherwise, after the split,
34
# the products are "mixed" and not grouped by product name any more
35
_order = "picking_id, name, id"
37
def copy(self, cr, uid, id, default=None, context=None):
40
default['new_prodlot_code'] = False
41
return super(stock_move, self).copy(cr, uid, id, default, context=context)
43
def _get_prodlot_code(self, cr, uid, ids, field_name, arg, context=None):
45
for move in self.browse(cr, uid, ids):
46
res[move.id] = move.prodlot_id and move.prodlot_id.name or False
49
def _set_prodlot_code(self, cr, uid, ids, name, value, arg, context=None):
50
if not value: return False
52
if isinstance(ids, (int, long)):
55
for move in self.browse(cr, uid, ids, context=context):
56
product_id = move.product_id.id
57
existing_prodlot = move.prodlot_id
58
if existing_prodlot: #avoid creating a prodlot twice
59
self.pool.get('stock.production.lot').write(cr, uid, existing_prodlot.id, {'name': value})
61
prodlot_id = self.pool.get('stock.production.lot').create(cr, uid, {
63
'product_id': product_id,
65
move.write({'prodlot_id' : prodlot_id})
67
def _get_tracking_code(self, cr, uid, ids, field_name, arg, context=None):
69
for move in self.browse(cr, uid, ids):
70
res[move.id] = move.tracking_id and move.tracking_id.name or False
73
def _set_tracking_code(self, cr, uid, ids, name, value, arg, context=None):
74
if not value: return False
76
if isinstance(ids, (int, long)):
79
for move in self.browse(cr, uid, ids, context=context):
80
product_id = move.product_id.id
81
existing_tracking = move.tracking_id
82
if existing_tracking: #avoid creating a tracking twice
83
self.pool.get('stock.tracking').write(cr, uid, existing_tracking.id, {'name': value})
85
tracking_id = self.pool.get('stock.tracking').create(cr, uid, {
88
move.write({'tracking_id' : tracking_id})
91
'new_prodlot_code': fields.function(_get_prodlot_code, fnct_inv=_set_prodlot_code,
92
method=True, type='char', size=64,
93
string='Prodlot fast input', select=1
95
'new_tracking_code': fields.function(_get_tracking_code, fnct_inv=_set_tracking_code,
96
method=True, type='char', size=64,
97
string='Tracking fast input', select=1
101
def action_done(self, cr, uid, ids, context=None):
103
If we autosplit moves without reconnecting them 1 by 1, at least when some move which has descendants is split
104
The following situation would happen (alphabetical order is order of creation, initially b and a pre-exists, then a is split, so a might get assigned and then split too):
105
Incoming moves b, c, d
106
Outgoing moves a, e, f
107
Then we have those links: b->a, c->a, d->a
109
The following code will detect this situation and reconnect properly the moves into only: b->a, c->e and d->f
111
result = super(stock_move, self).action_done(cr, uid, ids, context)
112
for move in self.browse(cr, uid, ids):
113
if move.product_id.lot_split_type and move.move_dest_id and move.move_dest_id.id:
114
cr.execute("select stock_move.id from stock_move_history_ids left join stock_move on stock_move.id = stock_move_history_ids.child_id where parent_id=%s and stock_move.product_qty=1", (move.id,))
115
unitary_out_moves = cr.fetchall()
116
if unitary_out_moves and len(unitary_out_moves) > 1:
117
unitary_in_moves = []
120
while len(unitary_in_moves) != len(unitary_out_moves) and counter < len(unitary_out_moves):
121
out_node = unitary_out_moves[counter][0]
122
cr.execute("select stock_move.id from stock_move_history_ids left join stock_move on stock_move.id = stock_move_history_ids.parent_id where child_id=%s and stock_move.product_qty=1", (out_node,))
123
unitary_in_moves = cr.fetchall()
126
if len(unitary_in_moves) == len(unitary_out_moves):
127
unitary_out_moves.reverse()
128
unitary_out_moves.pop()
129
unitary_in_moves.reverse()
130
unitary_in_moves.pop()
132
for unitary_in_move in unitary_in_moves:
133
cr.execute("delete from stock_move_history_ids where parent_id=%s and child_id=%s", (unitary_in_moves[counter][0], out_node))
134
cr.execute("update stock_move_history_ids set parent_id=%s where parent_id=%s and child_id=%s", (unitary_in_moves[counter][0], move.id, unitary_out_moves[counter][0]))
139
def split_move(self, cr, uid, ids, context=None):
141
for move in self.browse(cr, uid, ids, context=context):
142
qty = move.product_qty
144
if move.product_id.lot_split_type == 'lu':
145
if not move.product_id.packaging:
146
raise osv.except_osv(_('Error :'), _("Product '%s' has 'Lot split type' = 'Logistical Unit' but is missing packaging information.") % (move.product_id.name))
147
lu_qty = move.product_id.packaging[0].qty
148
elif move.product_id.lot_split_type == 'single':
150
if lu_qty and qty > 1:
151
# Set existing move to LU quantity
152
self.write(cr, uid, move.id, {'product_qty': lu_qty, 'product_uos_qty': move.product_id.uos_coeff})
154
# While still enough qty to create a new move, create it
156
all_ids.append( self.copy(cr, uid, move.id, {'state': move.state, 'prodlot_id': None}) )
158
# Create a last move for the remainder qty
160
all_ids.append( self.copy(cr, uid, move.id, {'state': move.state, 'prodlot_id': None, 'product_qty': qty}) )
166
class stock_picking(osv.osv):
167
_inherit = "stock.picking"
169
def action_assign_wkf(self, cr, uid, ids):
170
result = super(stock_picking, self).action_assign_wkf(cr, uid, ids)
172
for picking in self.browse(cr, uid, ids):
173
if picking.company_id.autosplit_is_active:
174
for move in picking.move_lines:
176
if ((move.product_id.track_production and move.location_id.usage == 'production') or \
177
(move.product_id.track_production and move.location_dest_id.usage == 'production') or \
178
(move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
179
(move.product_id.track_outgoing and move.location_dest_id.usage == 'customer')):
180
self.pool.get('stock.move').split_move(cr, uid, [move.id])
184
# Because stock move line can be splitted by the module, we merge
185
# invoice lines (if option 'is_group_invoice_line' is activated for the company)
186
# at the following conditions :
187
# - the product is the same and
188
# - the discount is the same and
189
# - the unit price is the same and
190
# - the description is the same and
191
# - taxes are the same
192
# - they are from the same sale order lines (requires extra-code)
193
# we merge invoice line together and do the sum of quantity and
195
def action_invoice_create(self, cursor, user, ids, journal_id=False,
196
group=False, type='out_invoice', context=None):
197
invoice_dict = super(stock_picking, self).action_invoice_create(cursor, user,
198
ids, journal_id, group, type, context=context)
200
for picking_key in invoice_dict:
201
invoice = self.pool.get('account.invoice').browse(cursor, user, invoice_dict[picking_key], context=context)
202
if not invoice.company_id.is_group_invoice_line:
207
for line in invoice.invoice_line:
210
key = unicode(line.product_id.id) + ";" \
211
+ unicode(line.discount) + ";" \
212
+ unicode(line.price_unit) + ";" \
215
# Add the tax key part
217
for tax in line.invoice_line_tax_id:
218
tax_tab.append(tax.id)
221
key = key + unicode(tax) + ";"
223
# Add the sale order line part but check if the field exist because
224
# it's install by a specific module (not from addons)
225
if self.pool.get('ir.model.fields').search(cursor, user,
226
[('name', '=', 'sale_order_lines'), ('model', '=', 'account.invoice.line')], context=context) != []:
228
for order_line in line.sale_order_lines:
229
order_line_tab.append(order_line.id)
230
order_line_tab.sort()
231
for order_line in order_line_tab:
232
key = key + unicode(order_line) + ";"
235
# Get the hash of the key
236
hash_key = hashlib.sha224(key.encode('utf8')).hexdigest()
238
# if the key doesn't already exist, we keep the invoice line
239
# and we add the key to new_line_list
240
if not new_line_list.has_key(hash_key):
241
new_line_list[hash_key] = {
243
'quantity': line.quantity,
244
'price_subtotal': line.price_subtotal,
246
# if the key already exist, we update new_line_list and
247
# we delete the invoice line
249
new_line_list[hash_key]['quantity'] = new_line_list[hash_key]['quantity'] + line.quantity
250
new_line_list[hash_key]['price_subtotal'] = new_line_list[hash_key]['price_subtotal'] \
251
+ line.price_subtotal
252
self.pool.get('account.invoice.line').unlink(cursor, user, line.id, context=context)
254
# Write modifications made on invoice lines
255
for hash_key in new_line_list:
256
line_id = new_line_list[hash_key]['id']
257
del new_line_list[hash_key]['id']
258
self.pool.get('account.invoice.line').write(cursor, user, line_id, new_line_list[hash_key], context=context)
265
class stock_production_lot(osv.osv):
266
_inherit = "stock.production.lot"
268
def _last_location_id(self, cr, uid, ids, field_name, arg, context={}):
269
"""Retrieves the last location where the product with given serial is.
270
Instead of using dates we assume the product is in the location having the
271
highest number of products with the given serial (should be 1 if no mistake). This
272
is better than using move dates because moves can easily be encoded at with wrong dates."""
275
for prodlot_id in ids:
277
"select location_dest_id " \
278
"from stock_move inner join stock_report_prodlots on stock_report_prodlots.location_id = location_dest_id and stock_report_prodlots.prodlot_id = %s " \
279
"where stock_move.prodlot_id = %s and stock_move.state=%s "\
280
"order by stock_report_prodlots.qty DESC ",
281
(prodlot_id, prodlot_id, 'done'))
282
results = cr.fetchone()
284
#TODO return tuple to avoid name_get being requested by the GTK client
285
res[prodlot_id] = results and results[0] or False
290
'last_location_id': fields.function(_last_location_id, method=True,
291
type="many2one", relation="stock.location",
292
string="Last location",
293
help="Display the current stock location of this production lot"),
296
stock_production_lot()