From: Christophe Simonis Date: Wed, 30 Jul 2014 18:30:14 +0000 (+0200) Subject: [MERGE] forward port of branch saas-4 up to fa07bc8 X-Git-Tag: 8.0.0~25^2~60 X-Git-Url: http://git.inspyration.org/?a=commitdiff_plain;h=e4cb5202a056ba0ad666af0268861e672a2cdab3;p=odoo%2Fodoo.git [MERGE] forward port of branch saas-4 up to fa07bc8 --- e4cb5202a056ba0ad666af0268861e672a2cdab3 diff --cc addons/delivery/delivery.py index 4bc3118,7b3791b..c2039e9 --- a/addons/delivery/delivery.py +++ b/addons/delivery/delivery.py @@@ -211,15 -195,14 +211,15 @@@ class delivery_grid(osv.osv) for line in order.order_line: if not line.product_id or line.is_delivery: continue - q = product_uom_obj._compute_qty(cr, uid, line.product_uom.id, line.product_uos_qty, line.product_id.uom_id.id) + q = product_uom_obj._compute_qty(cr, uid, line.product_uom.id, line.product_uom_qty, line.product_id.uom_id.id) weight += (line.product_id.weight or 0.0) * q volume += (line.product_id.volume or 0.0) * q + quantity += q total = order.amount_total or 0.0 - return self.get_price_from_picking(cr, uid, id, total,weight, volume, context=context) + return self.get_price_from_picking(cr, uid, id, total,weight, volume, quantity, context=context) - def get_price_from_picking(self, cr, uid, id, total, weight, volume, context=None): + def get_price_from_picking(self, cr, uid, id, total, weight, volume, quantity, context=None): grid = self.browse(cr, uid, id, context=context) price = 0.0 ok = False diff --cc addons/project/report/project_report.py index e3f2cd6,30a6110..515973d --- a/addons/project/report/project_report.py +++ b/addons/project/report/project_report.py @@@ -48,9 -48,8 +48,8 @@@ class report_project_task_user(osv.osv) help="Number of Days to Open the task"), 'delay_endings_days': fields.float('Overpassed Deadline', digits=(16,2), readonly=True), 'nbr': fields.integer('# of tasks', readonly=True), - 'priority': fields.selection([('4', 'Very Low'), ('3', 'Low'), ('2', 'Medium'), ('1', 'Urgent'), ('0', 'Very urgent')], + 'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], string='Priority', readonly=True), - 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')],'Status', readonly=True), 'company_id': fields.many2one('res.company', 'Company', readonly=True), 'partner_id': fields.many2one('res.partner', 'Contact', readonly=True), 'stage_id': fields.many2one('project.task.type', 'Stage'), diff --cc addons/stock/stock.py index 6b8a7c4,3e0a7d5..08b5e13 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@@ -892,562 -893,596 +892,561 @@@ class stock_picking(osv.osv) todo = [] for move in pick.move_lines: if move.state == 'draft': - self.pool.get('stock.move').action_confirm(cr, uid, [move.id], - context=context) - todo.append(move.id) - elif move.state in ('assigned','confirmed'): + todo.extend(self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context)) + elif move.state in ('assigned', 'confirmed'): todo.append(move.id) if len(todo): - self.pool.get('stock.move').action_done(cr, uid, todo, - context=context) + self.pool.get('stock.move').action_done(cr, uid, todo, context=context) return True - def get_currency_id(self, cr, uid, picking): - return False - - def _get_partner_to_invoice(self, cr, uid, picking, context=None): - """ Gets the partner that will be invoiced - Note that this function is inherited in the sale and purchase modules - @param picking: object of the picking for which we are selecting the partner to invoice - @return: object of the partner to invoice - """ - return picking.partner_id and picking.partner_id.id - - def _get_comment_invoice(self, cr, uid, picking): - """ - @return: comment string for invoice - """ - return picking.note or '' - - def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None): - """ Gets price unit for invoice - @param move_line: Stock move lines - @param type: Type of invoice - @return: The price unit for the move line - """ - if context is None: - context = {} - - if type in ('in_invoice', 'in_refund'): - # Take the user company and pricetype - context['currency_id'] = move_line.company_id.currency_id.id - amount_unit = move_line.product_id.price_get('standard_price', context=context)[move_line.product_id.id] - return amount_unit - else: - return move_line.product_id.list_price + def unlink(self, cr, uid, ids, context=None): + #on picking deletion, cancel its move then unlink them too + move_obj = self.pool.get('stock.move') + context = context or {} + for pick in self.browse(cr, uid, ids, context=context): + move_ids = [move.id for move in pick.move_lines] + move_obj.action_cancel(cr, uid, move_ids, context=context) + move_obj.unlink(cr, uid, move_ids, context=context) + return super(stock_picking, self).unlink(cr, uid, ids, context=context) - def _get_discount_invoice(self, cr, uid, move_line): - '''Return the discount for the move line''' - return 0.0 + def write(self, cr, uid, ids, vals, context=None): + res = super(stock_picking, self).write(cr, uid, ids, vals, context=context) + #if we changed the move lines or the pack operations, we need to recompute the remaining quantities of both + if 'move_lines' in vals or 'pack_operation_ids' in vals: + self.do_recompute_remaining_quantities(cr, uid, ids, context=context) + return res - def _get_taxes_invoice(self, cr, uid, move_line, type): - """ Gets taxes on invoice - @param move_line: Stock move lines - @param type: Type of invoice - @return: Taxes Ids for the move line + def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None): + """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines. """ - if type in ('in_invoice', 'in_refund'): - taxes = move_line.product_id.supplier_taxes_id - else: - taxes = move_line.product_id.taxes_id - - if move_line.picking_id and move_line.picking_id.partner_id and move_line.picking_id.partner_id.id: - return self.pool.get('account.fiscal.position').map_tax( - cr, - uid, - move_line.picking_id.partner_id.property_account_position, - taxes - ) - else: - return map(lambda x: x.id, taxes) - - def _get_account_analytic_invoice(self, cr, uid, picking, move_line): + if not backorder_moves: + backorder_moves = picking.move_lines + backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done', 'cancel')] + if 'do_only_split' in context and context['do_only_split']: + backorder_move_ids = [x.id for x in backorder_moves if x.id not in context.get('split', [])] + + if backorder_move_ids: + backorder_id = self.copy(cr, uid, picking.id, { + 'name': '/', + 'move_lines': [], + 'pack_operation_ids': [], + 'backorder_id': picking.id, + }) - back_order_name = self.browse(cr, uid, backorder_id, context=context).name - self.message_post(cr, uid, picking.id, body=_("Back order %s created.") % (back_order_name), context=context) ++ self.message_post(cr, uid, picking.id, body=_("Back order %s created.") % (picking.name), context=context) + move_obj = self.pool.get("stock.move") + move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context) + + self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) + self.action_confirm(cr, uid, [backorder_id], context=context) + return backorder_id return False - def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id): - '''Call after the creation of the invoice line''' - return - - def _invoice_hook(self, cr, uid, picking, invoice_id): - '''Call after the creation of the invoice''' - return - - def _get_invoice_type(self, pick): - src_usage = dest_usage = None - inv_type = None - if pick.invoice_state == '2binvoiced': - if pick.move_lines: - src_usage = pick.move_lines[0].location_id.usage - dest_usage = pick.move_lines[0].location_dest_id.usage - if pick.type == 'out' and dest_usage == 'supplier': - inv_type = 'in_refund' - elif pick.type == 'out' and dest_usage == 'customer': - inv_type = 'out_invoice' - elif pick.type == 'in' and src_usage == 'supplier': - inv_type = 'in_invoice' - elif pick.type == 'in' and src_usage == 'customer': - inv_type = 'out_refund' - else: - inv_type = 'out_invoice' - return inv_type - - def _prepare_invoice_group(self, cr, uid, picking, partner, invoice, context=None): - """ Builds the dict for grouped invoices - @param picking: picking object - @param partner: object of the partner to invoice (not used here, but may be usefull if this function is inherited) - @param invoice: object of the invoice that we are updating - @return: dict that will be used to update the invoice - """ - comment = self._get_comment_invoice(cr, uid, picking) - return { - 'name': (invoice.name or '') + ', ' + (picking.name or ''), - 'origin': (invoice.origin or '') + ', ' + (picking.name or '') + (picking.origin and (':' + picking.origin) or ''), - 'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''), - 'date_invoice': context.get('date_inv', False), - } + def recheck_availability(self, cr, uid, picking_ids, context=None): + self.action_assign(cr, uid, picking_ids, context=context) + self.do_prepare_partial(cr, uid, picking_ids, context=context) - def _prepare_invoice(self, cr, uid, picking, partner, inv_type, journal_id, context=None): - """ Builds the dict containing the values for the invoice - @param picking: picking object - @param partner: object of the partner to invoice - @param inv_type: type of the invoice ('out_invoice', 'in_invoice', ...) - @param journal_id: ID of the accounting journal - @return: dict that will be used to create the invoice object + def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None): + """This method searches for the higher level packages that can be moved as a single operation, given a list of quants + to move and their suggested destination, and returns the list of matching packages. """ - if isinstance(partner, int): - partner = self.pool.get('res.partner').browse(cr, uid, partner, context=context) - if inv_type in ('out_invoice', 'out_refund'): - account_id = partner.property_account_receivable.id - payment_term = partner.property_payment_term.id or False - else: - account_id = partner.property_account_payable.id - payment_term = partner.property_supplier_payment_term.id or False - comment = self._get_comment_invoice(cr, uid, picking) - invoice_vals = { - 'name': picking.name, - 'origin': (picking.name or '') + (picking.origin and (':' + picking.origin) or ''), - 'type': inv_type, - 'account_id': account_id, - 'partner_id': partner.id, - 'comment': comment, - 'payment_term': payment_term, - 'fiscal_position': partner.property_account_position.id, - 'date_invoice': context.get('date_inv', False), - 'company_id': picking.company_id.id, - 'user_id': uid, - } - cur_id = self.get_currency_id(cr, uid, picking) - if cur_id: - invoice_vals['currency_id'] = cur_id - if journal_id: - invoice_vals['journal_id'] = journal_id - return invoice_vals - - def _prepare_invoice_line(self, cr, uid, group, picking, move_line, invoice_id, - invoice_vals, context=None): - """ Builds the dict containing the values for the invoice line - @param group: True or False - @param picking: picking object - @param: move_line: move_line object - @param: invoice_id: ID of the related invoice - @param: invoice_vals: dict used to created the invoice - @return: dict that will be used to create the invoice line - """ - if group: - name = (picking.name or '') + '-' + move_line.name - else: - name = move_line.name - origin = move_line.picking_id.name or '' - if move_line.picking_id.origin: - origin += ':' + move_line.picking_id.origin - - if invoice_vals['type'] in ('out_invoice', 'out_refund'): - account_id = move_line.product_id.property_account_income.id - if not account_id: - account_id = move_line.product_id.categ_id.\ - property_account_income_categ.id - else: - account_id = move_line.product_id.property_account_expense.id - if not account_id: - account_id = move_line.product_id.categ_id.\ - property_account_expense_categ.id - if invoice_vals['fiscal_position']: - fp_obj = self.pool.get('account.fiscal.position') - fiscal_position = fp_obj.browse(cr, uid, invoice_vals['fiscal_position'], context=context) - account_id = fp_obj.map_account(cr, uid, fiscal_position, account_id) - # set UoS if it's a sale and the picking doesn't have one - uos_id = move_line.product_uos and move_line.product_uos.id or False - if not uos_id and invoice_vals['type'] in ('out_invoice', 'out_refund'): - uos_id = move_line.product_uom.id - - return { - 'name': name, - 'origin': origin, - 'invoice_id': invoice_id, - 'uos_id': uos_id, - 'product_id': move_line.product_id.id, - 'account_id': account_id, - 'price_unit': self._get_price_unit_invoice(cr, uid, move_line, invoice_vals['type']), - 'discount': self._get_discount_invoice(cr, uid, move_line), - 'quantity': move_line.product_uos_qty or move_line.product_qty, - 'invoice_line_tax_id': [(6, 0, self._get_taxes_invoice(cr, uid, move_line, invoice_vals['type']))], - 'account_analytic_id': self._get_account_analytic_invoice(cr, uid, picking, move_line), - } - - def action_invoice_create(self, cr, uid, ids, journal_id=False, - group=False, type='out_invoice', context=None): - """ Creates invoice based on the invoice state selected for picking. - @param journal_id: Id of journal - @param group: Whether to create a group invoice or not - @param type: Type invoice to be created - @return: Ids of created invoices for the pickings + # Try to find as much as possible top-level packages that can be moved + pack_obj = self.pool.get("stock.quant.package") + quant_obj = self.pool.get("stock.quant") + top_lvl_packages = set() + quants_to_compare = quants_suggested_locations.keys() + for pack in list(set([x.package_id for x in quants_suggested_locations.keys() if x and x.package_id])): + loop = True + test_pack = pack + good_pack = False + pack_destination = False + while loop: + pack_quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context) + all_in = True + for quant in quant_obj.browse(cr, uid, pack_quants, context=context): + # If the quant is not in the quants to compare and not in the common location + if not quant in quants_to_compare: + all_in = False + break + else: + #if putaway strat apply, the destination location of each quant may be different (and thus the package should not be taken as a single operation) + if not pack_destination: + pack_destination = quants_suggested_locations[quant] + elif pack_destination != quants_suggested_locations[quant]: + all_in = False + break + if all_in: + good_pack = test_pack + if test_pack.parent_id: + test_pack = test_pack.parent_id + else: + #stop the loop when there's no parent package anymore + loop = False + else: + #stop the loop when the package test_pack is not totally reserved for moves of this picking + #(some quants may be reserved for other picking or not reserved at all) + loop = False + if good_pack: + top_lvl_packages.add(good_pack) + return list(top_lvl_packages) + + def _prepare_pack_ops(self, cr, uid, picking, quants, forced_qties, context=None): + """ returns a list of dict, ready to be used in create() of stock.pack.operation. + + :param picking: browse record (stock.picking) + :param quants: browse record list (stock.quant). List of quants associated to the picking + :param forced_qties: dictionary showing for each product (keys) its corresponding quantity (value) that is not covered by the quants associated to the picking """ - if context is None: - context = {} + def _picking_putaway_apply(product): + location = False + # Search putaway strategy + if product_putaway_strats.get(product.id): + location = product_putaway_strats[product.id] + else: + location = self.pool.get('stock.location').get_putaway_strategy(cr, uid, picking.location_dest_id, product, context=context) + product_putaway_strats[product.id] = location + return location or picking.location_dest_id.id + + pack_obj = self.pool.get("stock.quant.package") + quant_obj = self.pool.get("stock.quant") + vals = [] + qtys_grouped = {} + #for each quant of the picking, find the suggested location + quants_suggested_locations = {} + product_putaway_strats = {} + for quant in quants: + if quant.qty <= 0: + continue + suggested_location_id = _picking_putaway_apply(quant.product_id) + quants_suggested_locations[quant] = suggested_location_id + + #find the packages we can movei as a whole + top_lvl_packages = self._get_top_level_packages(cr, uid, quants_suggested_locations, context=context) + # and then create pack operations for the top-level packages found + for pack in top_lvl_packages: + pack_quant_ids = pack_obj.get_content(cr, uid, [pack.id], context=context) + pack_quants = quant_obj.browse(cr, uid, pack_quant_ids, context=context) + vals.append({ + 'picking_id': picking.id, + 'package_id': pack.id, + 'product_qty': 1.0, + 'location_id': pack.location_id.id, + 'location_dest_id': quants_suggested_locations[pack_quants[0]], + }) + #remove the quants inside the package so that they are excluded from the rest of the computation + for quant in pack_quants: + del quants_suggested_locations[quant] + + # Go through all remaining reserved quants and group by product, package, lot, owner, source location and dest location + for quant, dest_location_id in quants_suggested_locations.items(): + key = (quant.product_id.id, quant.package_id.id, quant.lot_id.id, quant.owner_id.id, quant.location_id.id, dest_location_id) + if qtys_grouped.get(key): + qtys_grouped[key] += quant.qty + else: + qtys_grouped[key] = quant.qty - invoice_obj = self.pool.get('account.invoice') - invoice_line_obj = self.pool.get('account.invoice.line') - partner_obj = self.pool.get('res.partner') - invoices_group = {} - res = {} - inv_type = type - for picking in self.browse(cr, uid, ids, context=context): - if picking.invoice_state != '2binvoiced': + # Do the same for the forced quantities (in cases of force_assign or incomming shipment for example) + for product, qty in forced_qties.items(): + if qty <= 0: continue - partner = self._get_partner_to_invoice(cr, uid, picking, context=context) - if isinstance(partner, int): - partner = partner_obj.browse(cr, uid, [partner], context=context)[0] - if not 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) + suggested_location_id = _picking_putaway_apply(product) + key = (product.id, False, False, False, picking.location_id.id, suggested_location_id) + if qtys_grouped.get(key): + qtys_grouped[key] += qty else: - invoice_id = invoice_obj.create(cr, uid, invoice_vals, context=context) - invoices_group[partner.id] = invoice_id - res[picking.id] = invoice_id - for move_line in picking.move_lines: - if move_line.state == 'cancel': - continue - if move_line.scrapped: - # do no invoice scrapped products + qtys_grouped[key] = qty + + # Create the necessary operations for the grouped quants and remaining qtys + for key, qty in qtys_grouped.items(): + vals.append({ + 'picking_id': picking.id, + 'product_qty': qty, + 'product_id': key[0], + 'package_id': key[1], + 'lot_id': key[2], + 'owner_id': key[3], + 'location_id': key[4], + 'location_dest_id': key[5], + 'product_uom_id': self.pool.get("product.product").browse(cr, uid, key[0], context=context).uom_id.id, + }) + return vals + + def open_barcode_interface(self, cr, uid, picking_ids, context=None): + final_url="/barcode/web/#action=stock.ui&picking_id="+str(picking_ids[0]) + return {'type': 'ir.actions.act_url', 'url':final_url, 'target': 'self',} + + def do_partial_open_barcode(self, cr, uid, picking_ids, context=None): + self.do_prepare_partial(cr, uid, picking_ids, context=context) + return self.open_barcode_interface(cr, uid, picking_ids, context=context) + + def do_prepare_partial(self, cr, uid, picking_ids, context=None): + context = context or {} + pack_operation_obj = self.pool.get('stock.pack.operation') + #used to avoid recomputing the remaining quantities at each new pack operation created + ctx = context.copy() + ctx['no_recompute'] = True + + #get list of existing operations and delete them + existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context) + if existing_package_ids: + pack_operation_obj.unlink(cr, uid, existing_package_ids, context) + for picking in self.browse(cr, uid, picking_ids, context=context): + forced_qties = {} # Quantity remaining after calculating reserved quants + picking_quants = [] + #Calculate packages, reserved quants, qtys of this picking's moves + for move in picking.move_lines: + if move.state not in ('assigned', 'confirmed'): continue - vals = self._prepare_invoice_line(cr, uid, group, picking, move_line, - invoice_id, invoice_vals, context=context) - if vals: - invoice_line_id = invoice_line_obj.create(cr, uid, vals, context=context) - self._invoice_line_hook(cr, uid, move_line, invoice_line_id) - - invoice_obj.button_compute(cr, uid, [invoice_id], context=context, - set_total=(inv_type in ('in_invoice', 'in_refund'))) - self.write(cr, uid, [picking.id], { - 'invoice_state': 'invoiced', - }, context=context) - self._invoice_hook(cr, uid, picking, invoice_id) - self.write(cr, uid, res.keys(), { - 'invoice_state': 'invoiced', - }, context=context) - return res - - def test_done(self, cr, uid, ids, context=None): - """ Test whether the move lines are done or not. - @return: True or False + move_quants = move.reserved_quant_ids + picking_quants += move_quants + forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0 + #if we used force_assign() on the move, or if the move is incomming, forced_qty > 0 + if forced_qty: + if forced_qties.get(move.product_id): + forced_qties[move.product_id] += forced_qty + else: + forced_qties[move.product_id] = forced_qty + for vals in self._prepare_pack_ops(cr, uid, picking, picking_quants, forced_qties, context=context): + pack_operation_obj.create(cr, uid, vals, context=ctx) + #recompute the remaining quantities all at once + self.do_recompute_remaining_quantities(cr, uid, picking_ids, context=context) + self.write(cr, uid, picking_ids, {'recompute_pack_op': False}, context=context) + + def do_unreserve(self, cr, uid, picking_ids, context=None): """ - ok = False - for pick in self.browse(cr, uid, ids, context=context): - if not pick.move_lines: - return True - for move in pick.move_lines: - if move.state not in ('cancel','done'): - return False - if move.state=='done': - ok = True - return ok - - def test_cancel(self, cr, uid, ids, context=None): - """ Test whether the move lines are canceled or not. - @return: True or False + Will remove all quants for picking in picking_ids """ - for pick in self.browse(cr, uid, ids, context=context): - for move in pick.move_lines: - if move.state not in ('cancel',): - return False - return True - - def allow_cancel(self, cr, uid, ids, context=None): - for pick in self.browse(cr, uid, ids, context=context): - if not pick.move_lines: - return True - for move in pick.move_lines: - if move.state == 'done': - raise osv.except_osv(_('Error!'), _('You cannot cancel the picking as some moves have been done. You should cancel the picking lines.')) - return True - - def unlink(self, cr, uid, ids, context=None): - move_obj = self.pool.get('stock.move') - if context is None: - context = {} - for pick in self.browse(cr, uid, ids, context=context): - if pick.state in ['done','cancel']: - raise osv.except_osv(_('Error!'), _('You cannot remove the picking which is in %s state!')%(pick.state,)) + moves_to_unreserve = [] + pack_line_to_unreserve = [] + for picking in self.browse(cr, uid, picking_ids, context=context): + moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')] + pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids] + if moves_to_unreserve: + if pack_line_to_unreserve: + self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context) + self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context) + + def recompute_remaining_qty(self, cr, uid, picking, context=None): + def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False): + move_dict = prod2move_ids[product_id][index] + qty_on_link = min(move_dict['remaining_qty'], qty_to_assign) + self.pool.get('stock.move.operation.link').create(cr, uid, {'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id}, context=context) + if move_dict['remaining_qty'] == qty_on_link: + prod2move_ids[product_id].pop(index) else: - ids2 = [move.id for move in pick.move_lines] - ctx = context.copy() - ctx.update({'call_unlink':True}) - if pick.state != 'draft': - #Cancelling the move in order to affect Virtual stock of product - move_obj.action_cancel(cr, uid, ids2, ctx) - #Removing the move - move_obj.unlink(cr, uid, ids2, ctx) - - return super(stock_picking, self).unlink(cr, uid, ids, context=context) + move_dict['remaining_qty'] -= qty_on_link + return qty_on_link + + def _create_link_for_quant(operation_id, quant, qty): + """create a link for given operation and reserved move of given quant, for the max quantity possible, and returns this quantity""" + if not quant.reservation_id.id: + return _create_link_for_product(operation_id, quant.product_id.id, qty) + qty_on_link = 0 + for i in range(0, len(prod2move_ids[quant.product_id.id])): + if prod2move_ids[quant.product_id.id][i]['move'].id != quant.reservation_id.id: + continue + qty_on_link = _create_link_for_index(operation_id, i, quant.product_id.id, qty, quant_id=quant.id) + break + return qty_on_link + + def _create_link_for_product(operation_id, product_id, qty): + '''method that creates the link between a given operation and move(s) of given product, for the given quantity. + Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)''' + qty_to_assign = qty + if prod2move_ids.get(product_id): + while prod2move_ids[product_id] and qty_to_assign > 0: + qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False) + qty_to_assign -= qty_on_link + return qty_to_assign == 0 - # FIXME: needs refactoring, this code is partially duplicated in stock_move.do_partial()! - def do_partial(self, cr, uid, ids, partial_datas, context=None): - """ Makes partial picking and moves done. - @param partial_datas : Dictionary containing details of partial picking - like partner_id, partner_id, delivery_date, - delivery moves with product_id, product_qty, uom - @return: Dictionary of values - """ - if context is None: - context = {} - else: - context = dict(context) - res = {} - move_obj = self.pool.get('stock.move') - product_obj = self.pool.get('product.product') - currency_obj = self.pool.get('res.currency') uom_obj = self.pool.get('product.uom') - sequence_obj = self.pool.get('ir.sequence') - for pick in self.browse(cr, uid, ids, context=context): - new_picking = None - complete, too_many, too_few = [], [], [] - move_product_qty, prodlot_ids, product_avail, partial_qty, product_uoms = {}, {}, {}, {}, {} - for move in pick.move_lines: - if move.state in ('done', 'cancel'): - continue - partial_data = partial_datas.get('move%s'%(move.id), {}) - product_qty = partial_data.get('product_qty',0.0) - move_product_qty[move.id] = product_qty - product_uom = partial_data.get('product_uom',False) - product_price = partial_data.get('product_price',0.0) - product_currency = partial_data.get('product_currency',False) - prodlot_id = partial_data.get('prodlot_id') - prodlot_ids[move.id] = prodlot_id - product_uoms[move.id] = product_uom - partial_qty[move.id] = uom_obj._compute_qty(cr, uid, product_uoms[move.id], product_qty, move.product_uom.id) - if move.product_qty == partial_qty[move.id]: - complete.append(move) - elif move.product_qty > partial_qty[move.id]: - too_few.append(move) - else: - too_many.append(move) - - # Average price computation - if (pick.type == 'in') and (move.product_id.cost_method == 'average'): - product = product_obj.browse(cr, uid, move.product_id.id) - move_currency_id = move.company_id.currency_id.id - context['currency_id'] = move_currency_id - qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) - - 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, round=False) - new_price = uom_obj._compute_price(cr, uid, product_uom, new_price, - product.uom_id.id) - if product_avail[product.id] <= 0: - product_avail[product.id] = 0 - new_std_price = new_price + package_obj = self.pool.get('stock.quant.package') + quant_obj = self.pool.get('stock.quant') + quants_in_package_done = set() + prod2move_ids = {} + still_to_do = [] + #make a dictionary giving for each product, the moves and related quantity that can be used in operation links + for move in picking.move_lines: + if not prod2move_ids.get(move.product_id.id): + prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}] + else: + prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty}) + + need_rereserve = False + #sort the operations in order to give higher priority to those with a package, then a serial number + operations = picking.pack_operation_ids + operations.sort(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0)) + #delete existing operations to start again from scratch + cr.execute("DELETE FROM stock_move_operation_link WHERE operation_id in %s", (tuple([x.id for x in operations]),)) + + #1) first, try to create links when quants can be identified without any doubt + for ops in operations: + #for each operation, create the links with the stock move by seeking on the matching reserved quants, + #and deffer the operation if there is some ambiguity on the move to select + if ops.package_id and not ops.product_id: + #entire package + quant_ids = package_obj.get_content(cr, uid, [ops.package_id.id], context=context) + for quant in quant_obj.browse(cr, uid, quant_ids, context=context): + remaining_qty_on_quant = quant.qty + if quant.reservation_id: + #avoid quants being counted twice + quants_in_package_done.add(quant.id) + qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty) + remaining_qty_on_quant -= qty_on_link + if remaining_qty_on_quant: + still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant)) + need_rereserve = True + elif ops.product_id.id: + #Check moves with same product + qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context) + for move_dict in prod2move_ids.get(ops.product_id.id, []): + move = move_dict['move'] + for quant in move.reserved_quant_ids: + if not qty_to_assign > 0: + break + if quant.id in quants_in_package_done: + continue + + #check if the quant is matching the operation details + if ops.package_id: + flag = quant.package_id and bool(package_obj.search(cr, uid, [('id', 'child_of', [ops.package_id.id]), ('id', '=', quant.package_id.id)], context=context)) or False else: - # Get the standard price - amount_unit = product.price_get('standard_price', context=context)[product.id] - new_std_price = ((amount_unit * product_avail[product.id])\ - + (new_price * qty))/(product_avail[product.id] + qty) - # Write the field according to price type field - product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price}) - - # Record the values that were chosen in the wizard, so they can be - # used for inventory valuation if real-time valuation is enabled. - move_obj.write(cr, uid, [move.id], - {'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] - if not new_picking: - new_picking_name = pick.name - self.write(cr, uid, [pick.id], - {'name': sequence_obj.get(cr, uid, - 'stock.picking.%s'%(pick.type)), - }) - pick.refresh() - new_picking = self.copy(cr, uid, pick.id, - { - 'name': new_picking_name, - 'move_lines' : [], - 'state':'draft', - }) - if product_qty != 0: - defaults = { - 'product_qty' : product_qty, - 'product_uos_qty': product_qty, #TODO: put correct uos_qty - 'picking_id' : new_picking, - 'state': 'assigned', - 'move_dest_id': False, - 'price_unit': move.price_unit, - 'product_uom': product_uoms[move.id] + flag = not quant.package_id.id + flag = flag and ((ops.lot_id and ops.lot_id.id == quant.lot_id.id) or not ops.lot_id) + flag = flag and (ops.owner_id.id == quant.owner_id.id) + if flag: + max_qty_on_link = min(quant.qty, qty_to_assign) + qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link) + qty_to_assign -= qty_on_link + if qty_to_assign > 0: + #qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed + #all the quants (because they leave no choice on their related move and needs to be processed with higher priority) + still_to_do += [(ops, ops.product_id.id, qty_to_assign)] + need_rereserve = True + + #2) then, process the remaining part + all_op_processed = True + for ops, product_id, remaining_qty in still_to_do: + all_op_processed = all_op_processed and _create_link_for_product(ops.id, product_id, remaining_qty) + return (need_rereserve, all_op_processed) + + def picking_recompute_remaining_quantities(self, cr, uid, picking, context=None): + need_rereserve = False + all_op_processed = True + if picking.pack_operation_ids: + need_rereserve, all_op_processed = self.recompute_remaining_qty(cr, uid, picking, context=context) + return need_rereserve, all_op_processed + + def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None): + for picking in self.browse(cr, uid, picking_ids, context=context): + if picking.pack_operation_ids: + self.recompute_remaining_qty(cr, uid, picking, context=context) + + def _create_extra_moves(self, cr, uid, picking, context=None): + '''This function creates move lines on a picking, at the time of do_transfer, based on + unexpected product transfers (or exceeding quantities) found in the pack operations. + ''' + move_obj = self.pool.get('stock.move') + operation_obj = self.pool.get('stock.pack.operation') + moves = [] + for op in picking.pack_operation_ids: + for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items(): + if remaining_qty > 0: + product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) + vals = { + 'picking_id': picking.id, + 'location_id': picking.location_id.id, + 'location_dest_id': picking.location_dest_id.id, + 'product_id': product_id, + 'product_uom': product.uom_id.id, + 'product_uom_qty': remaining_qty, + 'name': _('Extra Move: ') + product.name, + 'state': 'draft', } - prodlot_id = prodlot_ids[move.id] - if prodlot_id: - defaults.update(prodlot_id=prodlot_id) - move_obj.copy(cr, uid, move.id, defaults) - move_obj.write(cr, uid, [move.id], - { - 'product_qty': move.product_qty - partial_qty[move.id], - 'product_uos_qty': move.product_qty - partial_qty[move.id], #TODO: put correct uos_qty - 'prodlot_id': False, - 'tracking_id': False, - }) - - if new_picking: - move_obj.write(cr, uid, [c.id for c in complete], {'picking_id': new_picking}) - for move in complete: - defaults = {'product_uom': product_uoms[move.id], 'product_qty': move_product_qty[move.id]} - if prodlot_ids.get(move.id): - defaults.update({'prodlot_id': prodlot_ids[move.id]}) - move_obj.write(cr, uid, [move.id], defaults) - for move in too_many: - product_qty = move_product_qty[move.id] - defaults = { - 'product_qty' : product_qty, - 'product_uos_qty': product_qty, #TODO: put correct uos_qty - 'product_uom': product_uoms[move.id] - } - prodlot_id = prodlot_ids.get(move.id) - if prodlot_ids.get(move.id): - defaults.update(prodlot_id=prodlot_id) - if new_picking: - defaults.update(picking_id=new_picking) - move_obj.write(cr, uid, [move.id], defaults) - - # At first we confirm the new picking (if necessary) - if new_picking: - self.signal_button_confirm(cr, uid, [new_picking]) - # Then we finish the good picking - self.write(cr, uid, [pick.id], {'backorder_id': new_picking}) - self.action_move(cr, uid, [new_picking], context=context) - self.signal_button_done(cr, uid, [new_picking]) - workflow.trg_write(uid, 'stock.picking', pick.id, cr) - delivered_pack_id = new_picking - self.message_post(cr, uid, new_picking, body=_("Back order %s has been created.") % (pick.name), context=context) - else: - self.action_move(cr, uid, [pick.id], context=context) - self.signal_button_done(cr, uid, [pick.id]) - delivered_pack_id = pick.id - - delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context) - res[pick.id] = {'delivered_picking': delivered_pack.id or False} - - return res - - # views associated to each picking type - _VIEW_LIST = { - 'out': 'view_picking_out_form', - 'in': 'view_picking_in_form', - 'internal': 'view_picking_form', - } - def _get_view_id(self, cr, uid, type): - """Get the view id suiting the given type - - @param type: the picking type as a string - @return: view i, or False if no view found - """ - res = self.pool.get('ir.model.data').get_object_reference(cr, uid, - 'stock', self._VIEW_LIST.get(type, 'view_picking_form')) - return res and res[1] or False - - -class stock_production_lot(osv.osv): - - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - reads = self.read(cr, uid, ids, ['name', 'prefix', 'ref'], context) - res = [] - for record in reads: - name = record['name'] - prefix = record['prefix'] - if prefix: - name = prefix + '/' + name - if record['ref']: - name = '%s [%s]' % (name, record['ref']) - res.append((record['id'], name)) - return res + moves.append(move_obj.create(cr, uid, vals, context=context)) + if moves: + move_obj.action_confirm(cr, uid, moves, context=context) + return moves - def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): - args = args or [] - ids = [] - if name: - ids = self.search(cr, uid, [('prefix', '=', name)] + args, limit=limit, context=context) - if not ids: - ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context) + def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None): + """ Unreserve quants then try to reassign quants.""" + stock_move_obj = self.pool.get('stock.move') + if not move_ids: + self.do_unreserve(cr, uid, [picking.id], context=context) + self.action_assign(cr, uid, [picking.id], context=context) else: - ids = self.search(cr, uid, args, limit=limit, context=context) - return self.name_get(cr, uid, ids, context) + stock_move_obj.do_unreserve(cr, uid, move_ids, context=context) + stock_move_obj.action_assign(cr, uid, move_ids, context=context) - _name = 'stock.production.lot' - _description = 'Serial Number' - - def _get_stock(self, cr, uid, ids, field_name, arg, context=None): - """ Gets stock of products for locations - @return: Dictionary of values + def do_transfer(self, cr, uid, picking_ids, context=None): + """ + If no pack operation, we do simple action_done of the picking + Otherwise, do the pack operations """ + if not context: + context = {} + stock_move_obj = self.pool.get('stock.move') + for picking in self.browse(cr, uid, picking_ids, context=context): + if not picking.pack_operation_ids: + self.action_done(cr, uid, [picking.id], context=context) + continue + else: + need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, picking, context=context) + #create extra moves in the picking (unexpected product moves coming from pack operations) + todo_move_ids = [] + if not all_op_processed: + todo_move_ids += self._create_extra_moves(cr, uid, picking, context=context) + + picking.refresh() + #split move lines eventually + + toassign_move_ids = [] + for move in picking.move_lines: + remaining_qty = move.remaining_qty + if move.state in ('done', 'cancel'): + #ignore stock moves cancelled or already done + continue + elif move.state == 'draft': + toassign_move_ids.append(move.id) + if remaining_qty == 0: + if move.state in ('draft', 'assigned', 'confirmed'): + todo_move_ids.append(move.id) + elif remaining_qty > 0 and remaining_qty < move.product_qty: + new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context) + todo_move_ids.append(move.id) + #Assign move as it was assigned before + toassign_move_ids.append(new_move) + if (need_rereserve or not all_op_processed) and not picking.location_id.usage in ("supplier", "production", "inventory"): + self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context) + self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context) + if todo_move_ids and not context.get('do_only_split'): + self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context) + elif context.get('do_only_split'): + context.update({'split': todo_move_ids}) + picking.refresh() + self._create_backorder(cr, uid, picking, context=context) + if toassign_move_ids: + stock_move_obj.action_assign(cr, uid, toassign_move_ids, context=context) + return True + + def do_split(self, cr, uid, picking_ids, context=None): + """ just split the picking (create a backorder) without making it 'done' """ if context is None: context = {} - if 'location_id' not in context: - locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context) - else: - locations = context['location_id'] and [context['location_id']] or [] + ctx = context.copy() + ctx['do_only_split'] = True + return self.do_transfer(cr, uid, picking_ids, context=ctx) - if isinstance(ids, (int, long)): - ids = [ids] + def get_next_picking_for_ui(self, cr, uid, context=None): + """ returns the next pickings to process. Used in the barcode scanner UI""" + if context is None: + context = {} + domain = [('state', 'in', ('assigned', 'partially_available'))] + if context.get('default_picking_type_id'): + domain.append(('picking_type_id', '=', context['default_picking_type_id'])) + return self.search(cr, uid, domain, context=context) + + def action_done_from_ui(self, cr, uid, picking_id, context=None): + """ called when button 'done' is pushed in the barcode scanner UI """ + #write qty_done into field product_qty for every package_operation before doing the transfer + pack_op_obj = self.pool.get('stock.pack.operation') + for operation in self.browse(cr, uid, picking_id, context=context).pack_operation_ids: + pack_op_obj.write(cr, uid, operation.id, {'product_qty': operation.qty_done}, context=context) + self.do_transfer(cr, uid, [picking_id], context=context) + #return id of next picking to work on + return self.get_next_picking_for_ui(cr, uid, context=context) + + def action_pack(self, cr, uid, picking_ids, operation_filter_ids=None, context=None): + """ Create a package with the current pack_operation_ids of the picking that aren't yet in a pack. + Used in the barcode scanner UI and the normal interface as well. + operation_filter_ids is used by barcode scanner interface to specify a subset of operation to pack""" + if operation_filter_ids == None: + operation_filter_ids = [] + stock_operation_obj = self.pool.get('stock.pack.operation') + package_obj = self.pool.get('stock.quant.package') + stock_move_obj = self.pool.get('stock.move') + for picking_id in picking_ids: + operation_search_domain = [('picking_id', '=', picking_id), ('result_package_id', '=', False)] + if operation_filter_ids != []: + operation_search_domain.append(('id', 'in', operation_filter_ids)) + operation_ids = stock_operation_obj.search(cr, uid, operation_search_domain, context=context) + pack_operation_ids = [] + if operation_ids: + for operation in stock_operation_obj.browse(cr, uid, operation_ids, context=context): + #If we haven't done all qty in operation, we have to split into 2 operation + op = operation + if (operation.qty_done < operation.product_qty): + new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context) + stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0, 'lot_id': False}, context=context) + op = stock_operation_obj.browse(cr, uid, new_operation, context=context) + pack_operation_ids.append(op.id) + for record in op.linked_move_operation_ids: + stock_move_obj.check_tracking(cr, uid, record.move_id, op.package_id.id or op.lot_id.id, context=context) + package_id = package_obj.create(cr, uid, {}, context=context) + stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context) + return True - res = {}.fromkeys(ids, 0.0) - if locations: - cr.execute('''select - prodlot_id, - sum(qty) - from - stock_report_prodlots - where - location_id IN %s and prodlot_id IN %s group by prodlot_id''',(tuple(locations),tuple(ids),)) - res.update(dict(cr.fetchall())) + def process_product_id_from_ui(self, cr, uid, picking_id, product_id, op_id, increment=True, context=None): + return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id),('id', '=', op_id)], increment=increment, context=context) - return res + def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, visible_op_ids, context=None): + '''This function is called each time there barcode scanner reads an input''' + lot_obj = self.pool.get('stock.production.lot') + package_obj = self.pool.get('stock.quant.package') + product_obj = self.pool.get('product.product') + stock_operation_obj = self.pool.get('stock.pack.operation') + stock_location_obj = self.pool.get('stock.location') + answer = {'filter_loc': False, 'operation_id': False} + #check if the barcode correspond to a location + matching_location_ids = stock_location_obj.search(cr, uid, [('loc_barcode', '=', barcode_str)], context=context) + if matching_location_ids: + #if we have a location, return immediatly with the location name + location = stock_location_obj.browse(cr, uid, matching_location_ids[0], context=None) + answer['filter_loc'] = stock_location_obj._name_get(cr, uid, location, context=None) + answer['filter_loc_id'] = matching_location_ids[0] + return answer + #check if the barcode correspond to a product + matching_product_ids = product_obj.search(cr, uid, ['|', ('ean13', '=', barcode_str), ('default_code', '=', barcode_str)], context=context) + if matching_product_ids: + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', matching_product_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context) + answer['operation_id'] = op_id + return answer + #check if the barcode correspond to a lot + matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', barcode_str)], context=context) + if matching_lot_ids: + lot = lot_obj.browse(cr, uid, matching_lot_ids[0], context=context) + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context) + answer['operation_id'] = op_id + return answer + #check if the barcode correspond to a package + matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context) + if matching_package_ids: + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context) + answer['operation_id'] = op_id + return answer + return answer - def _stock_search(self, cr, uid, obj, name, args, context=None): - """ Searches Ids of products - @return: Ids of locations - """ - locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')]) - cr.execute('''select - prodlot_id, - sum(qty) - from - stock_report_prodlots - where - location_id IN %s group by prodlot_id - having sum(qty) '''+ str(args[0][1]) + str(args[0][2]),(tuple(locations),)) - res = cr.fetchall() - ids = [('id', 'in', map(lambda x: x[0], res))] - return ids +class stock_production_lot(osv.osv): + _name = 'stock.production.lot' + _inherit = ['mail.thread'] + _description = 'Lot/Serial' _columns = { - 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number, will be displayed as: PREFIX/SERIAL [INT_REF]"), + 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number"), 'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"), - 'prefix': fields.char('Prefix', size=64, help="Optional prefix to prepend when displaying this serial number: PREFIX/SERIAL [INT_REF]"), 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]), - 'date': fields.datetime('Creation Date', required=True), - 'stock_available': fields.function(_get_stock, fnct_search=_stock_search, type="float", string="Available", select=True, - help="Current quantity of products with this Serial Number available in company warehouses", - digits_compute=dp.get_precision('Product Unit of Measure')), - 'revisions': fields.one2many('stock.production.lot.revision', 'lot_id', 'Revisions'), - 'company_id': fields.many2one('res.company', 'Company', select=True), - 'move_ids': fields.one2many('stock.move', 'prodlot_id', 'Moves for this serial number', readonly=True), + 'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants', readonly=True), + 'create_date': fields.datetime('Creation Date'), } _defaults = { - 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'), 'product_id': lambda x, y, z, c: c.get('product_id', False), } _sql_constraints = [ - ('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'), + ('name_ref_uniq', 'unique (name, ref, product_id, company_id)', 'The combination of Serial Number, internal reference, Product and Company must be unique !'), ] + def action_traceability(self, cr, uid, ids, context=None): - """ It traces the information of a product + """ It traces the information of lots @param self: The object pointer. @param cr: A database cursor @param uid: ID of the user currently logged in diff --cc addons/survey/controllers/main.py index 638b2ae,4287bbc..e21b0bb --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@@ -295,10 -295,14 +295,14 @@@ class WebsiteSurvey(http.Controller) 'quizz_correction': True if survey.quizz_mode and token else False}) @http.route(['/survey/results/'], - type='http', auth='user', multilang=True, website=True) + type='http', auth='user', website=True) def survey_reporting(self, survey, token=None, **post): '''Display survey Results & Statistics for given survey.''' - result_template, current_filters, filter_display_data, filter_finish = 'survey.result', [], [], False + result_template ='survey.result' + current_filters = [] + filter_display_data = [] + filter_finish = False + survey_obj = request.registry['survey.survey'] if not survey.user_input_ids or not [input_id.id for input_id in survey.user_input_ids if input_id.state != 'new']: result_template = 'survey.no_result' diff --cc addons/web/static/src/js/chrome.js index 3ffa5cf,b0700be..cd22f10 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@@ -183,8 -183,6 +183,7 @@@ instance.web.Dialog = instance.web.Widg */ close: function(reason) { if (this.dialog_inited && !this.__tmp_dialog_hiding) { + $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared - this.trigger("closing", reason); if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal this.__tmp_dialog_hiding = true; this.$dialog_box.modal('hide'); diff --cc addons/website/controllers/main.py index 9c6749a,092da52..7149a31 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@@ -196,17 -191,30 +198,20 @@@ class Website(openerp.addons.web.contro }) if view.model_data_id.module not in modules_to_update: modules_to_update.append(view.model_data_id.module) - module_obj = request.registry['ir.module.module'] - module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context) - module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context) + + if modules_to_update: + module_obj = request.registry['ir.module.module'] + module_ids = module_obj.search(request.cr, request.uid, [('name', 'in', modules_to_update)], context=request.context) + if module_ids: + module_obj.button_immediate_upgrade(request.cr, request.uid, module_ids, context=request.context) return request.redirect(redirect) - @http.route('/website/customize_template_toggle', type='json', auth='user', website=True) - def customize_template_set(self, view_id): - view_obj = request.registry.get("ir.ui.view") - view = view_obj.browse(request.cr, request.uid, int(view_id), - context=request.context) - if view.inherit_id: - value = False - else: - value = view.inherit_option_id and view.inherit_option_id.id or False - view_obj.write(request.cr, request.uid, [view_id], { - 'inherit_id': value - }, context=request.context) - return True - @http.route('/website/customize_template_get', type='json', auth='user', website=True) - def customize_template_get(self, xml_id, optional=True): + def customize_template_get(self, xml_id, full=False): + """ Lists the templates customizing ``xml_id``. By default, only + returns optional templates (which can be toggled on and off), if + ``full=True`` returns all templates customizing ``xml_id`` + """ imd = request.registry['ir.model.data'] view_model, view_theme_id = imd.get_object_reference( request.cr, request.uid, 'website', 'theme') diff --cc addons/website/static/src/js/website.editor.js index c3c016b,a05c226..cba6edf --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@@ -551,8 -545,14 +551,14 @@@ observer.disconnect(); var editor = this.rte.editor; - var root = editor.element.$; + var root = editor.element && editor.element.$; - editor.destroy(); + try { + editor.destroy(); + } + catch(err) { + // Hack to avoid the lost of all changes because ckeditor fails in destroy + console.log("Error in editor.destroy() : " + err.toString() + "\n " + err.stack); + } // FIXME: select editables then filter by dirty? var defs = this.rte.fetch_editables(root) .filter('.oe_dirty') diff --cc addons/website_crm/controllers/main.py index 5b615c9,2d17b4f..ca67358 --- a/addons/website_crm/controllers/main.py +++ b/addons/website_crm/controllers/main.py @@@ -83,7 -92,7 +92,7 @@@ class contactus(http.Controller) post_description.append("%s: %s" % ("REFERER", environ.get("HTTP_REFERER"))) values['description'] += dict_to_str(_("Environ Fields: "), post_description) - lead_id = self.create_lead(request, dict(values, user_id=False)) - lead_id = self.create_lead(request, values, kwargs) ++ lead_id = self.create_lead(request, dict(values, user_id=False), kwargs) if lead_id: for field_value in post_file: attachment_value = {