~stock-logistic-core-editors/stock-logistic-flows/6.1

« back to all changes in this revision

Viewing changes to product_serial/stock.py

  • Committer: Alexandre Fayolle
  • Author(s): alexis at ecp
  • Date: 2013-05-14 15:07:28 UTC
  • mfrom: (23.1.34 stock-logistic-flows)
  • Revision ID: alexandre.fayolle@camptocamp.com-20130514150728-sv0t0brr6pp3jpam
[MRG] migrate product_serial to community addons

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- encoding: utf-8 -*-
 
2
##############################################################################
 
3
#
 
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
 
10
#
 
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.
 
15
#
 
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.
 
20
#
 
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/>.
 
23
#
 
24
##############################################################################
 
25
 
 
26
from osv import fields, osv
 
27
import hashlib
 
28
from tools.translate import _
 
29
 
 
30
 
 
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"
 
36
 
 
37
    def copy(self, cr, uid, id, default=None, context=None):
 
38
        if not default:
 
39
            default = {}
 
40
        default['new_prodlot_code'] = False
 
41
        return super(stock_move, self).copy(cr, uid, id, default, context=context)
 
42
 
 
43
    def _get_prodlot_code(self, cr, uid, ids, field_name, arg, context=None):
 
44
        res = {}
 
45
        for move in self.browse(cr, uid, ids):
 
46
            res[move.id] = move.prodlot_id and move.prodlot_id.name or False
 
47
        return res
 
48
 
 
49
    def _set_prodlot_code(self, cr, uid, ids, name, value, arg, context=None):
 
50
        if not value: return False
 
51
 
 
52
        if isinstance(ids, (int, long)):
 
53
            ids = [ids]
 
54
 
 
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})
 
60
            else:
 
61
                prodlot_id = self.pool.get('stock.production.lot').create(cr, uid, {
 
62
                    'name': value,
 
63
                    'product_id': product_id,
 
64
                })
 
65
                move.write({'prodlot_id' : prodlot_id})
 
66
 
 
67
    def _get_tracking_code(self, cr, uid, ids, field_name, arg, context=None):
 
68
        res = {}
 
69
        for move in self.browse(cr, uid, ids):
 
70
            res[move.id] = move.tracking_id and move.tracking_id.name or False
 
71
        return res
 
72
 
 
73
    def _set_tracking_code(self, cr, uid, ids, name, value, arg, context=None):
 
74
        if not value: return False
 
75
 
 
76
        if isinstance(ids, (int, long)):
 
77
            ids = [ids]
 
78
 
 
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})
 
84
            else:
 
85
                tracking_id = self.pool.get('stock.tracking').create(cr, uid, {
 
86
                    'name': value,
 
87
                })
 
88
                move.write({'tracking_id' : tracking_id})
 
89
 
 
90
    _columns = {
 
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
 
94
                                           ),
 
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
 
98
                                           ),
 
99
    }
 
100
 
 
101
    def action_done(self, cr, uid, ids, context=None):
 
102
        """
 
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
 
108
        and: b->, b->e, b->f
 
109
        The following code will detect this situation and reconnect properly the moves into only: b->a, c->e and d->f
 
110
        """
 
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 = []
 
118
                    out_node = False
 
119
                    counter = 0
 
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()
 
124
                        counter += 1
 
125
 
 
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()
 
131
                        counter = 0
 
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]))
 
135
                            counter += 1
 
136
 
 
137
        return result
 
138
 
 
139
    def split_move(self, cr, uid, ids, context=None):
 
140
        all_ids = list(ids)
 
141
        for move in self.browse(cr, uid, ids, context=context):
 
142
            qty = move.product_qty
 
143
            lu_qty = False
 
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':
 
149
                lu_qty = 1
 
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})
 
153
                qty -= lu_qty
 
154
                # While still enough qty to create a new move, create it
 
155
                while qty >= lu_qty:
 
156
                    all_ids.append( self.copy(cr, uid, move.id, {'state': move.state, 'prodlot_id': None}) )
 
157
                    qty -= lu_qty
 
158
                # Create a last move for the remainder qty
 
159
                if qty > 0:
 
160
                    all_ids.append( self.copy(cr, uid, move.id, {'state': move.state, 'prodlot_id': None, 'product_qty': qty}) )
 
161
        return all_ids
 
162
 
 
163
stock_move()
 
164
 
 
165
 
 
166
class stock_picking(osv.osv):
 
167
    _inherit = "stock.picking"
 
168
 
 
169
    def action_assign_wkf(self, cr, uid, ids):
 
170
        result = super(stock_picking, self).action_assign_wkf(cr, uid, ids)
 
171
 
 
172
        for picking in self.browse(cr, uid, ids):
 
173
            if picking.company_id.autosplit_is_active:
 
174
                for move in picking.move_lines:
 
175
                    # Auto split
 
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])
 
181
 
 
182
        return result
 
183
 
 
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
 
194
    # subtotal.
 
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)
 
199
 
 
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:
 
203
                continue
 
204
 
 
205
            new_line_list = {}
 
206
 
 
207
            for line in invoice.invoice_line:
 
208
 
 
209
                # Build a key
 
210
                key = unicode(line.product_id.id) + ";" \
 
211
                    + unicode(line.discount) + ";" \
 
212
                    + unicode(line.price_unit) + ";" \
 
213
                    + line.name + ";"
 
214
 
 
215
                # Add the tax key part
 
216
                tax_tab = []
 
217
                for tax in line.invoice_line_tax_id:
 
218
                    tax_tab.append(tax.id)
 
219
                tax_tab.sort()
 
220
                for tax in tax_tab:
 
221
                    key = key + unicode(tax) + ";"
 
222
 
 
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) != []:
 
227
                    order_line_tab = []
 
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) + ";"
 
233
 
 
234
 
 
235
                # Get the hash of the key
 
236
                hash_key = hashlib.sha224(key.encode('utf8')).hexdigest()
 
237
 
 
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] = {
 
242
                        'id': line.id,
 
243
                        'quantity': line.quantity,
 
244
                        'price_subtotal': line.price_subtotal,
 
245
                    }
 
246
                # if the key already exist, we update new_line_list and 
 
247
                # we delete the invoice line
 
248
                else:
 
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)
 
253
 
 
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)
 
259
 
 
260
        return invoice_dict
 
261
 
 
262
stock_picking()
 
263
 
 
264
 
 
265
class stock_production_lot(osv.osv):
 
266
    _inherit = "stock.production.lot"
 
267
 
 
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."""
 
273
        res = {}
 
274
 
 
275
        for prodlot_id in ids:
 
276
            cr.execute(
 
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()
 
283
 
 
284
            #TODO return tuple to avoid name_get being requested by the GTK client
 
285
            res[prodlot_id] = results and results[0] or False
 
286
 
 
287
        return res
 
288
 
 
289
    _columns = {
 
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"),
 
294
    }
 
295
 
 
296
stock_production_lot()
 
297