示例#1
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
示例#2
0
文件: company.py 项目: ovnicraft/odoo
    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':
            debit_diff, credit_diff = self.get_opening_move_differences(self.account_opening_move_id.line_ids)

            currency = self.currency_id
            balancing_move_line = self.account_opening_move_id.line_ids.filtered(lambda x: x.account_id == self.get_unaffected_earnings_account())

            if float_is_zero(debit_diff + credit_diff, precision_rounding=currency.rounding):
                if balancing_move_line:
                    # zero difference and existing line : delete the line
                    balancing_move_line.unlink()
            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
                    balancing_account = self.get_unaffected_earnings_account()
                    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,
                    })
示例#3
0
    def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None):
        """ Increase or decrease `reserved_quantity` of a set of quants for a given set of
        product_id/location_id/lot_id/package_id/owner_id.

        :param product_id:
        :param location_id:
        :param quantity:
        :param lot_id:
        :param package_id:
        :param owner_id:
        :param datetime in_date: Should only be passed when calls to this method are done in
                                 order to move a quant. When creating a tracked quant, the
                                 current datetime will be used.
        :return: tuple (available_quantity, in_date as a datetime)
        """
        self = self.sudo()
        quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
        rounding = product_id.uom_id.rounding

        if lot_id:
            incoming_dates = quants.mapped('in_date')  # `mapped` already filtered out falsy items
            incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
            if in_date:
                incoming_dates += [in_date]
            # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
            # consider only the oldest one as being relevant.
            if incoming_dates:
                in_date = fields.Datetime.to_string(min(incoming_dates))
            else:
                in_date = fields.Datetime.now()

        for quant in quants:
            try:
                with self._cr.savepoint():
                    self._cr.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT", [quant.id], log_exceptions=False)
                    quant.write({
                        'quantity': quant.quantity + quantity,
                        'in_date': in_date,
                    })
                    # cleanup empty quants
                    if float_is_zero(quant.quantity, precision_rounding=rounding) and float_is_zero(quant.reserved_quantity, precision_rounding=rounding):
                        quant.unlink()
                    break
            except OperationalError as e:
                if e.pgcode == '55P03':  # could not obtain the lock
                    continue
                else:
                    raise
        else:
            self.create({
                'product_id': product_id.id,
                'location_id': location_id.id,
                'quantity': quantity,
                'lot_id': lot_id and lot_id.id,
                'package_id': package_id and package_id.id,
                'owner_id': owner_id and owner_id.id,
                'in_date': in_date,
            })
        return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False, allow_negative=True), fields.Datetime.from_string(in_date)
示例#4
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.
            oudated_move_lines_domain = [
                ('move_id.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),
            ]
            oudated_candidates = self.env['stock.move.line'].search(oudated_move_lines_domain)

            # 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 oudated_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()
示例#5
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.'))
示例#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.location_id.should_bypass_reservation() 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
示例#7
0
    def action_create_invoice(self):
        """Create the invoice associated to the PO.
        """
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')

        # 1) Prepare invoice vals and clean-up the section lines
        invoice_vals_list = []
        for order in self:
            if order.invoice_status != 'to invoice':
                continue

            pending_section = None
            # Invoice values.
            invoice_vals = order._prepare_invoice()
            # Invoice line values (keep only necessary sections).
            for line in order.order_line:
                if line.display_type == 'line_section':
                    pending_section = line
                    continue
                if not float_is_zero(line.qty_to_invoice, precision_digits=precision):
                    if pending_section:
                        invoice_vals['invoice_line_ids'].append((0, 0, pending_section._prepare_account_move_line()))
                        pending_section = None
                    invoice_vals['invoice_line_ids'].append((0, 0, line._prepare_account_move_line()))
            invoice_vals_list.append(invoice_vals)

        if not invoice_vals_list:
            raise UserError(_('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.'))

        # 2) group by (company_id, partner_id, currency_id) for batch creation
        new_invoice_vals_list = []
        for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: (x.get('company_id'), x.get('partner_id'), x.get('currency_id'))):
            origins = set()
            payment_refs = set()
            refs = set()
            ref_invoice_vals = None
            for invoice_vals in invoices:
                if not ref_invoice_vals:
                    ref_invoice_vals = invoice_vals
                else:
                    ref_invoice_vals['invoice_line_ids'] += invoice_vals['invoice_line_ids']
                origins.add(invoice_vals['invoice_origin'])
                payment_refs.add(invoice_vals['invoice_payment_ref'])
                refs.add(invoice_vals['ref'])
            ref_invoice_vals.update({
                'ref': ', '.join(refs)[:2000],
                'invoice_origin': ', '.join(origins),
                'invoice_payment_ref': len(payment_refs) == 1 and payment_refs.pop() or False,
            })
            new_invoice_vals_list.append(ref_invoice_vals)
        invoice_vals_list = new_invoice_vals_list

        # 3) Create invoices.
        moves = self.env['account.move']
        AccountMove = self.env['account.move'].with_context(default_move_type='in_invoice')
        for vals in invoice_vals_list:
            moves |= AccountMove.with_company(vals['company_id']).create(vals)

        # 4) Some moves might actually be refunds: convert them if the total amount is negative
        # We do this after the moves have been created since we need taxes, etc. to know if the total
        # is actually negative or not
        moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note()

        return self.action_view_invoice(moves)
示例#8
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.location_id.should_bypass_reservation() 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
示例#9
0
    def _stock_account_prepare_anglo_saxon_in_lines_vals(self):
        ''' Prepare values used to create the journal items (account.move.line) corresponding to the price difference
         lines for vendor bills.

        Example:

        Buy a product having a cost of 9 and a supplier price of 10 and being a storable product and having a perpetual
        valuation in FIFO. The vendor bill's journal entries looks like:

        Account                                     | Debit | Credit
        ---------------------------------------------------------------
        101120 Stock Interim Account (Received)     | 10.0  |
        ---------------------------------------------------------------
        101100 Account Payable                      |       | 10.0
        ---------------------------------------------------------------

        This method computes values used to make two additional journal items:

        ---------------------------------------------------------------
        101120 Stock Interim Account (Received)     |       | 1.0
        ---------------------------------------------------------------
        xxxxxx Price Difference Account             | 1.0   |
        ---------------------------------------------------------------

        :return: A list of Python dictionary to be passed to env['account.move.line'].create.
        '''
        lines_vals_list = []

        for move in self:
            if move.type not in ('in_invoice', 'in_refund', 'in_receipt') or not move.company_id.anglo_saxon_accounting:
                continue

            for line in move.invoice_line_ids.filtered(lambda line: line.product_id.type == 'product' and line.product_id.valuation == 'real_time'):

                # Filter out lines being not eligible for price difference.
                if line.product_id.type != 'product' or line.product_id.valuation != 'real_time':
                    continue

                # Retrieve accounts needed to generate the price difference.
                debit_pdiff_account = line.product_id.property_account_creditor_price_difference \
                                or line.product_id.categ_id.property_account_creditor_price_difference_categ
                debit_pdiff_account = move.fiscal_position_id.map_account(debit_pdiff_account)

                if line.product_id.cost_method != 'standard' and line.purchase_line_id:
                    po_currency = line.purchase_line_id.currency_id
                    po_company = line.purchase_line_id.company_id

                    # Retrieve stock valuation moves.
                    valuation_stock_moves = self.env['stock.move'].search([
                        ('purchase_line_id', '=', line.purchase_line_id.id),
                        ('state', '=', 'done'),
                        ('product_qty', '!=', 0.0),
                    ])
                    if move.type == 'in_refund':
                        valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_out())
                    else:
                        valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_in())

                    if valuation_stock_moves:
                        valuation_price_unit_total = 0
                        valuation_total_qty = 0
                        for val_stock_move in valuation_stock_moves:
                            # In case val_stock_move is a return move, its valuation entries have been made with the
                            # currency rate corresponding to the original stock move
                            valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date
                            svl = val_stock_move.mapped('stock_valuation_layer_ids').filtered(lambda l: l.quantity)
                            layers_qty = sum(svl.mapped('quantity'))
                            layers_values = sum(svl.mapped('value'))
                            valuation_price_unit_total += line.company_currency_id._convert(
                                layers_values, move.currency_id,
                                move.company_id, valuation_date, round=False,
                            )
                            valuation_total_qty += layers_qty
                        if float_is_zero(valuation_total_qty, precision_rounding=line.product_uom_id.rounding or line.product_id.uom_id.rounding):
                            raise UserError(_('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.') % _(line.product_id.display_name))
                        valuation_price_unit = valuation_price_unit_total / valuation_total_qty
                        valuation_price_unit = line.product_id.uom_id._compute_price(valuation_price_unit, line.product_uom_id)

                    elif line.product_id.cost_method == 'fifo':
                        # In this condition, we have a real price-valuated product which has not yet been received
                        valuation_price_unit = po_currency._convert(
                            line.purchase_line_id.price_unit, move.currency_id,
                            po_company, move.date, round=False,
                        )
                    else:
                        # For average/fifo/lifo costing method, fetch real cost price from incoming moves.
                        price_unit = line.purchase_line_id.product_uom._compute_price(line.purchase_line_id.price_unit, line.product_uom_id)
                        valuation_price_unit = po_currency._convert(
                            price_unit, move.currency_id,
                            po_company, move.date, round=False
                        )

                else:
                    # Valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate
                    price_unit = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id)
                    valuation_price_unit = line.company_currency_id._convert(
                        price_unit, move.currency_id,
                        move.company_id, fields.Date.today(), round=False
                    )

                invoice_cur_prec = move.currency_id.decimal_places

                price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
                if line.tax_ids:
                    price_unit = line.tax_ids.compute_all(
                        price_unit, currency=move.currency_id, quantity=1.0, is_refund=move.type == 'in_refund')['total_excluded']

                if float_compare(valuation_price_unit, price_unit, precision_digits=invoice_cur_prec) != 0 \
                        and float_compare(line['price_unit'], line.price_unit, precision_digits=invoice_cur_prec) == 0:

                    price_unit_val_dif = price_unit - valuation_price_unit

                    if move.currency_id.compare_amounts(price_unit, valuation_price_unit) != 0 and debit_pdiff_account:
                        # Add price difference account line.
                        vals = {
                            'name': line.name[:64],
                            'move_id': move.id,
                            'currency_id': line.currency_id.id,
                            'product_id': line.product_id.id,
                            'product_uom_id': line.product_uom_id.id,
                            'quantity': line.quantity,
                            'price_unit': price_unit_val_dif,
                            'price_subtotal': line.quantity * price_unit_val_dif,
                            'account_id': debit_pdiff_account.id,
                            'analytic_account_id': line.analytic_account_id.id,
                            'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)],
                            'exclude_from_invoice_tab': True,
                            'is_anglo_saxon_line': True,
                        }
                        vals.update(line._get_fields_onchange_subtotal(price_subtotal=vals['price_subtotal']))
                        lines_vals_list.append(vals)

                        # Correct the amount of the current line.
                        vals = {
                            'name': line.name[:64],
                            'move_id': move.id,
                            'currency_id': line.currency_id.id,
                            'product_id': line.product_id.id,
                            'product_uom_id': line.product_uom_id.id,
                            'quantity': line.quantity,
                            'price_unit': -price_unit_val_dif,
                            'price_subtotal': line.quantity * -price_unit_val_dif,
                            'account_id': line.account_id.id,
                            'analytic_account_id': line.analytic_account_id.id,
                            'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)],
                            'exclude_from_invoice_tab': True,
                            'is_anglo_saxon_line': True,
                        }
                        vals.update(line._get_fields_onchange_subtotal(price_subtotal=vals['price_subtotal']))
                        lines_vals_list.append(vals)
        return lines_vals_list
示例#10
0
    def _action_assign(self):
        """ Reserve stock moves by creating their stock move lines. A stock move is
        considered reserved once the sum of `product_qty` for all its move lines is
        equal to its `product_qty`. If it is less, the stock move is considered
        partially available.
        """
        assigned_moves = self.env['stock.move']
        partially_available_moves = self.env['stock.move']
        for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
            missing_reserved_uom_quantity = move.product_uom_qty - move.reserved_availability
            missing_reserved_quantity = move.product_uom._compute_quantity(missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP')
            if move.location_id.should_bypass_reservation()\
                    or move.product_id.type == 'consu':
                # create the move line(s) but do not impact quants
                if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
                    for i in range(0, int(missing_reserved_quantity)):
                        self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
                else:
                    to_update = move.move_line_ids.filtered(lambda ml: ml.product_uom_id == move.product_uom and
                                                            ml.location_id == move.location_id and
                                                            ml.location_dest_id == move.location_dest_id and
                                                            ml.picking_id == move.picking_id and
                                                            not ml.lot_id and
                                                            not ml.package_id and
                                                            not ml.owner_id)
                    if to_update:
                        to_update[0].product_uom_qty += missing_reserved_uom_quantity
                    else:
                        # 'not' is removed at line 205 as 'not' is not working on odoo.sh so, for test purpose.
                        # if not move.picking_id.picking_type_id.show_reserved:
                        #     self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=missing_reserved_quantity))
                        pass
                assigned_moves |= move
            else:
                if not move.move_orig_ids:
                    if move.procure_method == 'make_to_order':
                        continue
                    # If we don't need any quantity, consider the move assigned.
                    need = missing_reserved_quantity
                    if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding):
                        assigned_moves |= move
                        continue
                    # Reserve new quants and create move lines accordingly.
                    forced_package_id = move.package_level_id.package_id or None
                    available_quantity = self.env['stock.quant']._get_available_quantity(move.product_id, move.location_id, package_id=forced_package_id)
                    if available_quantity <= 0:
                        continue
                    taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, package_id=forced_package_id, strict=False)
                    if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
                        continue
                    if need == taken_quantity:
                        assigned_moves |= move
                    else:
                        partially_available_moves |= move
                else:
                    # Check what our parents brought and what our siblings took in order to
                    # determine what we can distribute.
                    # `qty_done` is in `ml.product_uom_id` and, as we will later increase
                    # the reserved quantity on the quants, convert it here in
                    # `product_id.uom_id` (the UOM of the quants is the UOM of the product).
                    move_lines_in = move.move_orig_ids.filtered(lambda m: m.state == 'done').mapped('move_line_ids')
                    keys_in_groupby = ['location_dest_id', 'lot_id', 'result_package_id', 'owner_id']

                    def _keys_in_sorted(ml):
                        return (ml.location_dest_id.id, ml.lot_id.id, ml.result_package_id.id, ml.owner_id.id)

                    grouped_move_lines_in = {}
                    for k, g in groupby(sorted(move_lines_in, key=_keys_in_sorted), key=itemgetter(*keys_in_groupby)):
                        qty_done = 0
                        for ml in g:
                            qty_done += ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
                        grouped_move_lines_in[k] = qty_done
                    move_lines_out_done = (move.move_orig_ids.mapped('move_dest_ids') - move)\
                        .filtered(lambda m: m.state in ['done'])\
                        .mapped('move_line_ids')
                    # As we defer the write on the stock.move's state at the end of the loop, there
                    # could be moves to consider in what our siblings already took.
                    moves_out_siblings = move.move_orig_ids.mapped('move_dest_ids') - move
                    moves_out_siblings_to_consider = moves_out_siblings & (assigned_moves + partially_available_moves)
                    reserved_moves_out_siblings = moves_out_siblings.filtered(lambda m: m.state in ['partially_available', 'assigned'])
                    move_lines_out_reserved = (reserved_moves_out_siblings | moves_out_siblings_to_consider).mapped('move_line_ids')
                    keys_out_groupby = ['location_id', 'lot_id', 'package_id', 'owner_id']

                    def _keys_out_sorted(ml):
                        return (ml.location_id.id, ml.lot_id.id, ml.package_id.id, ml.owner_id.id)

                    grouped_move_lines_out = {}
                    for k, g in groupby(sorted(move_lines_out_done, key=_keys_out_sorted), key=itemgetter(*keys_out_groupby)):
                        qty_done = 0
                        for ml in g:
                            qty_done += ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
                        grouped_move_lines_out[k] = qty_done
                    for k, g in groupby(sorted(move_lines_out_reserved, key=_keys_out_sorted), key=itemgetter(*keys_out_groupby)):
                        grouped_move_lines_out[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty'))
                    available_move_lines = {key: grouped_move_lines_in[key] - grouped_move_lines_out.get(key, 0) for key in grouped_move_lines_in.keys()}
                    # pop key if the quantity available amount to 0
                    available_move_lines = dict((k, v) for k, v in available_move_lines.items() if v)

                    if not available_move_lines:
                        continue
                    for move_line in move.move_line_ids.filtered(lambda m: m.product_qty):
                        if available_move_lines.get((move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)):
                            available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.product_qty
                    for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items():
                        need = move.product_qty - sum(move.move_line_ids.mapped('product_qty'))
                        # `quantity` is what is brought by chained done move lines. We double check
                        # here this quantity is available on the quants themselves. If not, this
                        # could be the result of an inventory adjustment that removed totally of
                        # partially `quantity`. When this happens, we chose to reserve the maximum
                        # still available. This situation could not happen on MTS move, because in
                        # this case `quantity` is directly the quantity on the quants themselves.
                        available_quantity = self.env['stock.quant']._get_available_quantity(
                            move.product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
                        if float_is_zero(available_quantity, precision_rounding=move.product_id.uom_id.rounding):
                            continue
                        taken_quantity = move._update_reserved_quantity(need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id)
                        if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
                            continue
                        if need - taken_quantity == 0.0:
                            assigned_moves |= move
                            break
                        partially_available_moves |= move
        partially_available_moves.write({'state': 'partially_available'})
        assigned_moves.write({'state': 'assigned'})
        self.mapped('picking_id')._check_entire_pack()
示例#11
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.
            oudated_move_lines_domain = [
                ('move_id.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),
            ]
            oudated_candidates = self.env['stock.move.line'].search(
                oudated_move_lines_domain)

            # 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 oudated_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()
示例#12
0
    def validate_documents(self, key, xml, account_id):
        """ Validate the incoming or outcoming document before create or
        attach the xml to invoice
        :param key: Name of the document that is being validated
        :type key: str
        :param xml: xml file with the datas of purchase
        :type xml: etree
        :param account_id: The account by default that must be used in the
            lines of the invoice if this is created
        :type account_id: int
        :return: Result of the validation of the CFDI and the invoices created.
        :rtype: dict
        """
        wrongfiles = {}
        invoices = {}
        inv_obj = self.env['account.move']
        partner_obj = self.env['res.partner']
        currency_obj = self.env['res.currency']
        inv = inv_obj
        inv_id = False
        xml_str = etree.tostring(xml, pretty_print=True, encoding='UTF-8')
        xml_vat_emitter, xml_vat_receiver, xml_amount, xml_currency, version,\
            xml_name_supplier, xml_type_of_document, xml_uuid, xml_folio,\
            xml_taxes = self._get_xml_data(xml)
        xml_related_uuid = related_invoice = False
        exist_supplier = partner_obj.search(
            [('vat', '=', xml_vat_emitter)], limit=1)
        domain = [
            '|', ('partner_id', 'child_of', exist_supplier.id),
            ('partner_id', '=', exist_supplier.id)]
        invoice = xml_folio
        if xml_folio:
            domain.append(('ref', '=ilike', xml_folio))
        else:
            domain.append(('amount_total', '>=', xml_amount - 1))
            domain.append(('amount_total', '<=', xml_amount + 1))
            domain.append(('l10n_mx_edi_cfdi_name', '=', False))
            domain.append(('state', '!=', 'cancel'))
        invoice = inv_obj.search(domain, limit=1)
        exist_reference = invoice if invoice and xml_uuid != invoice.l10n_mx_edi_cfdi_uuid else False  # noqa
        if exist_reference and not exist_reference.l10n_mx_edi_cfdi_uuid:
            inv = exist_reference
            inv_id = inv.id
            exist_reference = False
            inv.l10n_mx_edi_update_sat_status()
        xml_status = inv.l10n_mx_edi_sat_status
        inv_vat_receiver = (
            self.env.user.company_id.vat or '').upper()
        inv_vat_emitter = (
            inv and inv.commercial_partner_id.vat or '').upper()
        inv_amount = inv.amount_total
        inv_folio = inv.ref or ''
        domain = [('l10n_mx_edi_cfdi_name', '!=', False)]
        if exist_supplier:
            domain += [('partner_id', 'child_of', exist_supplier.id)]
        if xml_type_of_document == 'I':
            domain += [('type', '=', 'in_invoice')]
        if xml_type_of_document == 'E':
            domain += [('type', '=', 'in_refund')]
        uuid_dupli = xml_uuid in inv_obj.search(domain).mapped(
            'l10n_mx_edi_cfdi_uuid')
        mxns = [
            'mxp', 'mxn', 'pesos', 'peso mexicano', 'pesos mexicanos', 'mn']
        xml_currency = 'MXN' if xml_currency.lower(
        ) in mxns else xml_currency

        exist_currency = currency_obj.search(
            [('name', '=', xml_currency)], limit=1)
        xml_related_uuid = False
        if xml_type_of_document == 'E' and hasattr(xml, 'CfdiRelacionados'):
            xml_related_uuid = xml.CfdiRelacionados.CfdiRelacionado.get('UUID')
            related_invoice = xml_related_uuid in inv_obj.search([
                ('l10n_mx_edi_cfdi_name', '!=', False),
                ('type', '=', 'in_invoice')]).mapped('l10n_mx_edi_cfdi_uuid')
        omit_cfdi_related = self._context.get('omit_cfdi_related')
        force_save = False
        if self.env.user.has_group(
                'l10n_mx_edi_vendor_bills.allow_force_invoice_generation'):
            force_save = self._context.get('force_save')
        errors = [
            (not xml_uuid, {'signed': True}),
            (xml_status == 'cancelled', {'cancel': True}),
            ((xml_uuid and uuid_dupli), {'uuid_duplicate': xml_uuid}),
            ((inv_vat_receiver != xml_vat_receiver),
             {'rfc': (xml_vat_receiver, inv_vat_receiver)}),
            ((not inv_id and exist_reference),
             {'reference': (xml_name_supplier, xml_folio)}),
            (version != '3.3', {'version': True}),
            ((not inv_id and not exist_supplier),
             {'supplier': xml_name_supplier}),
            ((not inv_id and xml_currency and not exist_currency),
             {'currency': xml_currency}),
            ((not inv_id and xml_taxes.get('wrong_taxes', False)),
             {'taxes': xml_taxes.get('wrong_taxes', False)}),
            ((not inv_id and xml_taxes.get('withno_account', False)),
             {'taxes_wn_accounts': xml_taxes.get(
                 'withno_account', False)}),
            ((inv_id and inv_folio != xml_folio),
             {'folio': (xml_folio, inv_folio)}),
            ((inv_id and inv_vat_emitter != xml_vat_emitter), {
                'rfc_supplier': (xml_vat_emitter, inv_vat_emitter)}),
            ((inv_id and not float_is_zero(
                float(inv_amount) - xml_amount, precision_digits=2)),
                {'amount': (xml_amount, inv_amount)}),
            ((xml_related_uuid and not related_invoice and not force_save),
             {'invoice_not_found': xml_related_uuid}),
            ((not omit_cfdi_related and xml_type_of_document == 'E' and
              not xml_related_uuid), {'no_xml_related_uuid': True}),
        ]
        msg = {}
        for error in errors:
            if error[0]:
                msg.update(error[1])
        if msg:
            msg.update({'xml64': True})
            wrongfiles.update({key: msg})
            return {'wrongfiles': wrongfiles, 'invoices': invoices}

        if not inv_id:
            invoice_status = self.create_invoice(
                xml, exist_supplier, exist_currency,
                xml_taxes.get('taxes_ids', {}), account_id)

            if invoice_status['key'] is False:
                del invoice_status['key']
                invoice_status.update({'xml64': True})
                wrongfiles.update({key: invoice_status})
                return {'wrongfiles': wrongfiles, 'invoices': invoices}

            del invoice_status['key']
            invoices.update({key: invoice_status})
            return {'wrongfiles': wrongfiles, 'invoices': invoices}

        inv.l10n_mx_edi_cfdi = xml_str.decode('UTF-8')
        inv.generate_xml_attachment()
        inv.reference = '%s|%s' % (xml_folio, xml_uuid.split('-')[0])
        invoices.update({key: {'invoice_id': inv.id}})
        if not float_is_zero(float(inv.amount_total) - xml_amount,
                             precision_digits=0):
            inv.message_post(
                body=_('The XML attached total amount is different to '
                       'the total amount in this invoice. The XML total '
                       'amount is %s') % xml_amount)
        return {'wrongfiles': wrongfiles, 'invoices': invoices}
示例#13
0
    def _update_overtime(self, employee_attendance_dates=None):
        if employee_attendance_dates is None:
            employee_attendance_dates = self._get_attendances_dates()

        overtime_to_unlink = self.env['hr.attendance.overtime']
        overtime_vals_list = []

        for emp, attendance_dates in employee_attendance_dates.items():
            # get_attendances_dates returns the date translated from the local timezone without tzinfo,
            # and contains all the date which we need to check for overtime
            emp_tz = pytz.timezone(emp._get_tz())
            attendance_domain = []
            for attendance_date in attendance_dates:
                attendance_domain = OR([
                    attendance_domain,
                    [
                        ('check_in', '>=', attendance_date[0]),
                        ('check_in', '<',
                         attendance_date[0] + timedelta(hours=24)),
                    ]
                ])
            attendance_domain = AND([[('employee_id', '=', emp.id)],
                                     attendance_domain])

            # Attendances per LOCAL day
            attendances_per_day = defaultdict(
                lambda: self.env['hr.attendance'])
            all_attendances = self.env['hr.attendance'].search(
                attendance_domain)
            for attendance in all_attendances:
                check_in_day_start = attendance._get_day_start_and_day(
                    attendance.employee_id, attendance.check_in)
                attendances_per_day[check_in_day_start[1]] += attendance

            # As _attendance_intervals_batch and _leave_intervals_batch both take localized dates we need to localize those date
            start = pytz.utc.localize(
                min(attendance_dates, key=itemgetter(0))[0])
            stop = pytz.utc.localize(
                max(attendance_dates, key=itemgetter(0))[0] +
                timedelta(hours=24))

            # Retrieve expected attendance intervals
            expected_attendances = emp.resource_calendar_id._attendance_intervals_batch(
                start, stop, emp.resource_id)[emp.resource_id.id]
            # Substract Global Leaves
            expected_attendances -= emp.resource_calendar_id._leave_intervals_batch(
                start, stop, None)[False]

            # working_times = {date: [(start, stop)]}
            working_times = defaultdict(lambda: [])
            for expected_attendance in expected_attendances:
                # Exclude resource.calendar.attendance
                working_times[expected_attendance[0].date()].append(
                    expected_attendance[:2])

            overtimes = self.env['hr.attendance.overtime'].sudo().search([
                ('employee_id', '=', emp.id),
                ('date', 'in', [day_data[1] for day_data in attendance_dates]),
                ('adjustment', '=', False),
            ])

            company_threshold = emp.company_id.overtime_company_threshold / 60.0
            employee_threshold = emp.company_id.overtime_employee_threshold / 60.0

            for day_data in attendance_dates:
                attendance_date = day_data[1]
                attendances = attendances_per_day.get(attendance_date,
                                                      self.browse())
                unfinished_shifts = attendances.filtered(
                    lambda a: not a.check_out)
                overtime_duration = 0
                overtime_duration_real = 0
                # Overtime is not counted if any shift is not closed or if there are no attendances for that day,
                # this could happen when deleting attendances.
                if not unfinished_shifts and attendances:
                    # The employee usually doesn't work on that day
                    if not working_times[attendance_date]:
                        # User does not have any resource_calendar_attendance for that day (week-end for example)
                        overtime_duration = sum(
                            attendances.mapped('worked_hours'))
                        overtime_duration_real = overtime_duration
                    # The employee usually work on that day
                    else:
                        # Compute start and end time for that day
                        planned_start_dt, planned_end_dt = False, False
                        planned_work_duration = 0
                        for calendar_attendance in working_times[
                                attendance_date]:
                            planned_start_dt = min(
                                planned_start_dt, calendar_attendance[0]
                            ) if planned_start_dt else calendar_attendance[0]
                            planned_end_dt = max(
                                planned_end_dt, calendar_attendance[1]
                            ) if planned_end_dt else calendar_attendance[1]
                            planned_work_duration += (
                                calendar_attendance[1] - calendar_attendance[0]
                            ).total_seconds() / 3600.0
                        # Count time before, during and after 'working hours'
                        pre_work_time, work_duration, post_work_time = 0, 0, 0

                        for attendance in attendances:
                            # consider check_in as planned_start_dt if within threshold
                            # if delta_in < 0: Checked in after supposed start of the day
                            # if delta_in > 0: Checked in before supposed start of the day
                            local_check_in = emp_tz.localize(
                                attendance.check_in)
                            delta_in = (planned_start_dt - local_check_in
                                        ).total_seconds() / 3600.0

                            # Started before or after planned date within the threshold interval
                            if (delta_in > 0 and delta_in <= company_threshold) or\
                                (delta_in < 0 and abs(delta_in) <= employee_threshold):
                                local_check_in = planned_start_dt
                            local_check_out = emp_tz.localize(
                                attendance.check_out)

                            # same for check_out as planned_end_dt
                            delta_out = (local_check_out - planned_end_dt
                                         ).total_seconds() / 3600.0
                            # if delta_out < 0: Checked out before supposed start of the day
                            # if delta_out > 0: Checked out after supposed start of the day

                            # Finised before or after planned date within the threshold interval
                            if (delta_out > 0 and delta_out <= company_threshold) or\
                                (delta_out < 0 and abs(delta_out) <= employee_threshold):
                                local_check_out = planned_end_dt

                            # There is an overtime at the start of the day
                            if local_check_in < planned_start_dt:
                                pre_work_time += (
                                    min(planned_start_dt, local_check_out) -
                                    local_check_in).total_seconds() / 3600.0
                            # Interval inside the working hours -> Considered as working time
                            if local_check_in <= planned_end_dt and local_check_out >= planned_start_dt:
                                work_duration += (
                                    min(planned_end_dt, local_check_out) -
                                    max(planned_start_dt, local_check_in)
                                ).total_seconds() / 3600.0
                            # There is an overtime at the end of the day
                            if local_check_out > planned_end_dt:
                                post_work_time += (local_check_out - max(
                                    planned_end_dt,
                                    local_check_in)).total_seconds() / 3600.0

                        # Overtime within the planned work hours + overtime before/after work hours is > company threshold
                        overtime_duration = work_duration - planned_work_duration
                        if pre_work_time > company_threshold:
                            overtime_duration += pre_work_time
                        if post_work_time > company_threshold:
                            overtime_duration += post_work_time
                        # Global overtime including the thresholds
                        overtime_duration_real = sum(
                            attendances.mapped(
                                'worked_hours')) - planned_work_duration

                overtime = overtimes.filtered(
                    lambda o: o.date == attendance_date)
                if not float_is_zero(overtime_duration,
                                     2) or unfinished_shifts:
                    # Do not create if any attendance doesn't have a check_out, update if exists
                    if unfinished_shifts:
                        overtime_duration = 0
                    if not overtime and overtime_duration:
                        overtime_vals_list.append({
                            'employee_id':
                            emp.id,
                            'date':
                            attendance_date,
                            'duration':
                            overtime_duration,
                            'duration_real':
                            overtime_duration_real,
                        })
                    elif overtime:
                        overtime.sudo().write({
                            'duration':
                            overtime_duration,
                            'duration_real':
                            overtime_duration
                        })
                elif overtime:
                    overtime_to_unlink |= overtime
        self.env['hr.attendance.overtime'].sudo().create(overtime_vals_list)
        overtime_to_unlink.sudo().unlink()
示例#14
0
 def _create_exchanges(self):
     """
         To finalize data and create the Exchanged picking and Invoices if necessary
     :return: Int of the new created picking ID, Int of the picking type ID
     """
     # create new picking for exchanged products
     if self.picking_id.picking_type_id.exchange_picking_type_id:
         picking_type_id = self.picking_id.picking_type_id.exchange_picking_type_id.id
     elif self.picking_id.picking_type_id.return_picking_type_id:
         picking_type_id = self.picking_id.picking_type_id.return_picking_type_id.id
     else:
         picking_type_id = self.picking_id.picking_type_id.id
     moves = self.picking_id.move_ids_without_package
     purchase_lines = moves.mapped('purchase_line_id')
     purchase_order = purchase_lines.mapped('order_id')
     new_picking = self.picking_id.copy({
         'move_lines': [],
         'sale_id':
         False,
         'picking_type_id':
         picking_type_id,
         'exchange_sale_id':
         self.picking_id.sale_id.id,
         'exchange_purchase_id':
         purchase_order and purchase_order[0].id,
         'state':
         'draft',
         'origin':
         _("Exchange of %s") % self.picking_id.name,
         'location_id':
         self.picking_id.location_dest_id.id,
         'location_dest_id':
         self.location_id.id
     })
     new_picking.message_post_with_view(
         'mail.message_origin_link',
         values={
             'self': new_picking,
             'origin': self.picking_id
         },
         subtype_id=self.env.ref('mail.mt_note').id)
     exchanged_lines = 0
     invoices_values = []
     for exchange_line in self.exchange_line_ids:
         if not exchange_line.move_id:
             raise UserError(
                 _("You have manually created product lines, "
                   "please delete them to proceed."))
         if not float_is_zero(
                 exchange_line.quantity,
                 precision_rounding=exchange_line.uom_id.rounding):
             exchanged_lines += 1
             vals = self._prepare_move_default_values(
                 exchange_line, new_picking)
             move = exchange_line.move_id.copy(vals)
             exchange_line.move_id.exchanged = True
             val = {'exchange_move_id': move.id}
             line = move.sale_line_id or move.purchase_line_id
             invoice_value = self._prepare_invoice_lines(
                 exchange_line, line)
             if invoice_value:
                 invoices_values.append(invoice_value)
             line.write(val)
     if not exchanged_lines:
         raise UserError(
             _("Please specify at least one non-zero quantity."))
     if invoices_values:
         self.action_create_invoices(invoices_values)
     new_picking.action_confirm()
     new_picking.action_assign()
     return new_picking.id, picking_type_id
示例#15
0
    def _update_reserved_quantity(self,
                                  product_id,
                                  location_id,
                                  quantity,
                                  lot_id=None,
                                  package_id=None,
                                  owner_id=None,
                                  strict=False):
        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)
        lista = quants
        transf_oper = self.env['transf.operaciones'].search([
            ('id_user', '=', self.env.user.id)
        ])
        series_start = ''
        trasfe_id = 0
        type_transfer = ''
        for tras in transf_oper:
            if product_id.id == tras.id_producto.id:
                series_start = tras.serie_start
        if str(series_start) != '' and str(series_start) != 'False':
            cont = 0
            for lot in quants:
                if str(lot.lot_id.name) == str(series_start):
                    cont += 1
                    max_quantity_on_quan = lot.quantity - lot.reserved_quantity
                    if float_compare(max_quantity_on_quan,
                                     0,
                                     precision_rounding=rounding) <= 0:
                        raise ValidationError(
                            _('Ya ha sido utilizada la serie %s en el producto %s.'
                              ) % (series_start, product_id.name))
            if cont == 0:
                raise ValidationError(
                    _('No se encuentra la serie %s en el producto %s.') %
                    (series_start, product_id.name))
        for trasfe in transf_oper:
            if product_id.id == trasfe.id_producto.id and str(
                    trasfe.serie_start) == str(series_start):
                count = 0
                if trasfe.type_transfer == 'ov-ac':
                    while int(count) < int(trasfe.demanda):
                        if '-' in series_start:
                            letter = series_start.split('-')[0]
                            number = series_start.split('-')[1]
                            new_serie = letter + '-' + str(
                                int(number) + int(count)).zfill(len(number))
                            for lo in quants:
                                if str(lo.lot_id.name) == str(new_serie):
                                    max_quantity_on_qua = lo.quantity - lo.reserved_quantity
                                    if float_compare(
                                            max_quantity_on_qua,
                                            0,
                                            precision_rounding=rounding) <= 0:
                                        raise ValidationError(
                                            _('Ya ha sido utilizada la serie %s en el producto %s.'
                                              ) % (new_serie, product_id.name))
                            count += 1
                        else:
                            new_serie = str(int(series_start) + count).zfill(
                                len(series_start))
                            for lo in quants:
                                if str(lo.lot_id.name) == str(new_serie):
                                    max_quantity_on_qua = lo.quantity - lo.reserved_quantity
                                    if float_compare(
                                            max_quantity_on_qua,
                                            0,
                                            precision_rounding=rounding) <= 0:
                                        raise ValidationError(
                                            _('Ya ha sido utilizada la serie %s en el producto %s.'
                                              ) % (new_serie, product_id.name))
                            count += 1
                if trasfe.type_transfer == 'ov-ac':
                    contad = 0
                    while int(contad) < int(trasfe.demanda):
                        trasfe_id = trasfe.id
                        if '-' in series_start:
                            lette = series_start.split('-')[0]
                            numbe = series_start.split('-')[1]
                            new_seri = lette + '-' + str(
                                int(numbe) + int(contad)).zfill(len(numbe))
                            contad += 1
                            self.env['det.operaciones'].create({
                                'transf_operaciones':
                                trasfe.id,
                                'serie':
                                new_seri
                            })
                        else:
                            new_seri = str(int(series_start) + contad).zfill(
                                len(series_start))
                            contad += 1
                            self.env['det.operaciones'].create({
                                'transf_operaciones':
                                trasfe.id,
                                'serie':
                                new_seri
                            })
                else:
                    trasfe_id = trasfe.id
                    type_transfer = trasfe.type_transfer
                    self.env['det.operaciones'].create({
                        'transf_operaciones':
                        trasfe.id,
                        'serie':
                        series_start
                    })
        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
        det_oper = self.env['det.operaciones'].search([('transf_operaciones',
                                                        '=', int(trasfe_id))])
        for det_op in det_oper:
            available_quantityy, quantityy, buscar_serie = self.buscar_serie(
                lista, det_op.serie, quantity, rounding, available_quantity,
                type_transfer)
            quantity = quantityy
            available_quantity = available_quantityy
            for re in buscar_serie:
                reserved_quants.append(
                    (re['lot_'], re['max_quantity_on_quan']))
        if len(det_oper) == 0:
            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
        return reserved_quants
示例#16
0
    def check_xml(self, files, account_id=False):
        """Validate that attributes in the XML before create invoice
        or attach XML in it.
        If the invoice is not found in the system, will be created and
        validated using the same 'Serie-Folio' that in the CFDI.
        :param files: dictionary of CFDIs in b64
        :type files: dict
        :param account_id: The account by default that must be used in the
            lines of the invoice if this is created
        :type account_id: int
        :return: the Result of the CFDI validation
        :rtype: dict
        """
        if self._context.get('l10n_mx_edi_invoice_type') != 'out':
            return super().check_xml(files, account_id=account_id)
        if not isinstance(files, dict):
            raise UserError(
                _("Something went wrong. The parameter for XML "
                  "files must be a dictionary."))
        inv_obj = self.env['account.invoice']
        partner_obj = self.env['res.partner']
        currency_obj = self.env['res.currency']
        wrongfiles = {}
        invoices = {}
        account_id = self._context.get('account_id', False)
        for key, xml64 in files.items():
            inv = inv_obj
            inv_id = False
            try:
                xml64 = xml64.decode() if isinstance(xml64, bytes) else xml64
                xml_str = base64.b64decode(
                    xml64.replace('data:text/xml;base64,', ''))
                # Fix the CFDIs emitted by the SAT
                xml_str = xml_str.replace(b'xmlns:schemaLocation',
                                          b'xsi:schemaLocation')
                xml = objectify.fromstring(xml_str)
            except (AttributeError, SyntaxError) as exce:
                wrongfiles.update({
                    key: {
                        'xml64': xml64,
                        'where': 'CheckXML',
                        'error': [exce.__class__.__name__,
                                  str(exce)]
                    }
                })
                continue
            xml = self._l10n_mx_edi_convert_cfdi32_to_cfdi33(xml)
            if xml.get('TipoDeComprobante', False) != 'I':
                wrongfiles.update({key: {'cfdi_type': True, 'xml64': xml64}})
                continue
            xml_vat_emitter = xml.Emisor.get('Rfc', '').upper()
            xml_vat_receiver = xml.Receptor.get('Rfc', '').upper()
            xml_amount = xml.get('Total', 0.0)
            xml_tfd = inv_obj.l10n_mx_edi_get_tfd_etree(xml)
            xml_uuid = False if xml_tfd is None else xml_tfd.get('UUID', '')
            xml_folio = xml.get('Folio', '')
            xml_currency = xml.get('Moneda', 'MXN')
            xml_taxes = self.get_impuestos(xml)
            xml_local_taxes = self.get_local_taxes(xml)
            xml_taxes['wrong_taxes'] = xml_taxes.get(
                'wrong_taxes', []) + xml_local_taxes.get('wrong_taxes', [])
            xml_taxes['withno_account'] = xml_taxes.get(
                'withno_account', []) + xml_local_taxes.get(
                    'withno_account', [])
            version = xml.get('Version', xml.get('version'))
            xml_name_supplier = xml.Receptor.get('Nombre', '')
            domain = ['&',
                      ('vat', '=',
                       xml_vat_receiver)] if (xml_vat_receiver not in [
                           'XEXX010101000', 'XAXX010101000'
                       ]) else ['&', ('name', '=ilike', xml_name_supplier)]
            domain.extend(
                ['|', ('supplier', '=', True), ('customer', '=', True)])
            exist_supplier = partner_obj.search(domain, limit=1)
            exist_reference = xml_folio and inv_obj.search(
                [('origin', '=', xml_folio), ('type', '=', 'out_invoice'),
                 ('partner_id', '=', exist_supplier.id)],
                limit=1)
            if exist_reference and (not exist_reference.l10n_mx_edi_cfdi_uuid
                                    or exist_reference.l10n_mx_edi_cfdi_uuid
                                    == xml_uuid):
                inv = exist_reference
                inv_id = inv.id
                exist_reference = False
                inv.l10n_mx_edi_update_sat_status()
            xml_status = inv.l10n_mx_edi_sat_status
            inv_vat_emitter = (self.env.user.company_id.vat or '').upper()
            inv_vat_receiver = (inv and inv.commercial_partner_id.vat
                                or '').upper()
            inv_amount = inv.amount_total
            diff = inv.journal_id.l10n_mx_edi_amount_authorized_diff or 1
            inv_folio = inv.origin
            domain = [('l10n_mx_edi_cfdi_name', '!=', False),
                      ('type', '=', 'out_invoice'), ('id', '!=', inv_id)]
            if exist_supplier:
                domain += [('partner_id', '=', exist_supplier.id)]
            uuid_dupli = xml_uuid in inv_obj.search(domain).mapped(
                'l10n_mx_edi_cfdi_uuid')
            mxns = ['mxp', 'mxn', 'pesos', 'peso mexicano', 'pesos mexicanos']
            xml_currency = 'MXN' if xml_currency.lower(
            ) in mxns else xml_currency

            exist_currency = currency_obj.search([
                '|', ('name', '=', xml_currency),
                ('currency_unit_label', '=ilike', xml_currency)
            ],
                                                 limit=1)

            errors = [
                (not xml_uuid, {
                    'signed': True
                }),
                (xml_status == 'cancelled', {
                    'cancel': True
                }),
                ((xml_uuid and uuid_dupli), {
                    'uuid_duplicate': xml_uuid
                }),
                (
                    (inv_id and inv_vat_receiver
                     and inv_vat_receiver != xml_vat_receiver),  # noqa
                    {
                        'rfc_supplier': (xml_vat_receiver, inv_vat_receiver)
                    }),
                ((not inv_id and exist_reference), {
                    'reference': (xml_name_supplier, xml_folio)
                }),
                (version != '3.3', {
                    'version': True
                }),
                ((not inv_id and not exist_supplier), {
                    'customer': xml_name_supplier
                }),
                ((not inv_id and xml_currency and not exist_currency), {
                    'currency': xml_currency
                }),
                ((not inv_id and xml_taxes.get('wrong_taxes', False)), {
                    'taxes': xml_taxes.get('wrong_taxes', False)
                }),
                ((not inv_id and xml_taxes.get('withno_account', False)), {
                    'taxes_wn_accounts': xml_taxes.get('withno_account', False)
                }),
                ((inv_id and inv_folio != xml_folio), {
                    'folio': (xml_folio, inv_folio)
                }),
                ((inv_vat_emitter != xml_vat_emitter), {
                    'rfc_cust': (xml_vat_emitter, inv_vat_emitter)
                }),
                ((inv_id and
                  abs(round(float(inv_amount) - float(xml_amount), 2)) > diff),
                 {
                     'amount': (xml_amount, inv_amount)
                 }),
            ]
            msg = {}
            for error in errors:
                if error[0]:
                    msg.update(error[1])
            if msg:
                msg.update({'xml64': xml64})
                wrongfiles.update({key: msg})
                continue

            if not inv_id:
                invoice_status = self.create_invoice(
                    xml, exist_supplier, exist_currency,
                    xml_taxes.get('taxes_ids', {}), account_id)

                if invoice_status['key'] is False:
                    del invoice_status['key']
                    invoice_status.update({'xml64': xml64})
                    wrongfiles.update({key: invoice_status})
                    continue

                del invoice_status['key']
                invoices.update({key: invoice_status})
                continue

            inv.l10n_mx_edi_cfdi = xml_str.decode('UTF-8')
            inv.generate_xml_attachment()
            inv.action_invoice_open()
            inv.l10n_mx_edi_update_sat_status()
            invoices.update({key: {'invoice_id': inv.id}})
            if not float_is_zero(float(inv_amount) - float(xml_amount),
                                 precision_digits=0):
                inv.message_post(
                    body=_('The XML attached total amount is different to '
                           'the total amount in this invoice. The XML total '
                           'amount is %s'
                           '') % xml_amount)
        return {'wrongfiles': wrongfiles, 'invoices': invoices}
示例#17
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 = []
            cost_to_add_byproduct = defaultdict(lambda: 0.0)
            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':
                    cost_to_add_byproduct[product] += cost_to_add
                # Products with manual inventory valuation are ignored because they do not need to create journal entries.
                if product.valuation != "real_time":
                    continue
                # `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)

            # batch standard price computation avoid recompute quantity_svl at each iteration
            products = self.env['product.product'].browse(
                p.id for p in cost_to_add_byproduct.keys())
            for product in products:  # iterate on recordset to prefetch efficiently quantity_svl
                if not float_is_zero(
                        product.quantity_svl,
                        precision_rounding=product.uom_id.rounding):
                    product.with_context(
                        force_company=cost.company_id.id).sudo(
                        ).standard_price += cost_to_add_byproduct[
                            product] / product.quantity_svl

            move_vals['stock_valuation_layer_ids'] = [(6, None,
                                                       valuation_layer_ids)]
            # We will only create the accounting entry when there are defined lines (the lines will be those linked to products of real_time valuation category).
            cost_vals = {'state': 'done'}
            if move_vals.get("line_ids"):
                move = move.create(move_vals)
                cost_vals.update({'account_move_id': move.id})
            cost.write(cost_vals)
            if cost.account_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
示例#18
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'),
            ('result_package_id', 'stock.quant.package'),
            ('owner_id', 'res.partner'),
            ('product_uom_id', 'uom.uom')
        ]
        updates = {}
        for key, model in triggers:
            if key in vals:
                updates[key] = self.env[model].browse(vals[key])

        if 'result_package_id' in updates:
            for ml in self.filtered(lambda ml: ml.package_level_id):
                if updates.get('result_package_id'):
                    ml.package_level_id.package_id = updates.get('result_package_id')
                else:
                    # TODO: make package levels less of a pain and fix this
                    package_level = ml.package_level_id
                    ml.package_level_id = False
                    package_level.unlink()

        # 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):
                    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)

                # 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:
                        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.product_uom_id._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)
                product_uom_id = updates.get('product_uom_id', ml.product_uom_id)
                quantity = product_uom_id._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
示例#19
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
示例#20
0
    def write(self, vals):
        """ Through the interface, we allow users to change the charateristics of a move line. If a
        quantity has been reserved for this move line, we impact the reservation directly to free
        the old quants and allocate the new ones.
        """
        if self.env.context.get('bypass_reservation_update'):
            return super(StockMoveLine, self).write(vals)

        Quant = self.env['stock.quant']
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        # We forbid to change the reserved quantity in the interace, but it is needed in the
        # case of stock.move's split.
        # TODO Move me in the update
        if 'product_uom_qty' in vals:
            for ml in self.filtered(lambda m: m.state in
                                    ('partially_available', 'assigned'
                                     ) and m.product_id.type == 'product'):
                if not ml.location_id.should_bypass_reservation():
                    qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(
                        vals['product_uom_qty'],
                        ml.product_id.uom_id,
                        rounding_method='HALF-UP')
                    try:
                        Quant._update_reserved_quantity(
                            ml.product_id,
                            ml.location_id,
                            -qty_to_decrease,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id,
                            strict=True)
                    except UserError:
                        if ml.lot_id:
                            Quant._update_reserved_quantity(
                                ml.product_id,
                                ml.location_id,
                                -qty_to_decrease,
                                lot_id=False,
                                package_id=ml.package_id,
                                owner_id=ml.owner_id,
                                strict=True)
                        else:
                            raise

        triggers = [('location_id', 'stock.location'),
                    ('location_dest_id', 'stock.location'),
                    ('lot_id', 'stock.production.lot'),
                    ('package_id', 'stock.quant.package'),
                    ('result_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])

        if updates:
            for ml in self.filtered(
                    lambda ml: ml.state in ['partially_available', 'assigned']
                    and ml.product_id.type == 'product'):
                if not ml.location_id.should_bypass_reservation():
                    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 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

                if not updates.get('location_id',
                                   ml.location_id).should_bypass_reservation():
                    new_product_qty = 0
                    try:
                        q = Quant._update_reserved_quantity(
                            ml.product_id,
                            updates.get('location_id', ml.location_id),
                            ml.product_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)
                        new_product_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),
                                    ml.product_qty,
                                    lot_id=False,
                                    package_id=updates.get(
                                        'package_id', ml.package_id),
                                    owner_id=updates.get(
                                        'owner_id', ml.owner_id),
                                    strict=True)
                                new_product_qty = sum([x[1] for x in q])
                            except UserError:
                                pass
                    if new_product_qty != ml.product_qty:
                        new_product_uom_qty = ml.product_id.uom_id._compute_quantity(
                            new_product_qty,
                            ml.product_uom_id,
                            rounding_method='HALF-UP')
                        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:
            for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and
                                    ml.product_id.type == 'product'):
                # 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 location_id.should_bypass_reservation():
                    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 location_id.should_bypass_reservation():
                                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')
            for move in moves:
                move.product_uom_qty = move.quantity_done
        next_moves._do_unreserve()
        next_moves._action_assign()
        return res
示例#21
0
    def _compute_timesheet_revenue(self):
        for invoice in self:
            for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)):
                uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([
                    ('so_line', 'in', invoice_line.sale_line_ids.ids),
                    ('project_id', '!=', False),
                    ('timesheet_invoice_id', '=', False),
                    ('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed'])
                ])

                # NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs)
                if uninvoiced_timesheet_lines:
                    # delivered : update revenue with the prorata of number of hours on the timesheet line
                    if invoice_line.product_id.invoice_policy == 'delivery':
                        invoiced_price_per_hour = invoice_line.currency_id.round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount'))))
                        # invoicing analytic lines of different currency
                        total_revenue_per_currency = dict.fromkeys(uninvoiced_timesheet_lines.mapped('currency_id').ids, 0.0)
                        for index, timesheet_line in enumerate(uninvoiced_timesheet_lines.sorted(key=lambda ts: (ts.date, ts.id))):
                            if index+1 != len(uninvoiced_timesheet_lines):
                                line_revenue = invoice_line.currency_id._convert(
                                    invoiced_price_per_hour, timesheet_line.currency_id,
                                    self.env.user.company_id, fields.Date.today()) * timesheet_line.unit_amount
                                total_revenue_per_currency[timesheet_line.currency_id.id] += line_revenue
                            else:  # last line: add the difference to avoid rounding problem
                                total_revenue = sum([self.env['res.currency'].browse(currency_id)._convert(
                                    amount, timesheet_line.currency_id,
                                    self.env.user.company_id, fields.Date.today()
                                ) for currency_id, amount in total_revenue_per_currency.items()])
                                line_revenue = invoice_line.currency_id._convert(
                                    invoice_line.price_subtotal, timesheet_line.currency_id,
                                    self.env.user.company_id, fields.Date.today()
                                ) - total_revenue
                            timesheet_line.write({
                                'timesheet_invoice_id': invoice.id,
                                'timesheet_revenue': timesheet_line.currency_id.round(line_revenue),
                            })

                    # ordered : update revenue with the prorata of theorical revenue
                    elif invoice_line.product_id.invoice_policy == 'order':
                        zero_timesheet_revenue = uninvoiced_timesheet_lines.filtered(lambda line: line.timesheet_revenue == 0.0)
                        no_zero_timesheet_revenue = uninvoiced_timesheet_lines.filtered(lambda line: line.timesheet_revenue != 0.0)

                        # timesheet with zero theorical revenue keep the same revenue, but become invoiced (invoice_id set)
                        zero_timesheet_revenue.write({'timesheet_invoice_id': invoice.id})

                        # invoicing analytic lines of different currency
                        total_revenue_per_currency = dict.fromkeys(no_zero_timesheet_revenue.mapped('currency_id').ids, 0.0)

                        for index, timesheet_line in enumerate(no_zero_timesheet_revenue.sorted(key=lambda ts: (ts.date, ts.id))):
                            if index+1 != len(no_zero_timesheet_revenue):
                                price_subtotal_inv = invoice_line.currency_id._convert(
                                    invoice_line.price_subtotal, timesheet_line.currency_id, self.env.user.company_id, fields.Date.today())
                                price_subtotal_sol = timesheet_line.so_line.currency_id._convert(
                                    timesheet_line.so_line.price_subtotal, timesheet_line.currency_id, self.env.user.company_id, fields.Date.today())
                                if not float_is_zero(price_subtotal_sol, precision_rounding=timesheet_line.currency_id.rounding):
                                    line_revenue = timesheet_line.timesheet_revenue * price_subtotal_inv / price_subtotal_sol
                                    total_revenue_per_currency[timesheet_line.currency_id.id] += line_revenue
                                else:
                                    line_revenue = timesheet_line.timesheet_revenue
                                    total_revenue_per_currency[timesheet_line.currency_id.id] += line_revenue
                            else:  # last line: add the difference to avoid rounding problem
                                last_price_subtotal_inv = invoice_line.currency_id._convert(
                                    invoice_line.price_subtotal, timesheet_line.currency_id,
                                    self.env.user.company_id, fields.Date.today())
                                total_revenue = sum([self.env['res.currency'].browse(currency_id)._convert(
                                    amount, timesheet_line.currency_id,
                                    self.env.user.company_id, fields.Date.today()
                                ) for currency_id, amount in total_revenue_per_currency.items()])
                                line_revenue = last_price_subtotal_inv - total_revenue

                            timesheet_line.write({
                                'timesheet_invoice_id': invoice.id,
                                'timesheet_revenue': timesheet_line.currency_id.round(line_revenue),
                            })
示例#22
0
    def button_validate(self):
        self.ensure_one()
        if not self.move_lines and not self.move_line_ids:
            raise UserError(_('Please add some items to move.'))
        self.check_grn_quantity()

        # Clean-up the context key at validation to avoid forcing the creation of immediate
        # transfers.
        ctx = dict(self.env.context)
        ctx.pop('default_immediate_transfer', None)
        self = self.with_context(ctx)

        # add user as a follower
        self.message_subscribe([self.env.user.partner_id.id])

        # If no lots when needed, raise error
        picking_type = self.picking_type_id
        precision_digits = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        no_quantities_done = all(
            float_is_zero(move_line.qty_done,
                          precision_digits=precision_digits)
            for move_line in self.move_line_ids.filtered(
                lambda m: m.state not in ('done', 'cancel')))
        no_reserved_quantities = all(
            float_is_zero(move_line.product_qty,
                          precision_rounding=move_line.product_uom_id.rounding)
            for move_line in self.move_line_ids)
        if no_reserved_quantities and no_quantities_done:
            raise UserError(
                _('You cannot validate a transfer if no quantites are reserved nor done. To force the transfer, switch in edit more and encode the done quantities.'
                  ))

        if picking_type.use_create_lots or picking_type.use_existing_lots:
            lines_to_check = self.move_line_ids
            if not no_quantities_done:
                lines_to_check = lines_to_check.filtered(
                    lambda line: float_compare(line.qty_done,
                                               0,
                                               precision_rounding=line.
                                               product_uom_id.rounding))

            for line in lines_to_check:
                product = line.product_id
                if product and product.tracking != 'none':
                    if not line.lot_name and not line.lot_id:
                        raise UserError(
                            _('You need to supply a Lot/Serial number for product %s.'
                              ) % product.display_name)

        # Propose to use the sms mechanism the first time a delivery
        # picking is validated. Whatever the user's decision (use it or not),
        # the method button_validate is called again (except if it's cancel),
        # so the checks are made twice in that case, but the flow is not broken
        sms_confirmation = self._check_sms_confirmation_popup()
        if sms_confirmation:
            return sms_confirmation

        if no_quantities_done:
            view = self.env.ref('stock.view_immediate_transfer')
            wiz = self.env['stock.immediate.transfer'].create(
                {'pick_ids': [(4, self.id)]})
            return {
                'name': _('Immediate Transfer?'),
                'type': 'ir.actions.act_window',
                'view_mode': 'form',
                'res_model': 'stock.immediate.transfer',
                'views': [(view.id, 'form')],
                'view_id': view.id,
                'target': 'new',
                'res_id': wiz.id,
                'context': self.env.context,
            }

        if self._get_overprocessed_stock_moves(
        ) and not self._context.get('skip_overprocessed_check'):
            view = self.env.ref('stock.view_overprocessed_transfer')
            wiz = self.env['stock.overprocessed.transfer'].create(
                {'picking_id': self.id})
            return {
                'type': 'ir.actions.act_window',
                'view_mode': 'form',
                'res_model': 'stock.overprocessed.transfer',
                'views': [(view.id, 'form')],
                'view_id': view.id,
                'target': 'new',
                'res_id': wiz.id,
                'context': self.env.context,
            }

        # Check backorder should check for other barcodes
        if self._check_backorder():
            return self.action_generate_backorder_wizard()
        self.action_done()
        return
示例#23
0
 def unlink(self):
     precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
     for ml in self:
         # 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)
                 elif not self.env.context.get(MODULE_UNINSTALL_FLAG, False):
                     raise   # pylint: disable=raise-unlink-override
     moves = self.mapped('move_id')
     res = super(StockMoveLine, self).unlink()
     if moves:
         # Add with_prefetch() to set the _prefecht_ids = _ids
         # because _prefecht_ids generator look lazily on the cache of move_id
         # which is clear by the unlink of move line
         moves.with_prefetch()._recompute_state()
     return res
示例#24
0
    def _update_finished_move(self):
        """ Update the finished move & move lines in order to set the finished
        product lot on it as well as the produced quantity. This method get the
        information either from the last workorder or from the Produce wizard."""
        production_move = self.production_id.move_finished_ids.filtered(
            lambda move: move.product_id == self.product_id and
                         move.state not in ('done', 'cancel')
        )
        if production_move and production_move.product_id.tracking != 'none':
            if not self.finished_lot_id:
                raise UserError(_('You need to provide a lot for the finished product.'))
            move_line = production_move.move_line_ids.filtered(
                lambda line: line.lot_id.id == self.finished_lot_id.id
            )
            if move_line:
                if self.product_id.tracking == 'serial':
                    raise UserError(_('You cannot produce the same serial number twice.'))
                move_line.product_uom_qty += self.product_qty  # qty_producing in v13
                move_line.qty_done += self.product_qty
            else:
                location_dest_id = production_move.location_dest_id._get_putaway_strategy(
                    self.product_id).id or production_move.location_dest_id.id
                move_line.create({
                    'move_id': production_move.id,
                    'product_id': production_move.product_id.id,
                    'lot_id': self.finished_lot_id.id,
                    'product_uom_qty': self.product_qty,
                    'product_uom_id': self.product_uom_id.id,
                    'qty_done': self.product_qty,
                    'location_id': production_move.location_id.id,
                    'location_dest_id': location_dest_id,
                })
        # else:
        #     rounding = production_move.product_uom.rounding
        #     print(production_move)
        #     production_move._set_quantity_done(
        #         float_round(self.product_qty, precision_rounding=rounding)
        #     )

        # Part of the '_update_finished_move method' in v13 'mrp_subcontracting'
        """ After producing, set the move line on the subcontract picking. """
        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.product_qty,  # qty_producing in v13
                '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()
示例#25
0
    def _compute_quantities_dict(self,
                                 lot_id,
                                 owner_id,
                                 package_id,
                                 from_date=False,
                                 to_date=False):
        """ When the product is a kit, this override computes the fields :
         - 'virtual_available'
         - 'qty_available'
         - 'incoming_qty'
         - 'outgoing_qty'
         - 'free_qty'

        This override is used to get the correct quantities of products
        with 'phantom' as BoM type.
        """
        bom_kits = self.env['mrp.bom']._bom_find(self, bom_type='phantom')
        kits = self.filtered(lambda p: bom_kits.get(p))
        res = super(ProductProduct,
                    self - kits)._compute_quantities_dict(lot_id,
                                                          owner_id,
                                                          package_id,
                                                          from_date=from_date,
                                                          to_date=to_date)
        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,
                    raise_if_failure=False)
                if not qty_per_kit:
                    continue
                component_res = res.get(
                    component.id, {
                        "virtual_available": component.virtual_available,
                        "qty_available": component.qty_available,
                        "incoming_qty": component.incoming_qty,
                        "outgoing_qty": component.outgoing_qty,
                        "free_qty": component.free_qty,
                    })
                ratios_virtual_available.append(
                    component_res["virtual_available"] / qty_per_kit)
                ratios_qty_available.append(component_res["qty_available"] /
                                            qty_per_kit)
                ratios_incoming_qty.append(component_res["incoming_qty"] /
                                           qty_per_kit)
                ratios_outgoing_qty.append(component_res["outgoing_qty"] /
                                           qty_per_kit)
                ratios_free_qty.append(component_res["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.
                res[product.id] = {
                    'virtual_available': min(ratios_virtual_available) // 1,
                    'qty_available': min(ratios_qty_available) // 1,
                    'incoming_qty': min(ratios_incoming_qty) // 1,
                    'outgoing_qty': min(ratios_outgoing_qty) // 1,
                    'free_qty': min(ratios_free_qty) // 1,
                }
            else:
                res[product.id] = {
                    'virtual_available': 0,
                    'qty_available': 0,
                    'incoming_qty': 0,
                    'outgoing_qty': 0,
                    'free_qty': 0,
                }

        return res
示例#26
0
    def _update_available_quantity(self,
                                   product_id,
                                   location_id,
                                   quantity,
                                   lot_id=None,
                                   package_id=None,
                                   owner_id=None,
                                   in_date=None):
        """ Increase or decrease `reserved_quantity` of a set of quants for a given set of
        product_id/location_id/lot_id/package_id/owner_id.

        :param product_id:
        :param location_id:
        :param quantity:
        :param lot_id:
        :param package_id:
        :param owner_id:
        :param datetime in_date: Should only be passed when calls to this method are done in
                                 order to move a quant. When creating a tracked quant, the
                                 current datetime will be used.
        :return: tuple (available_quantity, in_date as a datetime)
        """
        self = self.sudo()
        quants = self._gather(product_id,
                              location_id,
                              lot_id=lot_id,
                              package_id=package_id,
                              owner_id=owner_id,
                              strict=True)
        rounding = product_id.uom_id.rounding

        if lot_id:
            incoming_dates = quants.mapped(
                'in_date')  # `mapped` already filtered out falsy items
            incoming_dates = [
                fields.Datetime.from_string(incoming_date)
                for incoming_date in incoming_dates
            ]
            if in_date:
                incoming_dates += [in_date]
            # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
            # consider only the oldest one as being relevant.
            if incoming_dates:
                in_date = fields.Datetime.to_string(min(incoming_dates))
            else:
                in_date = fields.Datetime.now()

        for quant in quants:
            try:
                with self._cr.savepoint():
                    self._cr.execute(
                        "SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT",
                        [quant.id],
                        log_exceptions=False)
                    quant.write({
                        'quantity': quant.quantity + quantity,
                        'in_date': in_date,
                    })
                    # cleanup empty quants
                    if float_is_zero(
                            quant.quantity,
                            precision_rounding=rounding) and float_is_zero(
                                quant.reserved_quantity,
                                precision_rounding=rounding):
                        quant.unlink()
                    break
            except OperationalError as e:
                if e.pgcode == '55P03':  # could not obtain the lock
                    continue
                else:
                    raise
        else:
            self.create({
                'product_id': product_id.id,
                'location_id': location_id.id,
                'quantity': quantity,
                'lot_id': lot_id and lot_id.id,
                'package_id': package_id and package_id.id,
                'owner_id': owner_id and owner_id.id,
                'in_date': in_date,
            })
        return self._get_available_quantity(
            product_id,
            location_id,
            lot_id=lot_id,
            package_id=package_id,
            owner_id=owner_id,
            strict=False,
            allow_negative=True), fields.Datetime.from_string(in_date)
	def button_validate(self):
		if self.picking_type_id.code != 'incoming':
			self.ensure_one()
			if not self.move_lines and not self.move_line_ids:
				raise UserError(_('Please add some lines to move'))
			super(StockPickingUpdate, self).button_validate()
			view = self.env.ref('bi_stock_transfer_backdate.view_change_stock_item')
			return {
					'view_type': 'form',
					'view_mode': 'form',
					'res_model': 'change.module',
					'type': 'ir.actions.act_window',
					'target': 'new',
					'res_id': False,
					'context': self.env.context,
				}
		else:
			self.ensure_one()
			if not self.move_lines and not self.move_line_ids:
				raise UserError(_('Please add some items to move.'))

			# If no lots when needed, raise error
			picking_type = self.picking_type_id
			precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
			no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids)
			no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
			if no_reserved_quantities and no_quantities_done:
				raise UserError(_('You cannot validate a transfer if no quantites are reserved nor done. To force the transfer, switch in edit more and encode the done quantities.'))

			if picking_type.use_create_lots or picking_type.use_existing_lots:
				lines_to_check = self.move_line_ids
				if not no_quantities_done:
					lines_to_check = lines_to_check.filtered(
						lambda line: float_compare(line.qty_done, 0,
												   precision_rounding=line.product_uom_id.rounding)
					)

				for line in lines_to_check:
					product = line.product_id
					if product and product.tracking != 'none':
						if not line.lot_name and not line.lot_id:
							raise UserError(_('You need to supply a Lot/Serial number for product %s.') % product.display_name)

			if no_quantities_done:
				view = self.env.ref('stock.view_immediate_transfer')
				wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, self.id)]})
				return {
					'name': _('Immediate Transfer?'),
					'type': 'ir.actions.act_window',
					'view_type': 'form',
					'view_mode': 'form',
					'res_model': 'stock.immediate.transfer',
					'views': [(view.id, 'form')],
					'view_id': view.id,
					'target': 'new',
					'res_id': wiz.id,
					'context': self.env.context,
				}

			if self._get_overprocessed_stock_moves() and not self._context.get('skip_overprocessed_check'):
				view = self.env.ref('stock.view_overprocessed_transfer')
				wiz = self.env['stock.overprocessed.transfer'].create({'picking_id': self.id})
				return {
					'type': 'ir.actions.act_window',
					'view_type': 'form',
					'view_mode': 'form',
					'res_model': 'stock.overprocessed.transfer',
					'views': [(view.id, 'form')],
					'view_id': view.id,
					'target': 'new',
					'res_id': wiz.id,
					'context': self.env.context,
				}

			# Check backorder should check for other barcodes
			if self._check_backorder():
				return self.action_generate_backorder_wizard()
			self.action_done()
			return	
示例#28
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 pack operations of a done picking'))
         # Unlinking a pack operation should unreserve.
         if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and not float_is_zero(ml.product_qty, precision_digits=precision):
             self.env['stock.quant']._decrease_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)
     return super(StockMoveLine, self).unlink()
示例#29
0
    def button_validate(self):
        self._check_can_validate()
        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:
            cost = cost.with_company(cost.company_id)
            move = self.env['account.move']
            move_vals = {
                'journal_id': cost.account_journal_id.id,
                'date': cost.date,
                'ref': cost.name,
                'line_ids': [],
                'move_type': 'entry',
            }
            valuation_layer_ids = []
            cost_to_add_byproduct = defaultdict(lambda: 0.0)
            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':
                    cost_to_add_byproduct[product] += cost_to_add
                # `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)

            # batch standard price computation avoid recompute quantity_svl at each iteration
            products = self.env['product.product'].browse(
                p.id for p in cost_to_add_byproduct.keys())
            for product in products:  # iterate on recordset to prefetch efficiently quantity_svl
                if not float_is_zero(
                        product.quantity_svl,
                        precision_rounding=product.uom_id.rounding):
                    product.with_company(cost.company_id).sudo().with_context(
                        disable_auto_svl=True
                    ).standard_price += cost_to_add_byproduct[
                        product] / product.quantity_svl

            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
示例#30
0
    def write(self, vals):
        """ Through the interface, we allow users to change the charateristics of a move line. If a
        quantity has been reserved for this move line, we impact the reservation directly to free
        the old quants and allocate the new ones.
        """
        if self.env.context.get('bypass_reservation_update'):
            return super(StockMoveLine, self).write(vals)

        Quant = self.env['stock.quant']
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
        # We forbid to change the reserved quantity in the interace, but it is needed in the
        # case of stock.move's split.
        # TODO Move me in the update
        if 'product_uom_qty' in vals:
            for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned') and m.product_id.type == 'product'):
                if not ml.location_id.should_bypass_reservation():
                    qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP')
                    try:
                        Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
                    except UserError:
                        if ml.lot_id:
                            Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
                        else:
                            raise

        triggers = [
            ('location_id', 'stock.location'),
            ('location_dest_id', 'stock.location'),
            ('lot_id', 'stock.production.lot'),
            ('package_id', 'stock.quant.package'),
            ('result_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])

        if updates:
            for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'):
                if not ml.location_id.should_bypass_reservation():
                    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 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

                if not updates.get('location_id', ml.location_id).should_bypass_reservation():
                    new_product_qty = 0
                    try:
                        q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_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)
                        new_product_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), ml.product_qty, lot_id=False,
                                                                     package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
                                new_product_qty = sum([x[1] for x in q])
                            except UserError:
                                pass
                    if new_product_qty != ml.product_qty:
                        new_product_uom_qty = self.product_id.uom_id._compute_quantity(new_product_qty, self.product_uom_id, rounding_method='HALF-UP')
                        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:
            for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'):
                # 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 location_id.should_bypass_reservation():
                    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 location_id.should_bypass_reservation():
                                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')
            for move in moves:
                move.product_uom_qty = move.quantity_done
        next_moves._do_unreserve()
        next_moves._action_assign()
        return res
示例#31
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',
            }
            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]  # Maybe the LC layer should be linked to multiple IN layer?

                # 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,
                    })
                    move_vals['stock_valuation_layer_ids'] = [(6, None, [valuation_layer.id])]
                    linked_layer.remaining_value += cost_to_add
                # 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_company(self.company_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 = 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).reconcile()
        return True
示例#32
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.'))
示例#33
0
 def action_done(self):
     """
     完成盘点
     :return:
     """
     if self.doexcuted:
         return
     self.write({'doexcuted': True})
     location_inventory = self.env.ref('stock.location_inventory')
     movelines, current_time = [], fields.Datetime.now()
     if self.inventory_lines and len(self.inventory_lines) > 0:
         for iline in self.inventory_lines:
             if float_is_zero(iline.differ_qty,
                              precision_rounding=0.000001):
                 continue
             movevals = {
                 'name': iline.product_id.name,
                 'product_id': iline.product_id.id,
                 'product_uom': iline.product_id.uom_id.id,
                 'create_date': current_time,
                 'restrict_lot_id': iline.product_lot.id,
                 'product_uom_qty': abs(iline.differ_qty)
             }
             if float_compare(iline.differ_qty,
                              0.0,
                              precision_rounding=0.000001) > 0.0:
                 movevals.update({
                     'location_id': location_inventory.id,
                     'location_dest_id': iline.location_id.id
                 })
             else:
                 movevals.update({
                     'location_id': iline.location_id.id,
                     'location_dest_id': location_inventory.id
                 })
             tempmove = self.env['stock.move'].create(movevals)
             tempmove.action_done()
             movelines.append((0, 0, {'move_id': tempmove.id}))
     if self.inventory_adjust and len(self.inventory_adjust) > 0:
         movedict = {}
         for iadjust in self.inventory_adjust:
             mkey = 'M_' + str(iadjust.product_id.id) + '_' + str(
                 iadjust.product_lot.id)
             location_src_id, location_dest_id = iadjust.location_id.id, location_inventory.id
             if float_compare(iadjust.adjust_product_qty,
                              0.0,
                              precision_rounding=0.000001) > 0.0:
                 location_src_id, location_dest_id = location_inventory.id, iadjust.location_id.id
             mkey += '_' + str(location_src_id) + '_' + str(
                 location_dest_id)
             if mkey not in movedict:
                 movedict[mkey] = {
                     'name': iadjust.product_id.name,
                     'product_id': iadjust.product_id.id,
                     'product_uom': iadjust.product_id.uom_id.id,
                     'create_date': current_time,
                     'restrict_lot_id': iadjust.product_lot.id,
                     'product_uom_qty': abs(iadjust.adjust_product_qty),
                     'location_id': location_src_id,
                     'location_dest_id': location_dest_id
                 }
             else:
                 movedict[mkey]['product_uom_qty'] += abs(
                     iadjust.adjust_product_qty)
             # 更新标签上的数量
             iadjust.inventory_label.label_id.write(
                 {'product_qty': iadjust.adjust_after_qty})
         for tkey, tval in movedict.items():
             tempmove = self.env['stock.move'].create(tval)
             tempmove.action_done()
             movelines.append((0, 0, {'move_id': tempmove.id}))
     inventoryvals = {'state': 'done'}
     if movelines and len(movelines) > 0:
         inventoryvals['inventory_moves'] = movelines
     self.write(inventoryvals)
     if not self.inventory_labels or len(self.inventory_labels) <= 0:
         return
     # 解除标签锁定
     labellist = self.env['aas.product.label']
     for tlabel in self.inventory_labels:
         if tlabel.label_id:
             labellist |= tlabel.label_id
     if labellist and len(labellist) > 0:
         labellist.write({'locked': False, 'locked_order': False})