Example #1
0
    def _compute_value(self):
        """ For standard and AVCO valuation, compute the current accounting
        valuation of the quants by multiplying the quantity by
        the standard price. Instead for FIFO, use the quantity times the
        average cost (valuation layers are not manage by location so the
        average cost is the same for all location and the valuation field is
        a estimation more than a real value).
        """
        self.currency_id = self.env.company.currency_id
        for quant in self:
            # If the user didn't enter a location yet while enconding a quant.
            if not quant.location_id:
                quant.value = 0
                return

            if not quant.location_id._should_be_valued() or\
                    (quant.owner_id and quant.owner_id != quant.company_id.partner_id):
                quant.value = 0
                continue
            if quant.product_id.cost_method == 'fifo':
                quantity = quant.product_id.quantity_svl
                if float_is_zero(
                        quantity,
                        precision_rounding=quant.product_id.uom_id.rounding):
                    quant.value = 0.0
                    continue
                average_cost = quant.product_id.value_svl / quantity
                quant.value = quant.quantity * average_cost
            else:
                quant.value = quant.quantity * quant.product_id.standard_price
Example #2
0
 def _compute_quantities(self):
     """ When the product is a kit, this override computes the fields :
      - 'virtual_available'
      - 'qty_available'
      - 'incoming_qty'
      - 'outgoing_qty'
      - 'free_qty'
     """
     self.virtual_available = 0
     self.qty_available = 0
     self.incoming_qty = 0
     self.outgoing_qty = 0
     self.free_qty = 0
     bom_kits = {
         product: bom
         for product in self
         for bom in (self.env['mrp.bom']._bom_find(product=product,
                                                   bom_type='phantom'), )
         if bom
     }
     kits = self.filtered(lambda p: bom_kits.get(p))
     super(
         ProductProduct,
         self.filtered(lambda p: p not in bom_kits))._compute_quantities()
     for product in bom_kits:
         boms, bom_sub_lines = bom_kits[product].explode(product, 1)
         ratios_virtual_available = []
         ratios_qty_available = []
         ratios_incoming_qty = []
         ratios_outgoing_qty = []
         ratios_free_qty = []
         for bom_line, bom_line_data in bom_sub_lines:
             component = bom_line.product_id
             if component.type != 'product' or float_is_zero(
                     bom_line_data['qty'],
                     precision_rounding=bom_line.product_uom_id.rounding):
                 # As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those
                 # to avoid a division by zero. The same logic is applied to non-storable products as those
                 # products have 0 qty available.
                 continue
             uom_qty_per_kit = bom_line_data['qty'] / bom_line_data[
                 'original_qty']
             qty_per_kit = bom_line.product_uom_id._compute_quantity(
                 uom_qty_per_kit, bom_line.product_id.uom_id)
             ratios_virtual_available.append(component.virtual_available /
                                             qty_per_kit)
             ratios_qty_available.append(component.qty_available /
                                         qty_per_kit)
             ratios_incoming_qty.append(component.incoming_qty /
                                        qty_per_kit)
             ratios_outgoing_qty.append(component.outgoing_qty /
                                        qty_per_kit)
             ratios_free_qty.append(component.free_qty / qty_per_kit)
         if bom_sub_lines and ratios_virtual_available:  # Guard against all cnsumable bom: at least one ratio should be present.
             product.virtual_available = min(ratios_virtual_available) // 1
             product.qty_available = min(ratios_qty_available) // 1
             product.incoming_qty = min(ratios_incoming_qty) // 1
             product.outgoing_qty = min(ratios_outgoing_qty) // 1
             product.free_qty = min(ratios_free_qty) // 1
Example #3
0
 def check_reserved_done_quantity(self):
     for move_line in self:
         if move_line.state == 'done' and not float_is_zero(
                 move_line.product_uom_qty,
                 precision_digits=self.env['decimal.precision'].
                 precision_get('Product Unit of Measure')):
             raise ValidationError(
                 _('A done move line should never have a reserved quantity.'
                   ))
Example #4
0
 def _update_finished_move(self):
     """ After producing, set the move line on the subcontract picking. """
     res = super(MrpProductProduce, self)._update_finished_move()
     if self.subcontract_move_id:
         self.env['stock.move.line'].create({
             'move_id':
             self.subcontract_move_id.id,
             'picking_id':
             self.subcontract_move_id.picking_id.id,
             'product_id':
             self.product_id.id,
             'location_id':
             self.subcontract_move_id.location_id.id,
             'location_dest_id':
             self.subcontract_move_id.location_dest_id.id,
             'product_uom_qty':
             0,
             'product_uom_id':
             self.product_uom_id.id,
             'qty_done':
             self.qty_producing,
             'lot_id':
             self.finished_lot_id and self.finished_lot_id.id,
         })
         if not self._get_todo(self.production_id):
             ml_reserved = self.subcontract_move_id.move_line_ids.filtered(
                 lambda ml: float_is_zero(ml.qty_done,
                                          precision_rounding=ml.
                                          product_uom_id.rounding) and
                 not float_is_zero(ml.product_uom_qty,
                                   precision_rounding=ml.product_uom_id.
                                   rounding))
             ml_reserved.unlink()
             for ml in self.subcontract_move_id.move_line_ids:
                 ml.product_uom_qty = ml.qty_done
             self.subcontract_move_id._recompute_state()
     return res
Example #5
0
    def _auto_balance_opening_move(self):
        """ Checks the opening_move of this company. If it has not been posted yet
        and is unbalanced, balances it with a automatic account.move.line in the
        current year earnings account.
        """
        if self.account_opening_move_id and self.account_opening_move_id.state == 'draft':
            balancing_account = self.get_unaffected_earnings_account()
            currency = self.currency_id

            balancing_move_line = self.account_opening_move_id.line_ids.filtered(
                lambda x: x.account_id == balancing_account)
            # There could be multiple lines if we imported the balance from unaffected earnings account too
            if len(balancing_move_line) > 1:
                self.with_context(
                    check_move_validity=False
                ).account_opening_move_id.line_ids -= balancing_move_line[1:]
                balancing_move_line = balancing_move_line[0]

            debit_diff, credit_diff = self.get_opening_move_differences(
                self.account_opening_move_id.line_ids)

            if float_is_zero(debit_diff + credit_diff,
                             precision_rounding=currency.rounding):
                if balancing_move_line:
                    # zero difference and existing line : delete the line
                    self.account_opening_move_id.line_ids -= balancing_move_line
            else:
                if balancing_move_line:
                    # Non-zero difference and existing line : edit the line
                    balancing_move_line.write({
                        'debit': credit_diff,
                        'credit': debit_diff
                    })
                else:
                    # Non-zero difference and no existing line : create a new line
                    self.env['account.move.line'].create({
                        'name':
                        _('Automatic Balancing Line'),
                        'move_id':
                        self.account_opening_move_id.id,
                        'account_id':
                        balancing_account.id,
                        'debit':
                        credit_diff,
                        'credit':
                        debit_diff,
                    })
Example #6
0
 def unlink(self):
     precision = self.env['decimal.precision'].precision_get(
         'Product Unit of Measure')
     for ml in self:
         if ml.state in ('done', 'cancel'):
             raise UserError(
                 _('You can not delete product moves if the picking is done. You can only correct the done quantities.'
                   ))
         # Unlinking a move line should unreserve.
         if ml.product_id.type == 'product' and not ml._should_bypass_reservation(
                 ml.location_id) and not float_is_zero(
                     ml.product_qty, precision_digits=precision):
             try:
                 self.env['stock.quant']._update_reserved_quantity(
                     ml.product_id,
                     ml.location_id,
                     -ml.product_qty,
                     lot_id=ml.lot_id,
                     package_id=ml.package_id,
                     owner_id=ml.owner_id,
                     strict=True)
             except UserError:
                 if ml.lot_id:
                     self.env['stock.quant']._update_reserved_quantity(
                         ml.product_id,
                         ml.location_id,
                         -ml.product_qty,
                         lot_id=False,
                         package_id=ml.package_id,
                         owner_id=ml.owner_id,
                         strict=True)
                 else:
                     raise
     moves = self.mapped('move_id')
     res = super(StockMoveLine, self).unlink()
     if moves:
         moves._recompute_state()
     return res
Example #7
0
    def _free_reservation(self,
                          product_id,
                          location_id,
                          quantity,
                          lot_id=None,
                          package_id=None,
                          owner_id=None,
                          ml_to_ignore=None):
        """ When editing a done move line or validating one with some forced quantities, it is
        possible to impact quants that were not reserved. It is therefore necessary to edit or
        unlink the move lines that reserved a quantity now unavailable.

        :param ml_to_ignore: recordset of `stock.move.line` that should NOT be unreserved
        """
        self.ensure_one()

        if ml_to_ignore is None:
            ml_to_ignore = self.env['stock.move.line']
        ml_to_ignore |= self

        # Check the available quantity, with the `strict` kw set to `True`. If the available
        # quantity is greather than the quantity now unavailable, there is nothing to do.
        available_quantity = self.env['stock.quant']._get_available_quantity(
            product_id,
            location_id,
            lot_id=lot_id,
            package_id=package_id,
            owner_id=owner_id,
            strict=True)
        if quantity > available_quantity:
            # We now have to find the move lines that reserved our now unavailable quantity. We
            # take care to exclude ourselves and the move lines were work had already been done.
            outdated_move_lines_domain = [
                ('state', 'not in', ['done', 'cancel']),
                ('product_id', '=', product_id.id),
                ('lot_id', '=', lot_id.id if lot_id else False),
                ('location_id', '=', location_id.id),
                ('owner_id', '=', owner_id.id if owner_id else False),
                ('package_id', '=', package_id.id if package_id else False),
                ('product_qty', '>', 0.0),
                ('id', 'not in', ml_to_ignore.ids),
            ]
            # We take the current picking first, then the pickings with the latest scheduled date
            current_picking_first = lambda cand: (
                cand.picking_id != self.move_id.picking_id,
                -(cand.picking_id.scheduled_date or cand.move_id.date_expected)
                .timestamp() if cand.picking_id or cand.move_id else -cand.id,
            )
            outdated_candidates = self.env['stock.move.line'].search(
                outdated_move_lines_domain).sorted(current_picking_first)

            # As the move's state is not computed over the move lines, we'll have to manually
            # recompute the moves which we adapted their lines.
            move_to_recompute_state = self.env['stock.move']

            rounding = self.product_uom_id.rounding
            for candidate in outdated_candidates:
                if float_compare(candidate.product_qty,
                                 quantity,
                                 precision_rounding=rounding) <= 0:
                    quantity -= candidate.product_qty
                    move_to_recompute_state |= candidate.move_id
                    if candidate.qty_done:
                        candidate.product_uom_qty = 0.0
                    else:
                        candidate.unlink()
                    if float_is_zero(quantity, precision_rounding=rounding):
                        break
                else:
                    # split this move line and assign the new part to our extra move
                    quantity_split = float_round(
                        candidate.product_qty - quantity,
                        precision_rounding=self.product_uom_id.rounding,
                        rounding_method='UP')
                    candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(
                        quantity_split,
                        candidate.product_uom_id,
                        rounding_method='HALF-UP')
                    move_to_recompute_state |= candidate.move_id
                    break
            move_to_recompute_state._recompute_state()
Example #8
0
    def write(self, vals):
        if self.env.context.get('bypass_reservation_update'):
            return super(StockMoveLine, self).write(vals)

        if 'product_id' in vals and any(
                vals.get('state', ml.state) != 'draft'
                and vals['product_id'] != ml.product_id.id for ml in self):
            raise UserError(
                _("Changing the product is only allowed in 'Draft' state."))

        moves_to_recompute_state = self.env['stock.move']
        Quant = self.env['stock.quant']
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        triggers = [('location_id', 'stock.location'),
                    ('location_dest_id', 'stock.location'),
                    ('lot_id', 'stock.production.lot'),
                    ('package_id', 'stock.quant.package'),
                    ('owner_id', 'res.partner')]
        updates = {}
        for key, model in triggers:
            if key in vals:
                updates[key] = self.env[model].browse(vals[key])

        # When we try to write on a reserved move line any fields from `triggers` or directly
        # `product_uom_qty` (the actual reserved quantity), we need to make sure the associated
        # quants are correctly updated in order to not make them out of sync (i.e. the sum of the
        # move lines `product_uom_qty` should always be equal to the sum of `reserved_quantity` on
        # the quants). If the new charateristics are not available on the quants, we chose to
        # reserve the maximum possible.
        if updates or 'product_uom_qty' in vals:
            for ml in self.filtered(
                    lambda ml: ml.state in ['partially_available', 'assigned']
                    and ml.product_id.type == 'product'):

                if 'product_uom_qty' in vals:
                    new_product_uom_qty = ml.product_uom_id._compute_quantity(
                        vals['product_uom_qty'],
                        ml.product_id.uom_id,
                        rounding_method='HALF-UP')
                    # Make sure `product_uom_qty` is not negative.
                    if float_compare(new_product_uom_qty,
                                     0,
                                     precision_rounding=ml.product_id.uom_id.
                                     rounding) < 0:
                        raise UserError(
                            _('Reserving a negative quantity is not allowed.'))
                else:
                    new_product_uom_qty = ml.product_qty

                # Unreserve the old charateristics of the move line.
                if not ml._should_bypass_reservation(ml.location_id):
                    try:
                        Quant._update_reserved_quantity(
                            ml.product_id,
                            ml.location_id,
                            -ml.product_qty,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id,
                            strict=True)
                    except UserError:
                        # If we were not able to unreserve on tracked quants, we can use untracked ones.
                        if ml.lot_id:
                            Quant._update_reserved_quantity(
                                ml.product_id,
                                ml.location_id,
                                -ml.product_qty,
                                lot_id=False,
                                package_id=ml.package_id,
                                owner_id=ml.owner_id,
                                strict=True)
                        else:
                            raise

                # Reserve the maximum available of the new charateristics of the move line.
                if not ml._should_bypass_reservation(
                        updates.get('location_id', ml.location_id)):
                    reserved_qty = 0
                    try:
                        q = Quant._update_reserved_quantity(
                            ml.product_id,
                            updates.get('location_id', ml.location_id),
                            new_product_uom_qty,
                            lot_id=updates.get('lot_id', ml.lot_id),
                            package_id=updates.get('package_id',
                                                   ml.package_id),
                            owner_id=updates.get('owner_id', ml.owner_id),
                            strict=True)
                        reserved_qty = sum([x[1] for x in q])
                    except UserError:
                        if updates.get('lot_id'):
                            # If we were not able to reserve on tracked quants, we can use untracked ones.
                            try:
                                q = Quant._update_reserved_quantity(
                                    ml.product_id,
                                    updates.get('location_id', ml.location_id),
                                    new_product_uom_qty,
                                    lot_id=False,
                                    package_id=updates.get(
                                        'package_id', ml.package_id),
                                    owner_id=updates.get(
                                        'owner_id', ml.owner_id),
                                    strict=True)
                                reserved_qty = sum([x[1] for x in q])
                            except UserError:
                                pass
                    if reserved_qty != new_product_uom_qty:
                        new_product_uom_qty = ml.product_id.uom_id._compute_quantity(
                            reserved_qty,
                            ml.product_uom_id,
                            rounding_method='HALF-UP')
                        moves_to_recompute_state |= ml.move_id
                        ml.with_context(bypass_reservation_update=True
                                        ).product_uom_qty = new_product_uom_qty

        # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
        next_moves = self.env['stock.move']
        if updates or 'qty_done' in vals:
            mls = self.filtered(lambda ml: ml.move_id.state == 'done' and ml.
                                product_id.type == 'product')
            if not updates:  # we can skip those where qty_done is already good up to UoM rounding
                mls = mls.filtered(lambda ml: not float_is_zero(
                    ml.qty_done - vals['qty_done'],
                    precision_rounding=ml.product_uom_id.rounding))
            for ml in mls:
                # undo the original move line
                qty_done_orig = ml.move_id.product_uom._compute_quantity(
                    ml.qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                in_date = Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_dest_id,
                    -qty_done_orig,
                    lot_id=ml.lot_id,
                    package_id=ml.result_package_id,
                    owner_id=ml.owner_id)[1]
                Quant._update_available_quantity(ml.product_id,
                                                 ml.location_id,
                                                 qty_done_orig,
                                                 lot_id=ml.lot_id,
                                                 package_id=ml.package_id,
                                                 owner_id=ml.owner_id,
                                                 in_date=in_date)

                # move what's been actually done
                product_id = ml.product_id
                location_id = updates.get('location_id', ml.location_id)
                location_dest_id = updates.get('location_dest_id',
                                               ml.location_dest_id)
                qty_done = vals.get('qty_done', ml.qty_done)
                lot_id = updates.get('lot_id', ml.lot_id)
                package_id = updates.get('package_id', ml.package_id)
                result_package_id = updates.get('result_package_id',
                                                ml.result_package_id)
                owner_id = updates.get('owner_id', ml.owner_id)
                quantity = ml.move_id.product_uom._compute_quantity(
                    qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                if not ml._should_bypass_reservation(location_id):
                    ml._free_reservation(product_id,
                                         location_id,
                                         quantity,
                                         lot_id=lot_id,
                                         package_id=package_id,
                                         owner_id=owner_id)
                if not float_is_zero(quantity, precision_digits=precision):
                    available_qty, in_date = Quant._update_available_quantity(
                        product_id,
                        location_id,
                        -quantity,
                        lot_id=lot_id,
                        package_id=package_id,
                        owner_id=owner_id)
                    if available_qty < 0 and lot_id:
                        # see if we can compensate the negative quants with some untracked quants
                        untracked_qty = Quant._get_available_quantity(
                            product_id,
                            location_id,
                            lot_id=False,
                            package_id=package_id,
                            owner_id=owner_id,
                            strict=True)
                        if untracked_qty:
                            taken_from_untracked_qty = min(
                                untracked_qty, abs(available_qty))
                            Quant._update_available_quantity(
                                product_id,
                                location_id,
                                -taken_from_untracked_qty,
                                lot_id=False,
                                package_id=package_id,
                                owner_id=owner_id)
                            Quant._update_available_quantity(
                                product_id,
                                location_id,
                                taken_from_untracked_qty,
                                lot_id=lot_id,
                                package_id=package_id,
                                owner_id=owner_id)
                            if not ml._should_bypass_reservation(location_id):
                                ml._free_reservation(ml.product_id,
                                                     location_id,
                                                     untracked_qty,
                                                     lot_id=False,
                                                     package_id=package_id,
                                                     owner_id=owner_id)
                    Quant._update_available_quantity(
                        product_id,
                        location_dest_id,
                        quantity,
                        lot_id=lot_id,
                        package_id=result_package_id,
                        owner_id=owner_id,
                        in_date=in_date)

                # Unreserve and reserve following move in order to have the real reserved quantity on move_line.
                next_moves |= ml.move_id.move_dest_ids.filtered(
                    lambda move: move.state not in ('done', 'cancel'))

                # Log a note
                if ml.picking_id:
                    ml._log_message(ml.picking_id, ml,
                                    'stock.track_move_template', vals)

        res = super(StockMoveLine, self).write(vals)

        # Update scrap object linked to move_lines to the new quantity.
        if 'qty_done' in vals:
            for move in self.mapped('move_id'):
                if move.scrapped:
                    move.scrap_ids.write({'scrap_qty': move.quantity_done})

        # As stock_account values according to a move's `product_uom_qty`, we consider that any
        # done stock move should have its `quantity_done` equals to its `product_uom_qty`, and
        # this is what move's `action_done` will do. So, we replicate the behavior here.
        if updates or 'qty_done' in vals:
            moves = self.filtered(
                lambda ml: ml.move_id.state == 'done').mapped('move_id')
            moves |= self.filtered(
                lambda ml: ml.move_id.state not in
                ('done', 'cancel') and ml.move_id.picking_id.immediate_transfer
                and not ml.product_uom_qty).mapped('move_id')
            for move in moves:
                move.product_uom_qty = move.quantity_done
        next_moves._do_unreserve()
        next_moves._action_assign()

        if moves_to_recompute_state:
            moves_to_recompute_state._recompute_state()

        return res
Example #9
0
    def button_validate(self):
        if any(cost.state != 'draft' for cost in self):
            raise UserError(_('Only draft landed costs can be validated'))
        if not all(cost.picking_ids for cost in self):
            raise UserError(
                _('Please define the transfers on which those additional costs should apply.'
                  ))
        cost_without_adjusment_lines = self.filtered(
            lambda c: not c.valuation_adjustment_lines)
        if cost_without_adjusment_lines:
            cost_without_adjusment_lines.compute_landed_cost()
        if not self._check_sum():
            raise UserError(
                _('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'
                  ))

        for cost in self:
            move = self.env['account.move']
            move_vals = {
                'journal_id': cost.account_journal_id.id,
                'date': cost.date,
                'ref': cost.name,
                'line_ids': [],
                'type': 'entry',
            }
            valuation_layer_ids = []
            for line in cost.valuation_adjustment_lines.filtered(
                    lambda line: line.move_id):
                remaining_qty = sum(
                    line.move_id.stock_valuation_layer_ids.mapped(
                        'remaining_qty'))
                linked_layer = line.move_id.stock_valuation_layer_ids[:1]

                # Prorate the value at what's still in stock
                cost_to_add = (remaining_qty / line.move_id.product_qty
                               ) * line.additional_landed_cost
                if not cost.company_id.currency_id.is_zero(cost_to_add):
                    valuation_layer = self.env['stock.valuation.layer'].create(
                        {
                            'value': cost_to_add,
                            'unit_cost': 0,
                            'quantity': 0,
                            'remaining_qty': 0,
                            'stock_valuation_layer_id': linked_layer.id,
                            'description': cost.name,
                            'stock_move_id': line.move_id.id,
                            'product_id': line.move_id.product_id.id,
                            'stock_landed_cost_id': cost.id,
                            'company_id': cost.company_id.id,
                        })
                    linked_layer.remaining_value += cost_to_add
                    valuation_layer_ids.append(valuation_layer.id)
                # Update the AVCO
                product = line.move_id.product_id
                if product.cost_method == 'average' and not float_is_zero(
                        product.quantity_svl,
                        precision_rounding=product.uom_id.rounding):
                    product.with_context(
                        force_company=self.company_id.id).sudo(
                        ).standard_price += cost_to_add / product.quantity_svl
                # `remaining_qty` is negative if the move is out and delivered proudcts that were not
                # in stock.
                qty_out = 0
                if line.move_id._is_in():
                    qty_out = line.move_id.product_qty - remaining_qty
                elif line.move_id._is_out():
                    qty_out = line.move_id.product_qty
                move_vals['line_ids'] += line._create_accounting_entries(
                    move, qty_out)

            move_vals['stock_valuation_layer_ids'] = [(6, None,
                                                       valuation_layer_ids)]
            move = move.create(move_vals)
            cost.write({'state': 'done', 'account_move_id': move.id})
            move.post()

            if cost.vendor_bill_id and cost.vendor_bill_id.state == 'posted' and cost.company_id.anglo_saxon_accounting:
                all_amls = cost.vendor_bill_id.line_ids | cost.account_move_id.line_ids
                for product in cost.cost_lines.product_id:
                    accounts = product.product_tmpl_id.get_product_accounts()
                    input_account = accounts['stock_input']
                    all_amls.filtered(
                        lambda aml: aml.account_id == input_account and not aml
                        .full_reconcile_id).reconcile()
        return True
Example #10
0
    def _update_reserved_quantity(self,
                                  product_id,
                                  location_id,
                                  quantity,
                                  lot_id=None,
                                  package_id=None,
                                  owner_id=None,
                                  strict=False):
        """ Increase the reserved quantity, i.e. increase `reserved_quantity` for the set of quants
        sharing the combination of `product_id, location_id` if `strict` is set to False or sharing
        the *exact same characteristics* otherwise. Typically, this method is called when reserving
        a move or updating a reserved move line. When reserving a chained move, the strict flag
        should be enabled (to reserve exactly what was brought). When the move is MTS,it could take
        anything from the stock, so we disable the flag. When editing a move line, we naturally
        enable the flag, to reflect the reservation according to the edition.

        :return: a list of tuples (quant, quantity_reserved) showing on which quant the reservation
            was done and how much the system was able to reserve on it
        """
        self = self.sudo()
        rounding = product_id.uom_id.rounding
        quants = self._gather(product_id,
                              location_id,
                              lot_id=lot_id,
                              package_id=package_id,
                              owner_id=owner_id,
                              strict=strict)
        reserved_quants = []

        if float_compare(quantity, 0, precision_rounding=rounding) > 0:
            # if we want to reserve
            available_quantity = self._get_available_quantity(
                product_id,
                location_id,
                lot_id=lot_id,
                package_id=package_id,
                owner_id=owner_id,
                strict=strict)
            if float_compare(quantity,
                             available_quantity,
                             precision_rounding=rounding) > 0:
                raise UserError(
                    _('It is not possible to reserve more products of %s than you have in stock.'
                      ) % product_id.display_name)
        elif float_compare(quantity, 0, precision_rounding=rounding) < 0:
            # if we want to unreserve
            available_quantity = sum(quants.mapped('reserved_quantity'))
            if float_compare(abs(quantity),
                             available_quantity,
                             precision_rounding=rounding) > 0:
                raise UserError(
                    _('It is not possible to unreserve more products of %s than you have in stock.'
                      ) % product_id.display_name)
        else:
            return reserved_quants

        for quant in quants:
            if float_compare(quantity, 0, precision_rounding=rounding) > 0:
                max_quantity_on_quant = quant.quantity - quant.reserved_quantity
                if float_compare(max_quantity_on_quant,
                                 0,
                                 precision_rounding=rounding) <= 0:
                    continue
                max_quantity_on_quant = min(max_quantity_on_quant, quantity)
                quant.reserved_quantity += max_quantity_on_quant
                reserved_quants.append((quant, max_quantity_on_quant))
                quantity -= max_quantity_on_quant
                available_quantity -= max_quantity_on_quant
            else:
                max_quantity_on_quant = min(quant.reserved_quantity,
                                            abs(quantity))
                quant.reserved_quantity -= max_quantity_on_quant
                reserved_quants.append((quant, -max_quantity_on_quant))
                quantity += max_quantity_on_quant
                available_quantity += max_quantity_on_quant

            if float_is_zero(
                    quantity, precision_rounding=rounding) or float_is_zero(
                        available_quantity, precision_rounding=rounding):
                break
        return reserved_quants