X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fstock%2Fstock.py;h=2c62432dda4033c3d31e5487ed5963956454e0b6;hb=5c0716d12e902c2c5135d2be80a0ffb850179ae2;hp=2ba3f50d2a7e9bd77e961ffe8da80b3d0ffeb457;hpb=86503f581216d271afe75980d977a7cb77715d09;p=odoo%2Fodoo.git diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 2ba3f50..2c62432 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -19,18 +19,17 @@ # ############################################################################## -from lxml import etree from datetime import datetime from dateutil.relativedelta import relativedelta import time from operator import itemgetter from itertools import groupby -from openerp.osv import fields, osv +from openerp.osv import fields, osv, orm from openerp.tools.translate import _ from openerp import netsvc from openerp import tools -from openerp.tools import float_compare +from openerp.tools import float_compare, DEFAULT_SERVER_DATETIME_FORMAT import openerp.addons.decimal_precision as dp import logging _logger = logging.getLogger(__name__) @@ -76,6 +75,8 @@ class stock_location(osv.osv): _parent_order = 'posz,name' _order = 'parent_left' + # TODO: implement name_search() in a way that matches the results of name_get! + def name_get(self, cr, uid, ids, context=None): # always return the full hierarchical name res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context) @@ -240,6 +241,9 @@ class stock_location(osv.osv): if location.chained_location_type == 'customer': if partner: result = partner.property_stock_customer + else: + loc_id = self.pool['res.partner'].default_get(cr, uid, ['property_stock_customer'], context=context)['property_stock_customer'] + result = self.pool['stock.location'].browse(cr, uid, loc_id, context=context) elif location.chained_location_type == 'fixed': result = location.chained_location_id if result: @@ -400,7 +404,17 @@ class stock_location(osv.osv): uom_rounding = self.pool.get('product.product').browse(cr, uid, product_id, context=context).uom_id.rounding if context.get('uom'): uom_rounding = uom_obj.browse(cr, uid, context.get('uom'), context=context).rounding - for id in self.search(cr, uid, [('location_id', 'child_of', ids)]): + + locations_ids = self.search(cr, uid, [('location_id', 'child_of', ids)]) + if locations_ids: + # Fetch only the locations in which this product has ever been processed (in or out) + cr.execute("""SELECT l.id FROM stock_location l WHERE l.id in %s AND + EXISTS (SELECT 1 FROM stock_move m WHERE m.product_id = %s + AND ((state = 'done' AND m.location_dest_id = l.id) + OR (state in ('done','assigned') AND m.location_id = l.id))) + """, (tuple(locations_ids), product_id,)) + locations_ids = [i for (i,) in cr.fetchall()] + for id in locations_ids: if lock: try: # Must lock with a separate select query because FOR UPDATE can't be used with @@ -548,6 +562,7 @@ class stock_picking(osv.osv): _name = "stock.picking" _inherit = ['mail.thread'] _description = "Picking List" + _order = "id desc" def _set_maximum_date(self, cr, uid, ids, name, value, arg, context=None): """ Calculates planned date if it is greater than 'value'. @@ -562,12 +577,11 @@ class stock_picking(osv.osv): ids = [ids] for pick in self.browse(cr, uid, ids, context=context): sql_str = """update stock_move set - date='%s' + date_expected='%s' where picking_id=%d """ % (value, pick.id) - if pick.max_date: - sql_str += " and (date='" + pick.max_date + "' or date>'" + value + "')" + sql_str += " and (date_expected='" + pick.max_date + "')" cr.execute(sql_str) return True @@ -584,11 +598,11 @@ class stock_picking(osv.osv): ids = [ids] for pick in self.browse(cr, uid, ids, context=context): sql_str = """update stock_move set - date='%s' + date_expected='%s' where picking_id=%s """ % (value, pick.id) if pick.min_date: - sql_str += " and (date='" + pick.min_date + "' or date<'" + value + "')" + sql_str += " and (date_expected='" + pick.min_date + "')" cr.execute(sql_str) return True @@ -618,7 +632,7 @@ class stock_picking(osv.osv): def create(self, cr, user, vals, context=None): if ('name' not in vals) or (vals.get('name')=='/'): - seq_obj_name = self._name + seq_obj_name = 'stock.picking.%s' % vals.get('type', 'internal') vals['name'] = self.pool.get('ir.sequence').get(cr, user, seq_obj_name) new_id = super(stock_picking, self).create(cr, user, vals, context) return new_id @@ -652,7 +666,7 @@ class stock_picking(osv.osv): ), 'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date", store=True, type='datetime', string='Scheduled Time', select=1, help="Scheduled time for the shipment to be processed"), - 'date': fields.datetime('Time', help="Creation time, usually the time of the order.", select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}), + 'date': fields.datetime('Creation Date', help="Creation date, usually the time of the order.", select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}), 'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}), 'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date", store=True, type='datetime', string='Max. Expected Date', select=2), @@ -704,29 +718,24 @@ class stock_picking(osv.osv): default = {} default = default.copy() picking_obj = self.browse(cr, uid, id, context=context) - move_obj=self.pool.get('stock.move') - if ('name' not in default) or (picking_obj.name=='/'): - seq_obj_name = 'stock.picking.' + picking_obj.type + if ('name' not in default) or (picking_obj.name == '/'): + seq_obj_name = 'stock.picking.' + picking_obj.type default['name'] = self.pool.get('ir.sequence').get(cr, uid, seq_obj_name) default['origin'] = '' default['backorder_id'] = False if 'invoice_state' not in default and picking_obj.invoice_state == 'invoiced': default['invoice_state'] = '2binvoiced' - res=super(stock_picking, self).copy(cr, uid, id, default, context) - if res: - picking_obj = self.browse(cr, uid, res, context=context) - for move in picking_obj.move_lines: - move_obj.write(cr, uid, [move.id], {'tracking_id': False,'prodlot_id':False, 'move_history_ids2': [(6, 0, [])], 'move_history_ids': [(6, 0, [])]}) + res = super(stock_picking, self).copy(cr, uid, id, default, context) return res - + def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False): if view_type == 'form' and not view_id: mod_obj = self.pool.get('ir.model.data') if self._name == "stock.picking.in": - model,view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_in_form') + model, view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_in_form') if self._name == "stock.picking.out": - model,view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form') - return super(stock_picking,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu) + model, view_id = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form') + return super(stock_picking, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu) def onchange_partner_in(self, cr, uid, ids, partner_id=None, context=None): return {} @@ -740,7 +749,12 @@ class stock_picking(osv.osv): @return: True """ pickings = self.browse(cr, uid, ids, context=context) - self.write(cr, uid, ids, {'state': 'confirmed'}) + to_update = [] + for pick in pickings: + if pick.state != 'confirmed': + to_update.append(pick.id) + if to_update: + self.write(cr, uid, to_update, {'state': 'confirmed'}) todo = [] for picking in pickings: for r in picking.move_lines: @@ -808,18 +822,21 @@ class stock_picking(osv.osv): """ Cancels picking and moves. @return: True """ - wf_service = netsvc.LocalService("workflow") for pick in self.browse(cr, uid, ids): move_ids = [x.id for x in pick.move_lines] self.pool.get('stock.move').cancel_assign(cr, uid, move_ids) - wf_service.trg_write(uid, 'stock.picking', pick.id, cr) return True def action_assign_wkf(self, cr, uid, ids, context=None): """ Changes picking state to assigned. @return: True """ - self.write(cr, uid, ids, {'state': 'assigned'}) + to_update = [] + for pick in self.browse(cr, uid, ids, context=context): + if pick.state != 'assigned': + to_update.append(pick.id) + if to_update: + self.write(cr, uid, to_update, {'state': 'assigned'}) return True def test_finished(self, cr, uid, ids): @@ -849,6 +866,8 @@ class stock_picking(osv.osv): if all([x.state != 'waiting' for x in pick.move_lines]): return True for move in pick.move_lines: + if (move.state) == 'waiting': + move.check_assign() if (move.state in ('confirmed', 'draft')) and (mt == 'one'): return False if (mt == 'direct') and (move.state == 'assigned') and (move.product_qty): @@ -1117,19 +1136,19 @@ class stock_picking(osv.osv): if isinstance(partner, int): partner = partner_obj.browse(cr, uid, [partner], context=context)[0] if not partner: - raise osv.except_osv(_('Error, no partner !'), + raise osv.except_osv(_('Error, no partner!'), _('Please put a partner on the picking list if you want to generate invoice.')) if not inv_type: inv_type = self._get_invoice_type(picking) + invoice_vals = self._prepare_invoice(cr, uid, picking, partner, inv_type, journal_id, context=context) if group and partner.id in invoices_group: invoice_id = invoices_group[partner.id] invoice = invoice_obj.browse(cr, uid, invoice_id) invoice_vals_group = self._prepare_invoice_group(cr, uid, picking, partner, invoice, context=context) invoice_obj.write(cr, uid, [invoice_id], invoice_vals_group, context=context) else: - invoice_vals = self._prepare_invoice(cr, uid, picking, partner, inv_type, journal_id, context=context) invoice_id = invoice_obj.create(cr, uid, invoice_vals, context=context) invoices_group[partner.id] = invoice_id res[picking.id] = invoice_id @@ -1259,17 +1278,17 @@ class stock_picking(osv.osv): context['currency_id'] = move_currency_id qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) - if product.id in product_avail: - product_avail[product.id] += qty - else: + if product.id not in product_avail: + # keep track of stock on hand including processed lines not yet marked as done product_avail[product.id] = product.qty_available if qty > 0: new_price = currency_obj.compute(cr, uid, product_currency, - move_currency_id, product_price) + move_currency_id, product_price, round=False) new_price = uom_obj._compute_price(cr, uid, product_uom, new_price, product.uom_id.id) - if product.qty_available <= 0: + if product_avail[product.id] <= 0: + product_avail[product.id] = 0 new_std_price = new_price else: # Get the standard price @@ -1285,6 +1304,9 @@ class stock_picking(osv.osv): {'price_unit': product_price, 'price_currency_id': product_currency}) + product_avail[product.id] += qty + + for move in too_few: product_qty = move_product_qty[move.id] @@ -1351,9 +1373,9 @@ class stock_picking(osv.osv): self.action_move(cr, uid, [new_picking], context=context) wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_done', cr) wf_service.trg_write(uid, 'stock.picking', pick.id, cr) - delivered_pack_id = new_picking + delivered_pack_id = pick.id back_order_name = self.browse(cr, uid, delivered_pack_id, context=context).name - self.message_post(cr, uid, ids, body=_("Back order %s has been created.") % (back_order_name), context=context) + self.message_post(cr, uid, new_picking, body=_("Back order %s has been created.") % (back_order_name), context=context) else: self.action_move(cr, uid, [pick.id], context=context) wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr) @@ -1646,7 +1668,7 @@ class stock_move(osv.osv): "* Waiting Availability: This state is reached when the procurement resolution is not straight forward. It may need the scheduler to run, a component to me manufactured...\n"\ "* Available: When products are reserved, it is set to \'Available\'.\n"\ "* Done: When the shipment is processed, the state is \'Done\'."), - 'price_unit': fields.float('Unit Price', digits_compute= dp.get_precision('Account'), help="Technical field used to record the product cost set by the user during a picking confirmation (when average price costing method is used)"), + 'price_unit': fields.float('Unit Price', digits_compute= dp.get_precision('Product Price'), help="Technical field used to record the product cost set by the user during a picking confirmation (when average price costing method is used)"), 'price_currency_id': fields.many2one('res.currency', 'Currency for average price', help="Technical field used to record the currency chosen by the user during a picking confirmation (when average price costing method is used)"), 'company_id': fields.many2one('res.company', 'Company', required=True, select=True), 'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Order of", select=True), @@ -1701,7 +1723,13 @@ class stock_move(osv.osv): elif picking_type == 'out': location_xml_id = 'stock_location_customers' if location_xml_id: - location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id) + try: + location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id) + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context) + except (orm.except_orm, ValueError): + location_id = False + return location_id def _default_location_source(self, cr, uid, context=None): @@ -1730,7 +1758,13 @@ class stock_move(osv.osv): elif picking_type in ('out', 'internal'): location_xml_id = 'stock_location_stock' if location_xml_id: - location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id) + try: + location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id) + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context) + except (orm.except_orm, ValueError): + location_id = False + return location_id def _default_destination_address(self, cr, uid, context=None): @@ -1773,16 +1807,19 @@ class stock_move(osv.osv): for move in self.browse(cr, uid, ids, context=context): if move.state == 'done': if frozen_fields.intersection(vals): - raise osv.except_osv(_('Operation forbidden !'), + raise osv.except_osv(_('Operation Forbidden!'), _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).')) return super(stock_move, self).write(cr, uid, ids, vals, context=context) - def copy(self, cr, uid, id, default=None, context=None): + def copy_data(self, cr, uid, id, default=None, context=None): if default is None: default = {} default = default.copy() - default.update({'move_history_ids2': [], 'move_history_ids': []}) - return super(stock_move, self).copy(cr, uid, id, default, context=context) + default.setdefault('tracking_id', False) + default.setdefault('prodlot_id', False) + default.setdefault('move_history_ids', []) + default.setdefault('move_history_ids2', []) + return super(stock_move, self).copy_data(cr, uid, id, default, context=context) def _auto_init(self, cursor, context=None): res = super(stock_move, self)._auto_init(cursor, context=context) @@ -1874,7 +1911,6 @@ class stock_move(osv.osv): result = { 'product_qty': 0.00 } - warning = {} if (not product_id) or (product_uos_qty <=0.0): result['product_uos_qty'] = 0.0 @@ -1882,22 +1918,15 @@ class stock_move(osv.osv): product_obj = self.pool.get('product.product') uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff']) - - # Warn if the quantity was decreased - for move in self.read(cr, uid, ids, ['product_uos_qty']): - if product_uos_qty < move['product_uos_qty']: - warning.update({ - 'title': _('Warning: No Back Order'), - 'message': _("By changing the quantity here, you accept the " - "new quantity as complete: OpenERP will not " - "automatically generate a Back Order.") }) - break + + # No warning if the quantity was decreased to avoid double warnings: + # The clients should call onchange_quantity too anyway if product_uos and product_uom and (product_uom != product_uos): result['product_qty'] = product_uos_qty / uos_coeff['uos_coeff'] else: result['product_qty'] = product_uos_qty - return {'value': result, 'warning': warning} + return {'value': result} def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, partner_id=False): @@ -1910,7 +1939,8 @@ class stock_move(osv.osv): """ if not prod_id: return {} - lang = False + user = self.pool.get('res.users').browse(cr, uid, uid) + lang = user and user.lang or False if partner_id: addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id) if addr_rec: @@ -1948,8 +1978,18 @@ class stock_move(osv.osv): elif type == 'out': location_source_id = 'stock_location_stock' location_dest_id = 'stock_location_customers' - source_location = mod_obj.get_object_reference(cr, uid, 'stock', location_source_id) - dest_location = mod_obj.get_object_reference(cr, uid, 'stock', location_dest_id) + try: + source_location = mod_obj.get_object_reference(cr, uid, 'stock', location_source_id) + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [source_location[1]], 'read', context=context) + except (orm.except_orm, ValueError): + source_location = False + try: + dest_location = mod_obj.get_object_reference(cr, uid, 'stock', location_dest_id) + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [dest_location[1]], 'read', context=context) + except (orm.except_orm, ValueError): + dest_location = False return {'value':{'location_id': source_location and source_location[1] or False, 'location_dest_id': dest_location and dest_location[1] or False}} def onchange_date(self, cr, uid, ids, date, date_expected, context=None): @@ -2042,38 +2082,46 @@ class stock_move(osv.osv): if context is None: context = {} seq_obj = self.pool.get('ir.sequence') - for picking, todo in self._chain_compute(cr, uid, moves, context=context).items(): - ptype = todo[0][1][5] and todo[0][1][5] or location_obj.picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0]) - if picking: - # name of new picking according to its type - new_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + ptype) - pickid = self._create_chained_picking(cr, uid, new_pick_name, picking, ptype, todo, context=context) - # Need to check name of old picking because it always considers picking as "OUT" when created from Sales Order - old_ptype = location_obj.picking_type_get(cr, uid, picking.move_lines[0].location_id, picking.move_lines[0].location_dest_id) - if old_ptype != picking.type: - old_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + old_ptype) - self.pool.get('stock.picking').write(cr, uid, [picking.id], {'name': old_pick_name, 'type': old_ptype}, context=context) - else: - pickid = False - for move, (loc, dummy, delay, dummy, company_id, ptype, invoice_state) in todo: - new_id = move_obj.copy(cr, uid, move.id, { - 'location_id': move.location_dest_id.id, - 'location_dest_id': loc.id, - 'date': time.strftime('%Y-%m-%d'), - 'picking_id': pickid, - 'state': 'waiting', - 'company_id': company_id or res_obj._company_default_get(cr, uid, 'stock.company', context=context) , - 'move_history_ids': [], - 'date_expected': (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta(days=delay or 0)).strftime('%Y-%m-%d'), - 'move_history_ids2': []} - ) - move_obj.write(cr, uid, [move.id], { - 'move_dest_id': new_id, - 'move_history_ids': [(4, new_id)] - }) - new_moves.append(self.browse(cr, uid, [new_id])[0]) - if pickid: - wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr) + for picking, chained_moves in self._chain_compute(cr, uid, moves, context=context).items(): + # We group the moves by automatic move type, so it creates different pickings for different types + moves_by_type = {} + for move in chained_moves: + moves_by_type.setdefault(move[1][1], []).append(move) + for todo in moves_by_type.values(): + ptype = todo[0][1][5] and todo[0][1][5] or location_obj.picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0]) + if picking: + # name of new picking according to its type + if ptype == 'internal': + new_pick_name = seq_obj.get(cr, uid,'stock.picking') + else : + new_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + ptype) + pickid = self._create_chained_picking(cr, uid, new_pick_name, picking, ptype, todo, context=context) + # Need to check name of old picking because it always considers picking as "OUT" when created from Sales Order + old_ptype = location_obj.picking_type_get(cr, uid, picking.move_lines[0].location_id, picking.move_lines[0].location_dest_id) + if old_ptype != picking.type: + old_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + old_ptype) + self.pool.get('stock.picking').write(cr, uid, [picking.id], {'name': old_pick_name, 'type': old_ptype}, context=context) + else: + pickid = False + for move, (loc, dummy, delay, dummy, company_id, ptype, invoice_state) in todo: + new_id = move_obj.copy(cr, uid, move.id, { + 'location_id': move.location_dest_id.id, + 'location_dest_id': loc.id, + 'date': time.strftime('%Y-%m-%d'), + 'picking_id': pickid, + 'state': 'waiting', + 'company_id': company_id or res_obj._company_default_get(cr, uid, 'stock.company', context=context) , + 'move_history_ids': [], + 'date_expected': (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta(days=delay or 0)).strftime('%Y-%m-%d'), + 'move_history_ids2': []} + ) + move_obj.write(cr, uid, [move.id], { + 'move_dest_id': new_id, + 'move_history_ids': [(4, new_id)] + }) + new_moves.append(self.browse(cr, uid, [new_id])[0]) + if pickid: + wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr) if new_moves: new_moves += self.create_chained_picking(cr, uid, new_moves, context) return new_moves @@ -2118,10 +2166,12 @@ class stock_move(osv.osv): # fix for bug lp:707031 # called write of related picking because changing move availability does # not trigger workflow of picking in order to change the state of picking + seen = set() wf_service = netsvc.LocalService('workflow') for move in self.browse(cr, uid, ids, context): - if move.picking_id: + if move.picking_id and move.picking_id.id not in seen: wf_service.trg_write(uid, 'stock.picking', move.picking_id.id, cr) + seen.add(move.picking_id.id) return True # @@ -2154,11 +2204,12 @@ class stock_move(osv.osv): done.append(move.id) pickings[move.picking_id.id] = 1 r = res.pop(0) - product_uos_qty = self.pool.get('stock.move').onchange_quantity(cr, uid, ids, move.product_id.id, r[0], move.product_id.uom_id.id, move.product_id.uos_id.id)['value']['product_uos_qty'] + product_uos_qty = self.pool.get('stock.move').onchange_quantity(cr, uid, [move.id], move.product_id.id, r[0], move.product_id.uom_id.id, move.product_id.uos_id.id)['value']['product_uos_qty'] cr.execute('update stock_move set location_id=%s, product_qty=%s, product_uos_qty=%s where id=%s', (r[1], r[0],product_uos_qty, move.id)) while res: r = res.pop(0) + product_uos_qty = self.pool.get('stock.move').onchange_quantity(cr, uid, [move.id], move.product_id.id, r[0], move.product_id.uom_id.id, move.product_id.uos_id.id)['value']['product_uos_qty'] move_id = self.copy(cr, uid, move.id, {'product_uos_qty': product_uos_qty, 'product_qty': r[0], 'location_id': r[1]}) done.append(move_id) if done: @@ -2201,15 +2252,15 @@ class stock_move(osv.osv): if move.picking_id: pickings.add(move.picking_id.id) if move.move_dest_id and move.move_dest_id.state == 'waiting': - self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}) + self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context) if context.get('call_unlink',False) and move.move_dest_id.picking_id: wf_service = netsvc.LocalService("workflow") wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr) - self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}) + self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context) if not context.get('call_unlink',False): for pick in self.pool.get('stock.picking').browse(cr, uid, list(pickings), context=context): if all(move.state == 'cancel' for move in pick.move_lines): - self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'}) + self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'}, context=context) wf_service = netsvc.LocalService("workflow") for id in ids: @@ -2275,7 +2326,9 @@ class stock_move(osv.osv): # if product is set to average price and a specific value was entered in the picking wizard, # we use it - if move.product_id.cost_method == 'average' and move.price_unit: + if move.location_dest_id.usage != 'internal' and move.product_id.cost_method == 'average': + reference_amount = qty * move.product_id.standard_price + elif move.product_id.cost_method == 'average' and move.price_unit: reference_amount = qty * move.price_unit reference_currency_id = move.price_currency_id.id or reference_currency_id @@ -2333,8 +2386,7 @@ class stock_move(osv.osv): { 'journal_id': j_id, 'line_id': move_lines, - 'ref': move.picking_id and move.picking_id.name}) - + 'ref': move.picking_id and move.picking_id.name}, context=context) def action_done(self, cr, uid, ids, context=None): """ Makes the move done and if all moves are done, it will finish the picking. @@ -2363,7 +2415,7 @@ class stock_move(osv.osv): picking_ids.append(move.picking_id.id) if move.move_dest_id.id and (move.state != 'done'): # Downstream move should only be triggered if this move is the last pending upstream move - other_upstream_move_ids = self.search(cr, uid, [('id','!=',move.id),('state','not in',['done','cancel']), + other_upstream_move_ids = self.search(cr, uid, [('id','not in',move_ids),('state','not in',['done','cancel']), ('move_dest_id','=',move.move_dest_id.id)], context=context) if not other_upstream_move_ids: self.write(cr, uid, [move.id], {'move_history_ids': [(4, move.move_dest_id.id)]}) @@ -2381,7 +2433,7 @@ class stock_move(osv.osv): if todo: self.action_confirm(cr, uid, todo, context=context) - self.write(cr, uid, move_ids, {'state': 'done', 'date': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) + self.write(cr, uid, move_ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) for id in move_ids: wf_service.trg_trigger(uid, 'stock.move', id, cr) @@ -2396,7 +2448,7 @@ class stock_move(osv.osv): processing of the given stock move. """ # prepare default values considering that the destination accounts have the reference_currency_id as their main currency - partner_id = (move.picking_id.partner_id and move.picking_id.partner_id.id and move.picking_id.partner_id.id) or False + partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False debit_line_vals = { 'name': move.name, 'product_id': move.product_id and move.product_id.id or False, @@ -2431,7 +2483,7 @@ class stock_move(osv.osv): # fix credit line: credit_line_vals['credit'] = cur_obj.compute(cr, uid, reference_currency_id, src_main_currency_id, reference_amount, context=context) if (not src_acct.currency_id) or src_acct.currency_id.id == reference_currency_id: - credit_line_vals.update(currency_id=reference_currency_id, amount_currency=reference_amount) + credit_line_vals.update(currency_id=reference_currency_id, amount_currency=-reference_amount) if reference_currency_id != dest_main_currency_id: # fix debit line: debit_line_vals['debit'] = cur_obj.compute(cr, uid, reference_currency_id, dest_main_currency_id, reference_amount, context=context) @@ -2445,9 +2497,8 @@ class stock_move(osv.osv): context = {} ctx = context.copy() for move in self.browse(cr, uid, ids, context=context): - if move.state != 'draft' and not ctx.get('call_unlink',False): - raise osv.except_osv(_('User Error!'), - _('You can only delete draft moves.')) + if move.state != 'draft' and not ctx.get('call_unlink', False): + raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.')) return super(stock_move, self).unlink( cr, uid, ids, context=ctx) @@ -2475,13 +2526,20 @@ class stock_move(osv.osv): raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.')) res = [] for move in self.browse(cr, uid, ids, context=context): + source_location = move.location_id + if move.state == 'done': + source_location = move.location_dest_id + if source_location.usage != 'internal': + #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere) + raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.')) move_qty = move.product_qty uos_qty = quantity / move_qty * move.product_uos_qty default_val = { + 'location_id': source_location.id, 'product_qty': quantity, 'product_uos_qty': uos_qty, 'state': move.state, - 'scrapped' : True, + 'scrapped': True, 'location_dest_id': location_id, 'tracking_id': move.tracking_id.id, 'prodlot_id': move.prodlot_id.id, @@ -2592,7 +2650,7 @@ class stock_move(osv.osv): quantity = move.product_qty uos_qty = quantity / move_qty * move.product_uos_qty - if quantity_rest > 0: + if float_compare(quantity_rest, 0, precision_rounding=move.product_id.uom_id.rounding): default_val = { 'product_qty': quantity, 'product_uos_qty': uos_qty, @@ -2667,7 +2725,7 @@ class stock_move(osv.osv): qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) if qty > 0: new_price = currency_obj.compute(cr, uid, product_currency, - move_currency_id, product_price) + move_currency_id, product_price, round=False) new_price = uom_obj._compute_price(cr, uid, product_uom, new_price, product.uom_id.id) if product.qty_available <= 0: @@ -2879,8 +2937,13 @@ class stock_inventory_line(osv.osv): } def _default_stock_location(self, cr, uid, context=None): - stock_location = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock') - return stock_location.id + try: + location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock') + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context) + except (orm.except_orm, ValueError): + location_id = False + return location_id _defaults = { 'location_id': _default_stock_location @@ -2919,12 +2982,24 @@ class stock_warehouse(osv.osv): } def _default_lot_input_stock_id(self, cr, uid, context=None): - lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock') - return lot_input_stock.id + try: + lot_input_stock_model, lot_input_stock_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock') + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [lot_input_stock_id], 'read', context=context) + except (ValueError, orm.except_orm): + # the user does not have read access on the location or it does not exists + lot_input_stock_id = False + return lot_input_stock_id def _default_lot_output_id(self, cr, uid, context=None): - lot_output = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_output') - return lot_output.id + try: + lot_output_model, lot_output_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_output') + with tools.mute_logger('openerp.osv.orm'): + self.pool.get('stock.location').check_access_rule(cr, uid, [lot_output_id], 'read', context=context) + except (ValueError, orm.except_orm): + # the user does not have read access on the location or it does not exists + lot_output_id = False + return lot_output_id _defaults = { 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c), @@ -2945,6 +3020,15 @@ class stock_picking_in(osv.osv): _table = "stock_picking" _description = "Incoming Shipments" + def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False): + return self.pool.get('stock.picking').search(cr, user, args, offset, limit, order, context, count) + + def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): + return self.pool.get('stock.picking').read(cr, uid, ids, fields=fields, context=context, load=load) + + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): + return self.pool['stock.picking'].read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + def check_access_rights(self, cr, uid, operation, raise_exception=True): #override in order to redirect the check of acces rights on the stock.picking object return self.pool.get('stock.picking').check_access_rights(cr, uid, operation, raise_exception=raise_exception) @@ -2963,6 +3047,25 @@ class stock_picking_in(osv.osv): #instead of it's own workflow (which is not existing) return self.pool.get('stock.picking')._workflow_signal(cr, uid, ids, signal, context=context) + def message_post(self, *args, **kwargs): + """Post the message on stock.picking to be able to see it in the form view when using the chatter""" + return self.pool.get('stock.picking').message_post(*args, **kwargs) + + def message_subscribe(self, *args, **kwargs): + """Send the subscribe action on stock.picking model as it uses _name in request""" + return self.pool.get('stock.picking').message_subscribe(*args, **kwargs) + + def message_unsubscribe(self, *args, **kwargs): + """Send the unsubscribe action on stock.picking model to match with subscribe""" + return self.pool.get('stock.picking').message_unsubscribe(*args, **kwargs) + + def default_get(self, cr, uid, fields_list, context=None): + # merge defaults from stock.picking with possible defaults defined on stock.picking.in + defaults = self.pool['stock.picking'].default_get(cr, uid, fields_list, context=context) + in_defaults = super(stock_picking_in, self).default_get(cr, uid, fields_list, context=context) + defaults.update(in_defaults) + return defaults + _columns = { 'backorder_id': fields.many2one('stock.picking.in', 'Back Order of', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.", select=True), 'state': fields.selection( @@ -2990,6 +3093,15 @@ class stock_picking_out(osv.osv): _table = "stock_picking" _description = "Delivery Orders" + def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False): + return self.pool.get('stock.picking').search(cr, user, args, offset, limit, order, context, count) + + def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): + return self.pool.get('stock.picking').read(cr, uid, ids, fields=fields, context=context, load=load) + + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): + return self.pool['stock.picking'].read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + def check_access_rights(self, cr, uid, operation, raise_exception=True): #override in order to redirect the check of acces rights on the stock.picking object return self.pool.get('stock.picking').check_access_rights(cr, uid, operation, raise_exception=raise_exception) @@ -3008,6 +3120,25 @@ class stock_picking_out(osv.osv): #instead of it's own workflow (which is not existing) return self.pool.get('stock.picking')._workflow_signal(cr, uid, ids, signal, context=context) + def message_post(self, *args, **kwargs): + """Post the message on stock.picking to be able to see it in the form view when using the chatter""" + return self.pool.get('stock.picking').message_post(*args, **kwargs) + + def message_subscribe(self, *args, **kwargs): + """Send the subscribe action on stock.picking model as it uses _name in request""" + return self.pool.get('stock.picking').message_subscribe(*args, **kwargs) + + def message_unsubscribe(self, *args, **kwargs): + """Send the unsubscribe action on stock.picking model to match with subscribe""" + return self.pool.get('stock.picking').message_unsubscribe(*args, **kwargs) + + def default_get(self, cr, uid, fields_list, context=None): + # merge defaults from stock.picking with possible defaults defined on stock.picking.out + defaults = self.pool['stock.picking'].default_get(cr, uid, fields_list, context=context) + out_defaults = super(stock_picking_out, self).default_get(cr, uid, fields_list, context=context) + defaults.update(out_defaults) + return defaults + _columns = { 'backorder_id': fields.many2one('stock.picking.out', 'Back Order of', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.", select=True), 'state': fields.selection(