Exemple #1
0
    def _search_quants_to_reconcile(self):
        """ Searches negative quants to reconcile for where the quant to reconcile is put """
        dom = [
            "&",
            "&",
            "&",
            "&",
            ("qty", "<", 0),
            ("location_id", "child_of", self.location_id.id),
            ("product_id", "=", self.product_id.id),
            ("owner_id", "=", self.owner_id.id),
            # Do not let the quant eat itself, or it will kill its history (e.g. returns / Stock -> Stock)
            ("id", "!=", self.propagated_from_id.id),
        ]
        if self.package_id.id:
            dom = ["&"] + dom + [("package_id", "=", self.package_id.id)]
        if self.lot_id:
            dom = ["&"] + dom + ["|", ("lot_id", "=", False), ("lot_id", "=", self.lot_id.id)]
            order = "lot_id, in_date"
        else:
            order = "in_date"

        rounding = self.product_id.uom_id.rounding
        quants = []
        quantity = self.qty
        for quant in self.search(dom, order=order):
            if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0:
                quants += [(quant, abs(quant.qty))]
                quantity -= abs(quant.qty)
            elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0:
                quants += [(quant, quantity)]
                quantity = 0
                break
        return quants
Exemple #2
0
    def _create_or_update_picking(self):
        for line in self:
            if line.product_id.type in ('product', 'consu'):
                # Prevent decreasing below received quantity
                if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0:
                    raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n'
                                      'Create a return first.'))

                if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1:
                    # If the quantity is now below the invoiced quantity, create an activity on the vendor bill
                    # inviting the user to create a refund.
                    activity = self.env['mail.activity'].sudo().create({
                        'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
                        'note': _('The quantities on your purchase order indicate less than billed. You should ask for a refund. '),
                        'res_id': line.invoice_lines[0].invoice_id.id,
                        'res_model_id': self.env.ref('account.model_account_invoice').id,
                    })
                    activity._onchange_activity_type_id()

                # If the user increased quantity of existing line or created a new line
                pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit'))
                picking = pickings and pickings[0] or False
                if not picking:
                    res = line.order_id._prepare_picking()
                    picking = self.env['stock.picking'].create(res)
                move_vals = line._prepare_stock_moves(picking)
                for move_val in move_vals:
                    self.env['stock.move']\
                        .create(move_val)\
                        ._action_confirm()\
                        ._action_assign()
Exemple #3
0
    def _paypal_form_get_invalid_parameters(self, data):
        invalid_parameters = []
        _logger.info('Received a notification from Paypal with IPN version %s', data.get('notify_version'))
        if data.get('test_ipn'):
            _logger.warning(
                'Received a notification from Paypal using sandbox'
            ),

        # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
        if self.acquirer_reference and data.get('txn_id') != self.acquirer_reference:
            invalid_parameters.append(('txn_id', data.get('txn_id'), self.acquirer_reference))
        # check what is buyed
        if float_compare(float(data.get('mc_gross', '0.0')), (self.amount + self.fees), 2) != 0:
            invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % self.amount))  # mc_gross is amount + fees
        if data.get('mc_currency') != self.currency_id.name:
            invalid_parameters.append(('mc_currency', data.get('mc_currency'), self.currency_id.name))
        if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), self.fees, 2) != 0:
            invalid_parameters.append(('handling_amount', data.get('handling_amount'), self.fees))
        # check buyer
        if self.payment_token_id and data.get('payer_id') != self.payment_token_id.acquirer_ref:
            invalid_parameters.append(('payer_id', data.get('payer_id'), self.payment_token_id.acquirer_ref))
        # check seller
        if data.get('receiver_id') and self.acquirer_id.paypal_seller_account and data['receiver_id'] != self.acquirer_id.paypal_seller_account:
            invalid_parameters.append(('receiver_id', data.get('receiver_id'), self.acquirer_id.paypal_seller_account))
        if not data.get('receiver_id') or not self.acquirer_id.paypal_seller_account:
            # Check receiver_email only if receiver_id was not checked.
            # In Paypal, this is possible to configure as receiver_email a different email than the business email (the login email)
            # In Odoo, there is only one field for the Paypal email: the business email. This isn't possible to set a receiver_email
            # different than the business email. Therefore, if you want such a configuration in your Paypal, you are then obliged to fill
            # the Merchant ID in the Paypal payment acquirer in Odoo, so the check is performed on this variable instead of the receiver_email.
            # At least one of the two checks must be done, to avoid fraudsters.
            if data.get('receiver_email') != self.acquirer_id.paypal_email_account:
                invalid_parameters.append(('receiver_email', data.get('receiver_email'), self.acquirer_id.paypal_email_account))

        return invalid_parameters
Exemple #4
0
    def _search_quants_to_reconcile(self):
        """ Searches negative quants to reconcile for where the quant to reconcile is put """
        dom = ['&', '&', '&', '&',
               ('qty', '<', 0),
               ('location_id', 'child_of', self.location_id.id),
               ('product_id', '=', self.product_id.id),
               ('owner_id', '=', self.owner_id.id),
               # Do not let the quant eat itself, or it will kill its history (e.g. returns / Stock -> Stock)
               ('id', '!=', self.propagated_from_id.id)]
        if self.package_id.id:
            dom = ['&'] + dom + [('package_id', '=', self.package_id.id)]
        if self.lot_id:
            dom = ['&'] + dom + ['|', ('lot_id', '=', False), ('lot_id', '=', self.lot_id.id)]
            order = 'lot_id, in_date'
        else:
            order = 'in_date'

        rounding = self.product_id.uom_id.rounding
        quants = []
        quantity = self.qty
        for quant in self.search(dom, order=order):
            if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0:
                quants += [(quant, abs(quant.qty))]
                quantity -= abs(quant.qty)
            elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0:
                quants += [(quant, quantity)]
                quantity = 0
                break
        return quants
Exemple #5
0
    def quants_reserve(self, quants, move, link=False):
        ''' This function reserves quants for the given move and optionally
        given link. If the total of quantity reserved is enough, the move state
        is also set to 'assigned'

        :param quants: list of tuple(quant browse record or None, qty to reserve). If None is given as first tuple element, the item will be ignored. Negative quants should not be received as argument
        :param move: browse record
        :param link: browse record (stock.move.operation.link)
        '''
        # TDE CLEANME: use ids + quantities dict
        # TDE CLEANME: check use of sudo
        quants_to_reserve_sudo = self.env['stock.quant'].sudo()
        reserved_availability = move.reserved_availability
        # split quants if needed
        for quant, qty in quants:
            if qty <= 0.0 or (quant and quant.qty <= 0.0):
                raise UserError(_('You can not reserve a negative quantity or a negative quant.'))
            if not quant:
                continue
            quant._quant_split(qty)
            quants_to_reserve_sudo |= quant
            reserved_availability += quant.qty
        # reserve quants
        if quants_to_reserve_sudo:
            quants_to_reserve_sudo.write({'reservation_id': move.id})
        # check if move state needs to be set as 'assigned'
        # TDE CLEANME: should be moved as a move model method IMO
        rounding = move.product_id.uom_id.rounding
        if float_compare(reserved_availability, move.product_qty, precision_rounding=rounding) == 0 and move.state in ('confirmed', 'waiting'):
            move.write({'state': 'assigned'})
        elif float_compare(reserved_availability, 0, precision_rounding=rounding) > 0 and not move.partially_available:
            move.write({'partially_available': True})
Exemple #6
0
    def _get_invoiced(self):
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
        for order in self:
            if order.state not in ('purchase', 'done'):
                order.invoice_status = 'no'
                continue

            if any(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) == -1 for line in order.order_line):
                order.invoice_status = 'to invoice'
            elif all(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) >= 0 for line in order.order_line) and order.invoice_ids:
                order.invoice_status = 'invoiced'
            else:
                order.invoice_status = 'no'
Exemple #7
0
 def _prepare_invoice_line_from_po_line(self, line):
     if line.product_id.purchase_method == 'purchase':
         qty = line.product_qty - line.qty_invoiced
     else:
         qty = line.qty_received - line.qty_invoiced
     if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
         qty = 0.0
     taxes = line.taxes_id
     invoice_line_tax_ids = line.order_id.fiscal_position_id.map_tax(taxes)
     invoice_line = self.env['account.invoice.line']
     data = {
         'purchase_line_id': line.id,
         'name': line.order_id.name+': '+line.name,
         'origin': line.order_id.origin,
         'uom_id': line.product_uom.id,
         'product_id': line.product_id.id,
         'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
         'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
         'quantity': qty,
         'discount': 0.0,
         'account_analytic_id': line.account_analytic_id.id,
         'analytic_tag_ids': line.analytic_tag_ids.ids,
         'invoice_line_tax_ids': invoice_line_tax_ids.ids
     }
     account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, line.order_id.fiscal_position_id, self.env.user.company_id)
     if account:
         data['account_id'] = account.id
     return data
Exemple #8
0
    def add_payment(self, data):
        statement_id = super(PosOrder, self).add_payment(data)
        statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id),
                                                                         ('pos_statement_id', '=', self.id),
                                                                         ('journal_id', '=', data['journal'])])
        statement_lines = statement_lines.filtered(lambda line: float_compare(line.amount, data['amount'],
                                                                              precision_rounding=line.journal_currency_id.rounding) == 0)

        # we can get multiple statement_lines when there are >1 credit
        # card payments with the same amount. In that case it doesn't
        # matter which statement line we pick, just pick one that
        # isn't already used.
        for line in statement_lines:
            if not line.mercury_card_brand:
                line.mercury_card_brand = data.get('card_brand')
                line.mercury_card_number = data.get('card_number')
                line.mercury_card_owner_name = data.get('card_owner_name')

                line.mercury_ref_no = data.get('ref_no')
                line.mercury_record_no = data.get('record_no')
                line.mercury_invoice_no = data.get('invoice_no')

                break

        return statement_id
Exemple #9
0
    def action_done(self):
        """ This method will finalize the work with a move line by "moving" quants to the
        destination location.
        """
        for ml in self:
            if ml.product_id.type != 'consu':
                Quant = self.env['stock.quant']
                rounding = ml.product_uom_id.rounding

                # if this move line is force assigned, unreserve elsewhere if needed
                if float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
                    extra_qty = ml.qty_done - ml.product_qty
                    ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
                # unreserve what's been reserved
                if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and ml.product_qty:
                    try:
                        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)
                    except UserError:
                        Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)

                # move what's been actually done
                quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
                if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
                    available_qty = Quant._decrease_available_quantity(ml.product_id, ml.location_id, quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
                    if available_qty < 0 and ml.lot_id:
                        # see if we can compensate the negative quants with some untracked quants
                        untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
                        if untracked_qty:
                            taken_from_untracked_qty = min(untracked_qty, abs(quantity))
                            Quant._decrease_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
                            Quant._increase_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
                if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
                    Quant._increase_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id)
Exemple #10
0
    def quants_get_reservation(
        self,
        qty,
        move,
        pack_operation_id=False,
        lot_id=False,
        company_id=False,
        domain=None,
        preferred_domain_list=None,
    ):
        """ This function tries to find quants for the given domain and move/ops, by trying to first limit
            the choice on the quants that match the first item of preferred_domain_list as well. But if the qty requested is not reached
            it tries to find the remaining quantity by looping on the preferred_domain_list (tries with the second item and so on).
            Make sure the quants aren't found twice => all the domains of preferred_domain_list should be orthogonal
        """
        # TDE FIXME: clean me
        reservations = [(None, qty)]

        pack_operation = self.env["stock.pack.operation"].browse(pack_operation_id)
        location = pack_operation.location_id if pack_operation else move.location_id

        # don't look for quants in location that are of type production, supplier or inventory.
        if location.usage in ["inventory", "production", "supplier"]:
            return reservations
            # return self._Reservation(reserved_quants, qty, qty, move, None)

        restrict_lot_id = lot_id if pack_operation else move.restrict_lot_id.id or lot_id
        removal_strategy = move.get_removal_strategy()

        domain = self._quants_get_reservation_domain(
            move, pack_operation_id=pack_operation_id, lot_id=lot_id, company_id=company_id, initial_domain=domain
        )

        if not restrict_lot_id and not preferred_domain_list:
            meta_domains = [[]]
        elif restrict_lot_id and not preferred_domain_list:
            meta_domains = [[("lot_id", "=", restrict_lot_id)], [("lot_id", "=", False)]]
        elif restrict_lot_id and preferred_domain_list:
            lot_list = []
            no_lot_list = []
            for inner_domain in preferred_domain_list:
                lot_list.append(inner_domain + [("lot_id", "=", restrict_lot_id)])
                no_lot_list.append(inner_domain + [("lot_id", "=", False)])
            meta_domains = lot_list + no_lot_list
        else:
            meta_domains = preferred_domain_list

        res_qty = qty
        while float_compare(res_qty, 0, precision_rounding=move.product_id.uom_id.rounding) and meta_domains:
            additional_domain = meta_domains.pop(0)
            reservations.pop()
            new_reservations = self._quants_get_reservation(
                res_qty, move, ops=pack_operation, domain=domain + additional_domain, removal_strategy=removal_strategy
            )
            for quant in new_reservations:
                if quant[0]:
                    res_qty -= quant[1]
            reservations += new_reservations

        return reservations
Exemple #11
0
    def _generate_moves(self):
        moves = self.env['stock.move']
        Quant = self.env['stock.quant']
        for line in self:
            line._fixup_negative_quants()

            if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
                continue
            diff = line.theoretical_qty - line.product_qty
            if diff < 0:  # found more than expected
                vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id)
            else:
                vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id)
            move = moves.create(vals)

            if diff > 0:
                domain = [('qty', '>', 0.0), ('package_id', '=', line.package_id.id), ('lot_id', '=', line.prod_lot_id.id), ('location_id', '=', line.location_id.id)]
                preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', line.inventory_id.id)]]
                quants = Quant.quants_get_preferred_domain(move.product_qty, move, domain=domain, preferred_domain_list=preferred_domain_list)
                Quant.quants_reserve(quants, move)
            elif line.package_id:
                move.action_done()
                move.quant_ids.write({'package_id': line.package_id.id})
                quants = Quant.search([('qty', '<', 0.0), ('product_id', '=', move.product_id.id),
                                       ('location_id', '=', move.location_dest_id.id), ('package_id', '!=', False)], limit=1)
                if quants:
                    for quant in move.quant_ids:
                        if quant.location_id.id == move.location_dest_id.id:  #To avoid we take a quant that was reconcile already
                            quant._quant_reconcile_negative(move)
        return moves
Exemple #12
0
    def _put_in_pack(self):
        package = False
        for pick in self:
            operations = pick.move_line_ids.filtered(lambda o: o.qty_done > 0 and not o.result_package_id)
            operation_ids = self.env['stock.move.line']
            if operations:
                package = self.env['stock.quant.package'].create({})
                for operation in operations:
                    if float_compare(operation.qty_done, operation.product_uom_qty, precision_rounding=operation.product_uom_id.rounding) >= 0:
                        operation_ids |= operation
                    else:
                        quantity_left_todo = float_round(
                            operation.product_uom_qty - operation.qty_done,
                            precision_rounding=operation.product_uom_id.rounding,
                            rounding_method='UP')
                        done_to_keep = operation.qty_done
                        new_operation = operation.copy(
                            default={'product_uom_qty': 0, 'qty_done': operation.qty_done})
                        operation.write({'product_uom_qty': quantity_left_todo, 'qty_done': 0.0})
                        new_operation.write({'product_uom_qty': done_to_keep})
                        operation_ids |= new_operation

                operation_ids.write({'result_package_id': package.id})
            else:
                raise UserError(_('Please process some quantities to put in the pack first!'))
        return package
Exemple #13
0
 def _compute_show_check_availability(self):
     for picking in self:
         has_moves_to_reserve = any(
             move.state in ('waiting', 'confirmed', 'partially_available') and
             float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding)
             for move in picking.move_lines
         )
         picking.show_check_availability = picking.is_locked and picking.state in ('confirmed', 'waiting', 'assigned') and has_moves_to_reserve
Exemple #14
0
 def split_quantities(self):
     for operation in self:
         if float_compare(operation.product_qty, operation.qty_done, precision_rounding=operation.product_uom_id.rounding) == 1:
             operation.copy(default={'qty_done': 0.0, 'product_qty': operation.product_qty - operation.qty_done})
             operation.write({'product_qty': operation.qty_done})
         else:
             raise UserError(_('The quantity to split should be smaller than the quantity To Do.  '))
     return True
Exemple #15
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()
                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')
                    quantity -= quantity_split
                    move_to_recompute_state |= candidate.move_id
                if quantity == 0.0:
                    break
            move_to_recompute_state._recompute_state()
Exemple #16
0
 def _ccavenue_form_get_invalid_parameters(self, data):
     invalid_parameters = []
     if self.acquirer_reference and data.get('order_id') != self.acquirer_reference:
         invalid_parameters.append(
             ('Transaction Id', data.get('order_id'), self.acquirer_reference))
     # check what is buyed
     if float_compare(float(data.get('amount', '0.0')), self.amount, precision_digits=2) != 0:
         invalid_parameters.append(('Amount', data.get('amount'), '%.2f' % self.amount))
     return invalid_parameters
Exemple #17
0
    def _authorize_form_get_invalid_parameters(self, data):
        invalid_parameters = []

        if self.acquirer_reference and data.get('x_trans_id') != self.acquirer_reference:
            invalid_parameters.append(('Transaction Id', data.get('x_trans_id'), self.acquirer_reference))
        # check what is buyed
        if float_compare(float(data.get('x_amount', '0.0')), self.amount, 2) != 0:
            invalid_parameters.append(('Amount', data.get('x_amount'), '%.2f' % self.amount))
        return invalid_parameters
Exemple #18
0
    def _transfer_form_get_invalid_parameters(self, data):
        invalid_parameters = []

        if float_compare(float(data.get('amount', '0.0')), self.amount, 2) != 0:
            invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount))
        if data.get('currency') != self.currency_id.name:
            invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name))

        return invalid_parameters
 def _process(self, cancel_backorder=False):
     if cancel_backorder:
         for pick_id in self.pick_ids:
             moves_to_log = {}
             for move in pick_id.move_lines:
                 if float_compare(move.product_uom_qty, move.quantity_done, move.product_uom.rounding) > 0:
                     moves_to_log[move] = (move.quantity_done, move.product_uom_qty)
             pick_id._log_less_quantities_than_expected(moves_to_log)
     self.pick_ids.with_context({'cancel_backorder': cancel_backorder}).action_done()
Exemple #20
0
 def _onchange_qty_done(self):
     """ When the user is encoding a move line for a tracked product, we apply some logic to
     help him. This onchange will warn him if he set `qty_done` to a non-supported value.
     """
     res = {}
     if self.qty_done and self.product_id.tracking == 'serial':
         if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
             message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
             res['warning'] = {'title': _('Warning'), 'message': message}
     return res
Exemple #21
0
    def _payulatam_form_get_invalid_parameters(self, data):
        invalid_parameters = []

        if self.acquirer_reference and data.get('referenceCode') != self.acquirer_reference:
            invalid_parameters.append(('Reference code', data.get('referenceCode'), self.acquirer_reference))
        if float_compare(float(data.get('TX_VALUE', '0.0')), self.amount, 2) != 0:
            invalid_parameters.append(('Amount', data.get('TX_VALUE'), '%.2f' % self.amount))
        if data.get('merchantId') != self.acquirer_id.payulatam_merchant_id:
            invalid_parameters.append(('Merchant Id', data.get('merchantId'), self.acquirer_id.payulatam_merchant_id))
        return invalid_parameters
 def _default_shipping_weight(self):
     picking = self.env['stock.picking'].browse(self.env.context.get('default_picking_id'))
     move_line_ids = picking.move_line_ids.filtered(lambda m:
         float_compare(m.qty_done, 0.0, precision_rounding=m.product_uom_id.rounding) > 0
         and not m.result_package_id
     )
     total_weight = 0.0
     for ml in move_line_ids:
         qty = ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
         total_weight += qty * ml.product_id.weight
     return total_weight
Exemple #23
0
    def _copy_remaining_pack_lot_ids(self, new_operation):
        for op in self:
            for lot in op.pack_lot_ids:
                new_qty_todo = lot.qty_todo - lot.qty

                if float_compare(new_qty_todo, 0, precision_rounding=op.product_uom_id.rounding) > 0:
                    lot.copy({
                        'operation_id': new_operation.id,
                        'qty_todo': new_qty_todo,
                        'qty': 0,
                    })
 def put_in_pack(self):
     move_line_ids = self.picking_id.move_line_ids.filtered(lambda ml:
         float_compare(ml.qty_done, 0.0, precision_rounding=ml.product_uom_id.rounding) > 0
         and not ml.result_package_id
     )
     delivery_package = self.picking_id._put_in_pack(move_line_ids)
     # write shipping weight and product_packaging on 'stock_quant_package' if needed
     if self.delivery_packaging_id:
         delivery_package.packaging_id = self.delivery_packaging_id
         if self.shipping_weight:
             delivery_package.shipping_weight = self.shipping_weight
Exemple #25
0
    def _payumoney_form_get_invalid_parameters(self, transaction, data):
        invalid_parameters = []

        if transaction.acquirer_reference and data.get('mihpayid') != transaction.acquirer_reference:
            invalid_parameters.append(
                ('Transaction Id', data.get('mihpayid'), transaction.acquirer_reference))
        #check what is buyed
        if float_compare(float(data.get('amount', '0.0')), transaction.amount, 2) != 0:
            invalid_parameters.append(
                ('Amount', data.get('amount'), '%.2f' % transaction.amount))

        return invalid_parameters
Exemple #26
0
    def _alipay_form_get_invalid_parameters(self, data):
        invalid_parameters = []

        if float_compare(float(data.get('total_fee', '0.0')), (self.amount + self.fees), 2) != 0:
            invalid_parameters.append(('total_fee', data.get('total_fee'), '%.2f' % (self.amount + self.fees)))  # mc_gross is amount + fees
        if self.acquirer_id.alipay_payment_method == 'standard_checkout':
            if data.get('currency') != self.currency_id.name:
                invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name))
        else:
            if data.get('seller_email') != self.acquirer_id.alipay_seller_email:
                invalid_parameters.append(('seller_email', data.get('seller_email'), self.acquirer_id.alipay_seller_email))
        return invalid_parameters
Exemple #27
0
 def _generate_moves(self):
     moves = self.env['stock.move']
     for line in self:
         if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
             continue
         diff = line.theoretical_qty - line.product_qty
         if diff < 0:  # found more than expected
             vals = line._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id, False)
         else:
             vals = line._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id, True)
         moves |= self.env['stock.move'].create(vals)
     return moves
Exemple #28
0
    def _buckaroo_form_get_invalid_parameters(self, data):
        invalid_parameters = []
        data = normalize_keys_upper(data)
        if self.acquirer_reference and data.get('BRQ_TRANSACTIONS') != self.acquirer_reference:
            invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), self.acquirer_reference))
        # check what is buyed
        if float_compare(float(data.get('BRQ_AMOUNT', '0.0')), self.amount, 2) != 0:
            invalid_parameters.append(('Amount', data.get('BRQ_AMOUNT'), '%.2f' % self.amount))
        if data.get('BRQ_CURRENCY') != self.currency_id.name:
            invalid_parameters.append(('Currency', data.get('BRQ_CURRENCY'), self.currency_id.name))

        return invalid_parameters
Exemple #29
0
 def _quant_split(self, qty):
     self.ensure_one()
     rounding = self.product_id.uom_id.rounding
     if float_compare(abs(self.qty), abs(qty), precision_rounding=rounding) <= 0: # if quant <= qty in abs, take it entirely
         return False
     qty_round = float_round(qty, precision_rounding=rounding)
     new_qty_round = float_round(self.qty - qty, precision_rounding=rounding)
     # Fetch the history_ids manually as it will not do a join with the stock moves then (=> a lot faster)
     self._cr.execute("""SELECT move_id FROM stock_quant_move_rel WHERE quant_id = %s""", (self.id,))
     res = self._cr.fetchall()
     new_quant = self.sudo().copy(default={'qty': new_qty_round, 'history_ids': [(4, x[0]) for x in res]})
     self.sudo().write({'qty': qty_round})
     return new_quant
Exemple #30
0
    def _sips_form_get_invalid_parameters(self, data):
        invalid_parameters = []

        data = self._sips_data_to_object(data.get('Data'))

        # TODO: txn_id: should be false at draft, set afterwards, and verified with txn details
        if self.acquirer_reference and data.get('transactionReference') != self.acquirer_reference:
            invalid_parameters.append(('transactionReference', data.get('transactionReference'), self.acquirer_reference))
        # check what is bought
        if float_compare(float(data.get('amount', '0.0')) / 100, self.amount, 2) != 0:
            invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount))

        return invalid_parameters
    def action_apply_quant(self):
        quant_ids = self.quant_ids.filtered(lambda x: x.new_quantity > 0.00)
        if not quant_ids:
            return
        precision_digits = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        self.move_id.do_unreserve_for_pda()
        route_vals = self.move_id.update_info_route_vals()
        moves = self.env['stock.move']

        for quant_id in quant_ids:
            quant = quant_id.quant_id
            ##SI CAMBIA EL PICKING_TYPE_ID DEL MOVMIMIENTOS SEGÚN LA NUEVA UBICACION
            if self.move_id.picking_type_id.code == 'incoming':
                field = 'location_dest_id'
            else:
                field = 'location_id'

            new_location = quant.location_id
            new_move_id = self.move_id._split(quant_id.new_quantity)
            new_move = self.env['stock.move'].browse(new_move_id)
            if new_location.picking_type_id and new_location.picking_type_id != self.move_id.location_id.picking_type_id:
                ##tengo que cambiarlod e albarán
                new_loc_vals = {
                    field: new_location.id,
                    'picking_type_id': new_location.picking_type_id.id,
                    'picking_id': False
                }
                new_loc_vals.update(route_vals)
                new_move.write(new_loc_vals)
                new_move.check_new_location()
                if float_compare(quant_id.new_quantity,
                                 0.0,
                                 precision_digits=precision_digits) > 0:
                    available_quantity = quant._get_available_quantity(
                        new_move.product_id,
                        quant[field],
                        lot_id=quant.lot_id,
                        package_id=quant.package_id,
                        owner_id=quant.owner_id,
                    )
                if float_compare(available_quantity,
                                 0.0,
                                 precision_digits=precision_digits) <= 0:
                    return
                new_move._update_reserved_quantity(quant_id.new_quantity,
                                                   available_quantity,
                                                   quant[field],
                                                   lot_id=quant.lot_id,
                                                   package_id=quant.package_id,
                                                   owner_id=quant.owner_id,
                                                   strict=True)
                moves |= new_move
            else:
                if float_compare(quant_id.new_quantity,
                                 0.0,
                                 precision_digits=precision_digits) > 0:
                    available_quantity = quant._get_available_quantity(
                        new_move.product_id,
                        quant[field],
                        lot_id=quant.lot_id,
                        package_id=quant.package_id,
                        owner_id=quant.owner_id,
                    )
                if float_compare(available_quantity,
                                 0.0,
                                 precision_digits=precision_digits) <= 0:
                    return
                new_move._update_reserved_quantity(quant_id.new_quantity,
                                                   available_quantity,
                                                   quant[field],
                                                   lot_id=quant.lot_id,
                                                   package_id=quant.package_id,
                                                   owner_id=quant.owner_id,
                                                   strict=True)
                moves |= new_move
        if moves and self.move_id not in moves:
            self.move_id.action_cancel_for_pda()

        moves._action_assign()
        moves.move_sel_assign_picking()
        self.move_id._recompute_state()
        moves._recompute_state()
        return self.env['stock.picking.type'].return_action_show_moves(
            domain=[('id', 'in', moves.ids)])
Exemple #32
0
    def _prepare_stock_moves(self, picking):
        """ Prepare the stock moves data for one order line. This function returns a list of
        dictionary ready to be used in stock.move's create()
        """
        self.ensure_one()
        res = []
        if self.product_id.type not in ['product', 'consu']:
            return res

        qty = 0.0
        price_unit = self._get_stock_move_price_unit()
        outgoing_moves, incoming_moves = self._get_outgoing_incoming_moves()
        for move in outgoing_moves:
            qty -= move.product_uom._compute_quantity(
                move.product_uom_qty,
                self.product_uom,
                rounding_method='HALF-UP')
        for move in incoming_moves:
            qty += move.product_uom._compute_quantity(
                move.product_uom_qty,
                self.product_uom,
                rounding_method='HALF-UP')

        move_dests = self.move_dest_ids
        if not move_dests:
            move_dests = self.move_ids.move_dest_ids.filtered(
                lambda m: m.state != 'cancel' and not m.location_dest_id.usage
                == 'supplier')

        if not move_dests:
            qty_to_attach = 0
            qty_to_push = self.product_qty - qty
        else:
            move_dests_initial_demand = self.product_id.uom_id._compute_quantity(
                sum(
                    move_dests.filtered(lambda m: m.state != 'cancel' and not m
                                        .location_dest_id.usage == 'supplier').
                    mapped('product_qty')),
                self.product_uom,
                rounding_method='HALF-UP')
            qty_to_attach = move_dests_initial_demand - qty
            qty_to_push = self.product_qty - move_dests_initial_demand

        if float_compare(qty_to_attach,
                         0.0,
                         precision_rounding=self.product_uom.rounding) > 0:
            product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(
                qty_to_attach, self.product_id.uom_id)
            res.append(
                self._prepare_stock_move_vals(picking, price_unit,
                                              product_uom_qty, product_uom))
        if float_compare(qty_to_push,
                         0.0,
                         precision_rounding=self.product_uom.rounding) > 0:
            product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(
                qty_to_push, self.product_id.uom_id)
            extra_move_vals = self._prepare_stock_move_vals(
                picking, price_unit, product_uom_qty, product_uom)
            extra_move_vals['move_dest_ids'] = False  # don't attach
            res.append(extra_move_vals)
        return res
Exemple #33
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)
        if lot_id and quantity > 0:
            quants = quants.filtered(lambda q: q.lot_id)

        if location_id.should_bypass_reservation():
            incoming_dates = []
        else:
            incoming_dates = [quant.in_date for quant in quants if quant.in_date and
                              float_compare(quant.quantity, 0, precision_rounding=quant.product_uom_id.rounding) > 0]
        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(flush=False):  # Avoid flush compute store of package
                    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,
                    })
                    break
            except OperationalError as e:
                if e.pgcode == '55P03':  # could not obtain the lock
                    continue
                else:
                    # Because savepoint doesn't flush, we need to invalidate the cache
                    # when there is a error raise from the write (other than lock-error)
                    self.clear_caches()
                    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)
Exemple #34
0
 def _prepare_stock_moves(self, picking):
     """ Prepare the stock moves data for one order line. This function returns a list of
     dictionary ready to be used in stock.move's create()
     """
     self.ensure_one()
     res = []
     if self.product_id.type not in ['product', 'consu']:
         return res
     qty = 0.0
     price_unit = self._get_stock_move_price_unit()
     for move in self.move_ids.filtered(
             lambda x: x.state != 'cancel' and not x.location_dest_id.usage
             == "supplier"):
         qty += move.product_uom._compute_quantity(
             move.product_uom_qty,
             self.product_uom,
             rounding_method='HALF-UP')
     template = {
         # truncate to 2000 to avoid triggering index limit error
         # TODO: remove index in master?
         'name': (self.name or '')[:2000],
         'product_id':
         self.product_id.id,
         'product_uom':
         self.product_uom.id,
         'date':
         self.order_id.date_order,
         'date_expected':
         self.date_planned,
         'location_id':
         self.order_id.partner_id.property_stock_supplier.id,
         'location_dest_id':
         self.order_id._get_destination_location(),
         'picking_id':
         picking.id,
         'partner_id':
         self.order_id.dest_address_id.id,
         'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
         'state':
         'draft',
         'purchase_line_id':
         self.id,
         'company_id':
         self.order_id.company_id.id,
         'price_unit':
         price_unit,
         'picking_type_id':
         self.order_id.picking_type_id.id,
         'group_id':
         self.order_id.group_id.id,
         'origin':
         self.order_id.name,
         'propagate_date':
         self.propagate_date,
         'propagate_date_minimum_delta':
         self.propagate_date_minimum_delta,
         'description_picking':
         self.product_id._get_description(self.order_id.picking_type_id),
         'propagate_cancel':
         self.propagate_cancel,
         'route_ids':
         self.order_id.picking_type_id.warehouse_id and [(6, 0, [
             x.id
             for x in self.order_id.picking_type_id.warehouse_id.route_ids
         ])] or [],
         'warehouse_id':
         self.order_id.picking_type_id.warehouse_id.id,
     }
     diff_quantity = self.product_qty - qty
     if float_compare(diff_quantity,
                      0.0,
                      precision_rounding=self.product_uom.rounding) > 0:
         po_line_uom = self.product_uom
         quant_uom = self.product_id.uom_id
         product_uom_qty, product_uom = po_line_uom._adjust_uom_quantities(
             diff_quantity, quant_uom)
         template['product_uom_qty'] = product_uom_qty
         template['product_uom'] = product_uom.id
         res.append(template)
     return res
Exemple #35
0
    def _action_assign(self, mo_obj, lot_name):
        move_ids = mo_obj.move_raw_ids
        assigned_moves = self.env['stock.move']
        partially_available_moves = self.env['stock.move']
        # Read the `reserved_availability` field of the moves out of the loop to prevent unwanted
        # cache invalidation when actually reserving the move.
        reserved_availability = {
            move: move.reserved_availability
            for move in move_ids
        }
        roundings = {
            move: move.product_id.uom_id.rounding
            for move in move_ids
        }
        move_line_vals_list = []
        not_available_list = []
        for move in move_ids.filtered(lambda m: m.state != 'cancel'):
            rounding = roundings[move]
            missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[
                move]
            missing_reserved_quantity = move.product_uom._compute_quantity(
                missing_reserved_uom_quantity,
                move.product_id.uom_id,
                rounding_method='HALF-UP')
            if move._should_bypass_reservation():
                # 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)):
                        move_line_vals_list.append(
                            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:
                        move_line_vals_list.append(
                            move._prepare_move_line_vals(
                                quantity=missing_reserved_quantity))
                assigned_moves |= move
            else:
                if float_is_zero(move.product_uom_qty,
                                 precision_rounding=move.product_uom.rounding):
                    assigned_moves |= move
                elif 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
                    lot_id = None
                    if move.product_id.tracking == 'none':
                        if float_is_zero(need, precision_rounding=rounding):
                            assigned_moves |= move
                            continue
                    else:
                        move_line = move.move_line_ids
                        if move_line and move_line.filtered(
                                lambda m: m.lot_id.name == lot_name or m.
                                lot_name == lot_name):
                            if float_is_zero(need,
                                             precision_rounding=rounding):
                                assigned_moves |= move
                                continue
                            lot_id = move_line[0].lot_id
                        else:
                            move_line.unlink()
                            need = move.product_uom_qty
                    # Reserve new quants and create move lines accordingly.
                    if not lot_id and move.product_id.tracking != 'none':
                        lot_id = self.env['stock.production.lot'].search([
                            ('product_id', '=', move.product_id.id),
                            ('name', '=', lot_name),
                            ('product_qty', '>=', need)
                        ])
                        if not lot_id:
                            not_available_list.append(lot_name)
                            continue
                    forced_package_id = move.package_level_id.package_id or None
                    available_quantity = move._get_available_quantity(
                        move.location_id,
                        lot_id=lot_id,
                        package_id=forced_package_id)
                    if available_quantity < need:
                        not_available_list.append(lot_name)
                        continue
                    taken_quantity = move._update_reserved_quantity(
                        need,
                        available_quantity,
                        move.location_id,
                        lot_id=lot_id,
                        package_id=forced_package_id,
                        strict=False)
                    if float_is_zero(taken_quantity,
                                     precision_rounding=rounding):
                        continue
                    if float_compare(need,
                                     taken_quantity,
                                     precision_rounding=rounding) == 0:
                        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 = move._get_available_quantity(
                            location_id,
                            lot_id=lot_id,
                            package_id=package_id,
                            owner_id=owner_id,
                            strict=True)
                        if float_is_zero(available_quantity,
                                         precision_rounding=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=rounding):
                            continue
                        if float_is_zero(need - taken_quantity,
                                         precision_rounding=rounding):
                            assigned_moves |= move
                            break
                        partially_available_moves |= move
            if move.product_id.tracking == 'serial':
                move.next_serial_count = move.product_uom_qty
        if not_available_list:
            raise UserError(
                _(' Lot %s added in uploaded file is not available in stock.',
                  (", ".join(a for a in set(not_available_list)))))

        self.env['stock.move.line'].create(move_line_vals_list)
        partially_available_moves.write({'state': 'partially_available'})
        assigned_moves.write({'state': 'assigned'})
        move_ids.mapped('picking_id')._check_entire_pack()
Exemple #36
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
Exemple #37
0
    def _get_available_quantity(self,
                                product_id,
                                location_id,
                                lot_id=None,
                                package_id=None,
                                owner_id=None,
                                strict=False,
                                allow_negative=False):
        """ Return the available quantity, i.e. the sum of `quantity` minus the sum of
        `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.
        This method is called in the following usecases:
            - when a stock move checks its availability
            - when a stock move actually assign
            - when editing a move line, to check if the new value is forced or not
            - when validating a move line with some forced values and have to potentially unlink an
              equivalent move line in another picking
        In the two first usecases, `strict` should be set to `False`, as we don't know what exact
        quants we'll reserve, and the characteristics are meaningless in this context.
        In the last ones, `strict` should be set to `True`, as we work on a specific set of
        characteristics.

        :return: available quantity as a float
        """
        self = self.sudo()
        quants = self._gather(product_id,
                              location_id,
                              lot_id=lot_id,
                              package_id=package_id,
                              owner_id=owner_id,
                              strict=strict)
        rounding = product_id.uom_id.rounding
        if product_id.tracking == 'none':
            available_quantity = sum(quants.mapped('quantity')) - sum(
                quants.mapped('reserved_quantity'))
            if allow_negative:
                return available_quantity
            else:
                return available_quantity if float_compare(
                    available_quantity, 0.0,
                    precision_rounding=rounding) >= 0.0 else 0.0
        else:
            availaible_quantities = {
                lot_id: 0.0
                for lot_id in list(set(quants.mapped('lot_id'))) +
                ['untracked']
            }
            for quant in quants:
                if not quant.lot_id:
                    availaible_quantities[
                        'untracked'] += quant.quantity - quant.reserved_quantity
                else:
                    availaible_quantities[
                        quant.
                        lot_id] += quant.quantity - quant.reserved_quantity
            if allow_negative:
                return sum(availaible_quantities.values())
            else:
                return sum([
                    available_quantity
                    for available_quantity in availaible_quantities.values()
                    if float_compare(
                        available_quantity, 0, precision_rounding=rounding) > 0
                ])
Exemple #38
0
    def test_01_compute_price_operation_cost(self):
        """Test calcuation of bom cost with operations."""
        workcenter_from1 = Form(self.env['mrp.workcenter'])
        workcenter_from1.name = 'Workcenter'
        workcenter_from1.time_efficiency = 100
        workcenter_from1.capacity = 2
        workcenter_from1.oee_target = 100
        workcenter_from1.time_start = 0
        workcenter_from1.time_stop = 0
        workcenter_from1.costs_hour = 100
        workcenter_1 = workcenter_from1.save()

        self.bom_1.write({
            'operation_ids': [
                (0, 0, {
                    'name': 'Cutting',
                    'workcenter_id': workcenter_1.id,
                    'time_mode': 'manual',
                    'time_cycle_manual': 20,
                    'sequence': 1,
                }),
                (0, 0, {
                    'name': 'Drilling',
                    'workcenter_id': workcenter_1.id,
                    'time_mode': 'manual',
                    'time_cycle_manual': 25,
                    'sequence': 2,
                }),
                (0, 0, {
                    'name': 'Fitting',
                    'workcenter_id': workcenter_1.id,
                    'time_mode': 'manual',
                    'time_cycle_manual': 30,
                    'sequence': 3,
                }),
            ],
        }),
        self.bom_2.write({
            'operation_ids': [
                (0, 0, {
                    'name': 'Cutting',
                    'workcenter_id': workcenter_1.id,
                    'time_mode': 'manual',
                    'time_cycle_manual': 20,
                    'sequence': 1,
                }),
                (0, 0, {
                    'name': 'Drilling',
                    'workcenter_id': workcenter_1.id,
                    'time_mode': 'manual',
                    'time_cycle_manual': 25,
                    'sequence': 2,
                }),
                (0, 0, {
                    'name': 'Fitting',
                    'workcenter_id': workcenter_1.id,
                    'time_mode': 'manual',
                    'time_cycle_manual': 30,
                    'sequence': 3,
                }),
            ],
        }),

        # -----------------------------------------------------------------
        # Dinning Table Operation Cost(1 Unit)
        # -----------------------------------------------------------------
        # Operation cost calculate for 1 units
        # Cutting        (20 / 60) * 100 =  33.33
        # Drilling       (25 / 60) * 100 =  41.67
        # Fitting        (30 / 60) * 100 =  50.00
        # ----------------------------------------
        # Operation Cost  1 unit = 125
        # -----------------------------------------------------------------

        # --------------------------------------------------------------------------
        # Table Head Operation Cost (1 Dozen)
        # --------------------------------------------------------------------------
        # Operation cost calculate for 1 dozens
        # Cutting        (20 * 1 / 60) * 100 =  33,33
        # Drilling       (25 * 1 / 60) * 100 =  41,67
        # Fitting        (30 * 1 / 60) * 100 =  50
        # ----------------------------------------
        # Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit
        # --------------------------------------------------------------------------

        self.assertEqual(self.dining_table.standard_price, 1000,
                         "Initial price of the Product should be 1000")
        self.dining_table.button_bom_cost()
        # Total cost of Dining Table = (550) + Total cost of operations (125) = 675.0
        self.assertEqual(
            float_round(self.dining_table.standard_price, precision_digits=2),
            675.0, "After computing price from BoM price should be 612.5")
        self.Product.browse([self.dining_table.id,
                             self.table_head.id]).action_bom_cost()
        # Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 10.42) = 854.17
        self.assertEqual(
            float_compare(self.dining_table.standard_price,
                          854.17,
                          precision_digits=2), 0,
            "After computing price from BoM price should be 786.46")
Exemple #39
0
    def _free_reservation(self,
                          product_id,
                          location_id,
                          quantity,
                          lot_id=None,
                          package_id=None,
                          owner_id=None,
                          ml_to_ignore=None):
        """ When editing a done move line or validating one with some forced quantities, it is
        possible to impact quants that were not reserved. It is therefore necessary to edit or
        unlink the move lines that reserved a quantity now unavailable.

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

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

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

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

            rounding = self.product_uom_id.rounding
            for candidate in outdated_candidates:
                if float_compare(candidate.product_qty,
                                 quantity,
                                 precision_rounding=rounding) <= 0:
                    quantity -= candidate.product_qty
                    move_to_recompute_state |= candidate.move_id
                    if candidate.qty_done:
                        candidate.product_uom_qty = 0.0
                    else:
                        candidate.unlink()
                    if float_is_zero(quantity, precision_rounding=rounding):
                        break
                else:
                    # split this move line and assign the new part to our extra move
                    quantity_split = float_round(
                        candidate.product_qty - quantity,
                        precision_rounding=self.product_uom_id.rounding,
                        rounding_method='UP')
                    candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(
                        quantity_split,
                        candidate.product_uom_id,
                        rounding_method='HALF-UP')
                    move_to_recompute_state |= candidate.move_id
                    break
            move_to_recompute_state._recompute_state()
Exemple #40
0
 def check_quantity(self):
     for quant in self:
         if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
             raise ValidationError(_('The serial number has already been assigned: \n Product: %s, Serial Number: %s') % (quant.product_id.display_name, quant.lot_id.name))
Exemple #41
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')]
        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.move_id.product_uom._compute_quantity(
                    ml.qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                in_date = Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_dest_id,
                    -qty_done_orig,
                    lot_id=ml.lot_id,
                    package_id=ml.result_package_id,
                    owner_id=ml.owner_id)[1]
                Quant._update_available_quantity(ml.product_id,
                                                 ml.location_id,
                                                 qty_done_orig,
                                                 lot_id=ml.lot_id,
                                                 package_id=ml.package_id,
                                                 owner_id=ml.owner_id,
                                                 in_date=in_date)

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

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

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

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

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

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

        if moves_to_recompute_state:
            moves_to_recompute_state._recompute_state()

        return res
Exemple #42
0
 def check_backorder(self):
     need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(done_qtys=True)
     for move in self.move_lines:
         if float_compare(move.remaining_qty, 0, precision_rounding=move.product_id.uom_id.rounding) != 0:
             return True
     return False
Exemple #43
0
def generate_stock_valuation_layer(env):
    openupgrade.logged_query(
        env.cr,
        """
            ALTER TABLE stock_valuation_layer
            ADD COLUMN old_product_price_history_id integer""",
    )
    company_obj = env["res.company"]
    product_obj = env["product.product"]
    # Needed to modify global variable
    global precision_price
    precision_price = env["decimal.precision"].precision_get("Product Price")
    precision_uom = env["decimal.precision"].precision_get(
        "Product Unit of Measure")
    companies = company_obj.search([])
    products = product_obj.with_context(active_test=False).search([
        ("type", "in", ("product", "consu"))
    ])
    all_svl_list = []
    for company in companies:
        _logger.info("Doing svl for company_id {}".format(company.id))
        for product in products:
            history_lines = []
            if product.cost_method != "fifo":
                history_lines = get_product_price_history(
                    env, company.id, product.id)
            moves = get_stock_moves(env, company.id, product.id)
            svl_in_vals_list = []
            svl_out_vals_list = []
            svl_man_vals_list = []
            svl_in_index = 0
            h_index = 0
            previous_price = 0.0
            previous_qty = 0.0
            for move in moves:
                is_dropship = True if move["move_type"] in (
                    "dropship", "dropship_return") else False
                if product.cost_method in ("average", "standard"):
                    # useless for Fifo because we have price unit in stock.move
                    # Add manual adjusts
                    have_qty = not float_is_zero(
                        previous_qty, precision_digits=precision_uom)
                    while h_index < len(history_lines) and history_lines[
                            h_index]["datetime"] < move["date"]:
                        price_history_rec = history_lines[h_index]
                        if float_compare(price_history_rec["cost"],
                                         previous_price,
                                         precision_digits=precision_price):
                            if have_qty:
                                svl_vals = _prepare_man_svl_vals(
                                    price_history_rec, previous_price,
                                    previous_qty, company, product)
                                svl_man_vals_list.append(svl_vals)
                            previous_price = price_history_rec["cost"]
                        h_index += 1
                # Add in svl
                if move["move_type"] == "in" or is_dropship:
                    total_qty = previous_qty + move["product_qty"]
                    # TODO: is needed vaccum if total_qty is negative?
                    if float_is_zero(total_qty,
                                     precision_digits=precision_uom):
                        previous_price = move["price_unit"]
                    else:
                        previous_price = float_round(
                            (previous_price * previous_qty +
                             move["price_unit"] * move["product_qty"]) /
                            total_qty,
                            precision_digits=precision_price)
                    svl_vals = _prepare_in_svl_vals(move, move["product_qty"],
                                                    move["price_unit"],
                                                    product, is_dropship)
                    svl_in_vals_list.append(svl_vals)
                    previous_qty = total_qty
                # Add out svl
                if move["move_type"] == "out" or is_dropship:
                    qty = move["product_qty"]
                    if product.cost_method in ("average",
                                               "fifo") and not is_dropship:
                        # Reduce remaininig qty in svl of type "in"
                        while qty > 0 and svl_in_index < len(svl_in_vals_list):
                            if svl_in_vals_list[svl_in_index][
                                    "remaining_qty"] >= qty:
                                candidate_cost = (
                                    svl_in_vals_list[svl_in_index]
                                    ["remaining_value"] /
                                    svl_in_vals_list[svl_in_index]
                                    ["remaining_qty"])
                                svl_in_vals_list[svl_in_index][
                                    "remaining_qty"] -= qty
                                svl_in_vals_list[svl_in_index][
                                    "remaining_value"] = float_round(
                                        candidate_cost *
                                        svl_in_vals_list[svl_in_index]
                                        ["remaining_qty"],
                                        precision_digits=precision_price)
                                qty = 0
                            elif svl_in_vals_list[svl_in_index][
                                    "remaining_qty"]:
                                qty -= svl_in_vals_list[svl_in_index][
                                    "remaining_qty"]
                                svl_in_vals_list[svl_in_index][
                                    "remaining_qty"] = 0.0
                                svl_in_vals_list[svl_in_index][
                                    "remaining_value"] = 0.0
                                svl_in_index += 1
                            else:
                                svl_in_index += 1
                    if product.cost_method == 'fifo':
                        svl_vals = _prepare_out_svl_vals(
                            move, move["product_qty"], abs(move["price_unit"]),
                            product)
                    else:
                        svl_vals = _prepare_out_svl_vals(
                            move, move["product_qty"], previous_price, product)
                    svl_out_vals_list.append(svl_vals)
                    previous_qty -= move["product_qty"]
            # Add manual adjusts after last move
            if product.cost_method in (
                    "average", "standard") and not float_is_zero(
                        previous_qty, precision_digits=precision_uom):
                # useless for Fifo because we have price unit on product form
                while h_index < len(history_lines):
                    price_history_rec = history_lines[h_index]
                    if float_compare(price_history_rec["cost"],
                                     previous_price,
                                     precision_digits=precision_price):
                        svl_vals = _prepare_man_svl_vals(
                            price_history_rec, previous_price, previous_qty,
                            company, product)
                        svl_man_vals_list.append(svl_vals)
                        previous_price = price_history_rec["cost"]
                    h_index += 1
            all_svl_list.extend(svl_in_vals_list + svl_out_vals_list +
                                svl_man_vals_list)
    if all_svl_list:
        all_svl_list = sorted(all_svl_list, key=lambda k: (k["create_date"]))
        _logger.info("To create {} svl records".format(len(all_svl_list)))
        query_insert(env.cr, "stock_valuation_layer", all_svl_list)
Exemple #44
0
 def _prepare_stock_moves(self, picking):
     """ Prepare the stock moves data for one order line. This function returns a list of
     dictionary ready to be used in stock.move's create()
     """
     self.ensure_one()
     res = []
     if self.product_id.type not in ['product', 'consu']:
         return res
     qty = 0.0
     price_unit = self._get_stock_move_price_unit()
     for move in self.move_ids.filtered(
             lambda x: x.state != 'cancel' and not x.location_dest_id.usage
             == "supplier"):
         qty += move.product_qty
     template = {
         'name':
         self.name or '',
         'product_id':
         self.product_id.id,
         'product_uom':
         self.product_uom.id,
         'date':
         self.order_id.date_order,
         'date_expected':
         self.date_planned,
         'location_id':
         self.order_id.partner_id.property_stock_supplier.id,
         'location_dest_id':
         self.order_id._get_destination_location(),
         'picking_id':
         picking.id,
         'partner_id':
         self.order_id.dest_address_id.id,
         'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
         'state':
         'draft',
         'purchase_line_id':
         self.id,
         'company_id':
         self.order_id.company_id.id,
         'price_unit':
         price_unit,
         'picking_type_id':
         self.order_id.picking_type_id.id,
         'group_id':
         self.order_id.group_id.id,
         'origin':
         self.order_id.name,
         'route_ids':
         self.order_id.picking_type_id.warehouse_id and [(6, 0, [
             x.id
             for x in self.order_id.picking_type_id.warehouse_id.route_ids
         ])] or [],
         'warehouse_id':
         self.order_id.picking_type_id.warehouse_id.id,
         'branch_id':
         self.order_id.branch_id.id
     }
     diff_quantity = self.product_qty - qty
     if float_compare(diff_quantity,
                      0.0,
                      precision_rounding=self.product_uom.rounding) > 0:
         template['product_uom_qty'] = diff_quantity
         res.append(template)
     return res
Exemple #45
0
    def _schedule_hours(self,
                        hours,
                        day_dt,
                        compute_leaves=False,
                        resource_id=None,
                        domain=None):
        """ Schedule hours of work, using a calendar and an optional resource to
        compute working and leave days. This method can be used backwards, i.e.
        scheduling days before a deadline. For compute_leaves, resource_id:
        see _get_day_work_intervals. This method does not use rrule because
        rrule does not allow backwards computation.

        :param int hours: number of hours to schedule. Use a negative number to
                          compute a backwards scheduling.
        :param datetime day_dt: reference date to compute working days. If days is
                                > 0 date is the starting date. If days is < 0
                                date is the ending date.

        :return list intervals: list of time intervals in naive UTC """
        self.ensure_one()
        backwards = (hours < 0)
        intervals = []
        remaining_hours, iterations = abs(hours * 1.0), 0

        day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
        current_datetime = day_dt_tz

        call_args = dict(compute_leaves=compute_leaves,
                         resource_id=resource_id,
                         domain=domain)

        while float_compare(
                remaining_hours, 0.0,
                precision_digits=2) in (1, 0) and iterations < 1000:
            if backwards:
                call_args['end_time'] = current_datetime.time()
            else:
                call_args['start_time'] = current_datetime.time()

            working_intervals = self._get_day_work_intervals(
                current_datetime.date(), **call_args)

            if working_intervals:
                new_working_intervals = self._interval_schedule_hours(
                    working_intervals, remaining_hours, backwards=backwards)

                res = timedelta()
                for interval in working_intervals:
                    res += interval[1] - interval[0]
                remaining_hours -= res.total_seconds() / 3600.0

                intervals = intervals + new_working_intervals if not backwards else new_working_intervals + intervals
            # get next day
            if backwards:
                current_datetime = datetime.datetime.combine(
                    self._get_previous_work_day(current_datetime),
                    datetime.time(23, 59, 59))
            else:
                current_datetime = datetime.datetime.combine(
                    self._get_next_work_day(current_datetime), datetime.time())
            # avoid infinite loops
            iterations += 1

        return intervals
Exemple #46
0
    def action_done(self):
        """ Process completely the moves given and if all moves are done, it will finish the picking. """
        self.filtered(lambda move: move.state == 'draft').action_confirm()

        Uom = self.env['product.uom']
        Quant = self.env['stock.quant']

        pickings = self.env['stock.picking']
        procurements = self.env['procurement.order']
        operations = self.env['stock.pack.operation']

        remaining_move_qty = {}

        for move in self:
            if move.picking_id:
                pickings |= move.picking_id
            remaining_move_qty[move.id] = move.product_qty
            for link in move.linked_move_operation_ids:
                operations |= link.operation_id
                pickings |= link.operation_id.picking_id

        # Sort operations according to entire packages first, then package + lot, package only, lot only
        operations = operations.sorted(
            key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) +
            (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0))

        for operation in operations:

            # product given: result put immediately in the result package (if False: without package)
            # but if pack moved entirely, quants should not be written anything for the destination package
            quant_dest_package_id = operation.product_id and operation.result_package_id.id or False
            entire_pack = not operation.product_id and True or False

            # compute quantities for each lot + check quantities match
            lot_quantities = dict(
                (pack_lot.lot_id.id,
                 operation.product_uom_id._compute_quantity(
                     pack_lot.qty, operation.product_id.uom_id))
                for pack_lot in operation.pack_lot_ids)

            qty = operation.product_qty
            if operation.product_uom_id and operation.product_uom_id != operation.product_id.uom_id:
                qty = operation.product_uom_id._compute_quantity(
                    qty, operation.product_id.uom_id)
            if operation.pack_lot_ids and float_compare(
                    sum(lot_quantities.values()),
                    qty,
                    precision_rounding=operation.product_id.uom_id.rounding
            ) != 0.0:
                raise UserError(
                    _('You have a difference between the quantity on the operation and the quantities specified for the lots. '
                      ))

            quants_taken = []
            false_quants = []
            lot_move_qty = {}

            prout_move_qty = {}
            for link in operation.linked_move_operation_ids:
                prout_move_qty[link.move_id] = prout_move_qty.get(
                    link.move_id, 0.0) + link.qty

            # Process every move only once for every pack operation
            for move in prout_move_qty.keys():
                # TDE FIXME: do in batch ?
                move.check_tracking(operation)

                # TDE FIXME: I bet the message error is wrong
                if not remaining_move_qty.get(move.id):
                    raise UserError(
                        _("The roundings of your unit of measure %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. "
                          ) %
                        (move.product_uom.name, move.product_id.uom_id.name))

                if not operation.pack_lot_ids:
                    preferred_domain_list = [[('reservation_id', '=', move.id)
                                              ],
                                             [('reservation_id', '=', False)],
                                             [
                                                 '&',
                                                 ('reservation_id', '!=',
                                                  move.id),
                                                 ('reservation_id', '!=',
                                                  False)
                                             ]]
                    quants = Quant.quants_get_preferred_domain(
                        prout_move_qty[move],
                        move,
                        ops=operation,
                        domain=[('qty', '>', 0)],
                        preferred_domain_list=preferred_domain_list)
                    Quant.quants_move(quants,
                                      move,
                                      operation.location_dest_id,
                                      location_from=operation.location_id,
                                      lot_id=False,
                                      owner_id=operation.owner_id.id,
                                      src_package_id=operation.package_id.id,
                                      dest_package_id=quant_dest_package_id,
                                      entire_pack=entire_pack)
                else:
                    # Check what you can do with reserved quants already
                    qty_on_link = prout_move_qty[move]
                    rounding = operation.product_id.uom_id.rounding
                    for reserved_quant in move.reserved_quant_ids:
                        if (reserved_quant.owner_id.id != operation.owner_id.id) or (reserved_quant.location_id.id != operation.location_id.id) or \
                                (reserved_quant.package_id.id != operation.package_id.id):
                            continue
                        if not reserved_quant.lot_id:
                            false_quants += [reserved_quant]
                        elif float_compare(lot_quantities.get(
                                reserved_quant.lot_id.id, 0),
                                           0,
                                           precision_rounding=rounding) > 0:
                            if float_compare(
                                    lot_quantities[reserved_quant.lot_id.id],
                                    reserved_quant.qty,
                                    precision_rounding=rounding) >= 0:
                                lot_quantities[reserved_quant.lot_id.
                                               id] -= reserved_quant.qty
                                quants_taken += [(reserved_quant,
                                                  reserved_quant.qty)]
                                qty_on_link -= reserved_quant.qty
                            else:
                                quants_taken += [
                                    (reserved_quant,
                                     lot_quantities[reserved_quant.lot_id.id])
                                ]
                                lot_quantities[reserved_quant.lot_id.id] = 0
                                qty_on_link -= lot_quantities[
                                    reserved_quant.lot_id.id]
                    lot_move_qty[move.id] = qty_on_link

                remaining_move_qty[move.id] -= prout_move_qty[move]

            # Handle lots separately
            if operation.pack_lot_ids:
                # TDE FIXME: fix call to move_quants_by_lot to ease understanding
                self._move_quants_by_lot(operation, lot_quantities,
                                         quants_taken, false_quants,
                                         lot_move_qty, quant_dest_package_id)

            # Handle pack in pack
            if not operation.product_id and operation.package_id and operation.result_package_id.id != operation.package_id.parent_id.id:
                operation.package_id.sudo().write(
                    {'parent_id': operation.result_package_id.id})

        # Check for remaining qtys and unreserve/check move_dest_id in
        move_dest_ids = set()
        for move in self:
            if float_compare(remaining_move_qty[move.id],
                             0,
                             precision_rounding=move.product_id.uom_id.rounding
                             ) > 0:  # In case no pack operations in picking
                move.check_tracking(
                    False)  # TDE: do in batch ? redone ? check this

                preferred_domain_list = [[('reservation_id', '=', move.id)],
                                         [('reservation_id', '=', False)],
                                         [
                                             '&',
                                             ('reservation_id', '!=', move.id),
                                             ('reservation_id', '!=', False)
                                         ]]
                quants = Quant.quants_get_preferred_domain(
                    remaining_move_qty[move.id],
                    move,
                    domain=[('qty', '>', 0)],
                    preferred_domain_list=preferred_domain_list)
                Quant.quants_move(quants,
                                  move,
                                  move.location_dest_id,
                                  lot_id=move.restrict_lot_id.id,
                                  owner_id=move.restrict_partner_id.id)

            # If the move has a destination, add it to the list to reserve
            if move.move_dest_id and move.move_dest_id.state in ('waiting',
                                                                 'confirmed'):
                move_dest_ids.add(move.move_dest_id.id)

            if move.procurement_id:
                procurements |= move.procurement_id

            # unreserve the quants and make them available for other operations/moves
            move.quants_unreserve()

        # Check the packages have been placed in the correct locations
        self.mapped('quant_ids').filtered(
            lambda quant: quant.package_id and quant.qty > 0).mapped(
                'package_id')._check_location_constraint()

        # set the move as done
        # setting force_date into stock moves
        f_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
        for move in self:
            if move.picking_id.force_date:
                f_date = move.picking_id.force_date
        self.write({'state': 'done', 'date': f_date})
        # self.write({'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
        procurements.check()
        # assign destination moves
        if move_dest_ids:
            # TDE FIXME: record setise me
            self.browse(list(move_dest_ids)).action_assign()

        pickings.filtered(lambda picking: picking.state == 'done' and
                          not picking.date_done).write({'date_done': f_date})
        # pickings.filtered(lambda picking: picking.state == 'done' and not picking.date_done).write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})

        return True
Exemple #47
0
    def button_validate(self):
        self.ensure_one()

        if self.purchase_id.requisition_id.id != False:
            picking_id = self.search([('purchase_requisition_id', '=',
                                       self.purchase_id.requisition_id.id)])
            if picking_id:
                picking_id.state = "waiting"

        if not self.move_lines and not self.move_line_ids:
            raise UserError(_('Please add some lines 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 you have not processed any quantity. You should rather cancel the transfer.'
                  ))

        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 %s.')
                            % product.display_name)
                    elif line.qty_done == 0:
                        raise UserError(
                            _('You cannot validate a transfer if you have not processed any quantity for %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
Exemple #48
0
    def _anglo_saxon_purchase_move_lines(self, i_line, res):
        """Return the additional move lines for purchase invoices and refunds.

        i_line: An account.invoice.line object.
        res: The move line entries produced so far by the parent move_line_get.
        """
        inv = i_line.invoice_id
        company_currency = inv.company_id.currency_id
        if i_line.product_id and i_line.product_id.valuation == 'real_time' and i_line.product_id.type == 'product':
            # get the fiscal position
            fpos = i_line.invoice_id.fiscal_position_id
            # get the price difference account at the product
            acc = i_line.product_id.property_account_creditor_price_difference
            if not acc:
                # if not found on the product get the price difference account at the category
                acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ
            acc = fpos.map_account(acc).id
            # reference_account_id is the stock input account
            reference_account_id = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)['stock_input'].id
            diff_res = []
            # calculate and write down the possible price difference between invoice price and product price
            for line in res:
                if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']:
                    valuation_price_unit = i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id)
                    line_quantity = line['quantity']

                    if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
                        #for average/fifo/lifo costing method, fetch real cost price from incomming moves
                        valuation_price_unit = i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id)
                        stock_move_obj = self.env['stock.move']
                        valuation_stock_move = stock_move_obj.search([('purchase_line_id', '=', i_line.purchase_line_id.id), ('state', '=', 'done')])

                        if valuation_stock_move:
                            valuation_price_unit_total = 0
                            valuation_total_qty = 0
                            for val_stock_move in valuation_stock_move:
                                valuation_price_unit_total += abs(val_stock_move.price_unit) * val_stock_move.product_qty
                                valuation_total_qty += val_stock_move.product_qty
                            valuation_price_unit = valuation_price_unit_total / valuation_total_qty
                            valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id)
                            line_quantity = valuation_total_qty

                        elif i_line.product_id.cost_method == 'real':
                            # In this condition, we have a real price-valuated product which has not yet been received
                            valuation_price_unit = i_line.purchase_line_id.price_unit

                    interim_account_price = valuation_price_unit * line_quantity
                    if inv.currency_id.id != company_currency.id:
                            # We express everyhting in the invoice currency
                            valuation_price_unit = company_currency._convert(valuation_price_unit, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False)
                            interim_account_price = company_currency._convert(interim_account_price, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False)

                    invoice_cur_prec = inv.currency_id.decimal_places

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

                        # price with discount and without tax included
                        price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0)
                        tax_ids = []
                        if line['tax_ids']:
                            #line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...]
                            taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']])
                            price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded']
                            for tax in taxes:
                                tax_ids.append((4, tax.id, None))
                                for child in tax.children_tax_ids:
                                    if child.type_tax_use != 'none':
                                        tax_ids.append((4, child.id, None))

                        price_before = line.get('price', 0.0)
                        price_unit_val_dif = price_unit - valuation_price_unit

                        price_val_dif = price_before - interim_account_price
                        if inv.currency_id.compare_amounts(i_line.price_unit, i_line.purchase_line_id.price_unit) != 0 and acc:
                            # If the unit prices have not changed and we have a
                            # valuation difference, it means this difference is due to exchange rates,
                            # so we don't create anything, the exchange rate entries will
                            # be processed automatically by the rest of the code.
                            diff_res.append({
                                'type': 'src',
                                'name': i_line.name[:64],
                                'price_unit': inv.currency_id.round(price_unit_val_dif),
                                'quantity': line_quantity,
                                'price': inv.currency_id.round(price_val_dif),
                                'account_id': acc,
                                'product_id': line['product_id'],
                                'uom_id': line['uom_id'],
                                'account_analytic_id': line['account_analytic_id'],
                                'tax_ids': tax_ids,
                                })
            return diff_res
        return []
Exemple #49
0
    def rebuild_moves(self):
        company = self.env.user.company_id.id
        # warehouse = self.env['stock.warehouse'].search([('company_id', '=', company)], limit=1)

        # force remove quant's
        for product in self.product_variant_ids:
            rounding = product.uom_id.rounding
            for warehouse in self.env['stock.warehouse'].search([('company_id', '=', company)]):
                location = warehouse.lot_stock_id
                location |= warehouse.wh_input_stock_loc_id
                location |= warehouse.wh_qc_stock_loc_id
                location |= warehouse.wh_output_stock_loc_id
                location |= warehouse.wh_pack_stock_loc_id

                quants = self.env['stock.quant'].sudo()._gather(product, location, strict=False)
                # _logger.info("QUINTS FOR PRODUCT IN LOCATIONS %s:%s:%s:%s" % (product.default_code, product, location.name, quants))

                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': 0,
                                'reserved_quantity': 0,
                            })
                            # 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

            history = self.env['product.price.history'].search([
                ('company_id', '=', company),
                ('product_id', 'in', self.product_variant_ids.ids)], order='datetime desc,id desc')
            if history:
                history.unlink()
            # quants = self.env['stock.quant'].sudo()._gather(product, location, strict=False)
            # _logger.info("QUINTS FOR PRODUCT IN LOCATIONS %s:%s:%s:%s" % (product.default_code, product, location.name, quants))

            moves = self.env['stock.move'].search([('product_id', '=', product.id), ('state', '=', 'done')])
            if self._uid == SUPERUSER_ID:
                moves = moves.filtered(lambda r: r.company_id == self.env.user.company_id)
            landed_cost = self.env['stock.valuation.adjustment.lines'].search([('move_id', 'in', moves.ids)])
            # try:
            # _logger.info("MOVES %s" % moves)
            date_move = False
            for move in moves.sorted(lambda r: r.date):
                # move.write({"state": 'assigned'})
                # move.move_line_ids.write({"state": 'assigned'})
                # # !!!! need to move special module if you are mrp is ok but is not this logic is incorrect !!!!
                # if move.production_id:
                #     production = move.production_id
                #     production.with_context(dict(self._context, force_only_production=True)).rebuild_account_move()
                #     continue
                move.remaining_value = 0.0
                move.remaining_qty = 0.0
                move.value = 0.0
                move.price_unit = 0.0
                if move.quantity_done == 0:
                    continue
                if move.inventory_id:
                    prod_inventory = move.inventory_id.line_ids.filtered(lambda r: r.product_id == move.product_id)
                    if prod_inventory and prod_inventory[0].price_unit != 0:
                        move.price_unit = prod_inventory[0].price_unit
                # if move.raw_material_production_id:
                #     force_accounting_date = move.raw_material_production_id.date_finished
                #     product = move.product_id.with_context(
                #         dict(self._context, to_date=force_accounting_date))
                #     if product.qty_at_date != 0:
                #         move.price_unit = product.account_value / product.qty_at_date
                correction_value = move._run_valuation(move.quantity_done)
                for move_line in move.move_line_ids.filtered(
                        lambda r: float_compare(r.qty_done, 0, precision_rounding=r.product_uom_id.rounding) > 0):
                    move_line._action_done()
                if self.only_quants:
                    continue

                # now to regenerate account moves
                acc_moves = False
                for acc_move in move.account_move_ids:
                    # if acc_move.state == 'posted':
                    if not acc_moves:
                        acc_moves = acc_move
                    else:
                        acc_moves |= acc_move
                if acc_moves:
                    for acc_move in acc_moves:
                        if acc_move.state == 'draft':
                            acc_move.unlink()
                            continue
                        ret = acc_move.button_cancel()
                        if ret:
                            acc_move.unlink()
                # _logger.info("MOVE %s" % move.reference)
                if move.picking_id:
                    move.write({'date': move.picking_id.date_done,
                                'accounting_date': move.picking_id.date_done})
                    move.move_line_ids.write({'date': move.picking_id.date_done})
                if move.inventory_id:
                    move.write({'date': move.inventory_id.accounting_date or move.inventory_id.date,
                                'accounting_date': move.inventory_id.accounting_date or move.inventory_id.date})
                    move.move_line_ids.write({'date': move.inventory_id.accounting_date or move.inventory_id.date})
                move.with_context(dict(self._context, force_valuation_amount=correction_value, force_date=move.date,
                                       rebuld_try=True, force_re_calculate=True)).rebuild_account_move()
                # move_landed_cost = landed_cost.filtered(lambda r: r.move_id == move)
                # for landed_cost_adjustment in move_landed_cost:
                #     landed_cost_adjustment.former_cost = move.value
                # if not date_move:
                #     date_move = move.date
                # _logger.info("move_landed_cost %s <> %s" % (date_move, move.date))
                # move_landed_cost = landed_cost.filtered(lambda r: r.move_id == move and (date_move > r.cost_id.date <= move.date))
                # if move_landed_cost:
                #     _logger.info("move_landed_cost %s=%s (date_move(%s) > r.cost_id.date(%s) <= move.date(%s))" %
                #                  (move_landed_cost.cost_id.name, move, date_move, move_landed_cost.cost_id.date,
                #                   move.date))
                #     for landed_cost_adjustment in move_landed_cost:
                #         landed_cost_adjustment.former_cost = move.value
                #     move_landed_cost.mapped('cost_id').rebuild_account_move()
                self._rebuild_moves(product, move, date_move)
                # date_move = move.date
            landed_cost.mapped('cost_id').rebuild_account_move()
Exemple #50
0
    def _schedule_hours(self,
                        hours,
                        day_dt=None,
                        compute_leaves=False,
                        resource_id=None,
                        default_interval=None):
        """ Schedule hours of work, using a calendar and an optional resource to
        compute working and leave days. This method can be used backwards, i.e.
        scheduling days before a deadline.

        :param int hours: number of hours to schedule. Use a negative number to
                          compute a backwards scheduling.
        :param datetime day_dt: reference date to compute working days. If days is
                                > 0 date is the starting date. If days is < 0
                                date is the ending date.
        :param boolean compute_leaves: if set, compute the leaves based on calendar
                                       and resource. Otherwise no leaves are taken
                                       into account.
        :param int resource_id: the id of the resource to take into account when
                                computing the leaves. If not set, only general
                                leaves are computed. If set, generic and
                                specific leaves are computed.
        :param tuple default_interval: if no id, try to return a default working
                                       day using default_interval[0] as beginning
                                       hour, and default_interval[1] as ending hour.
                                       Example: default_interval = (8, 16).
                                       Otherwise, a void list of working intervals
                                       is returned when id is None.

        :return tuple (datetime, intervals): datetime is the beginning/ending date
                                             of the schedulign; intervals are the
                                             working intervals of the scheduling.

        Note: Why not using rrule.rrule ? Because rrule does not seem to allow
        getting back in time.
        """
        if day_dt is None:
            day_dt = datetime.datetime.now()
        backwards = (hours < 0)
        hours = abs(hours)
        intervals = []
        remaining_hours = hours * 1.0
        iterations = 0
        current_datetime = day_dt

        call_args = dict(compute_leaves=compute_leaves,
                         resource_id=resource_id,
                         default_interval=default_interval)

        while float_compare(
                remaining_hours, 0.0,
                precision_digits=2) in (1, 0) and iterations < 1000:
            if backwards:
                call_args['end_dt'] = current_datetime
            else:
                call_args['start_dt'] = current_datetime

            working_intervals = self.get_working_intervals_of_day(**call_args)

            if not self and not working_intervals:  # no calendar -> consider working 8 hours
                remaining_hours -= 8.0
            elif working_intervals:
                if backwards:
                    working_intervals.reverse()
                new_working_intervals = self.interval_schedule_hours(
                    working_intervals, remaining_hours, not backwards)
                if backwards:
                    new_working_intervals.reverse()

                res = timedelta()
                for interval in working_intervals:
                    res += interval[1] - interval[0]
                remaining_hours -= (seconds(res) / 3600.0)
                if backwards:
                    intervals = new_working_intervals + intervals
                else:
                    intervals = intervals + new_working_intervals
            # get next day
            if backwards:
                current_datetime = datetime.datetime.combine(
                    self.get_previous_day(current_datetime),
                    datetime.time(23, 59, 59))
            else:
                current_datetime = datetime.datetime.combine(
                    self.get_next_day(current_datetime), datetime.time())
            # avoid infinite loops
            iterations += 1

        return intervals
Exemple #51
0
    def button_validate(self):
        self.ensure_one()
        if not self.move_lines and not self.move_line_ids:
            raise UserError(_('Please add some lines to move'))

        # If no lots when needed, raise error
        picking_type = self.picking_type_id
        no_quantities_done = all(line.qty_done == 0.0
                                 for line in self.move_line_ids)
        no_initial_demand = all(move.product_uom_qty == 0.0
                                for move in self.move_lines)
        if no_initial_demand and no_quantities_done:
            raise UserError(
                _('You cannot validate a transfer if you have not processed any quantity.'
                  ))

        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' and (
                        line.qty_done == 0 or
                    (not line.lot_name and not line.lot_id)):
                    raise UserError(
                        _('You need to supply a lot/serial number for %s.') %
                        product.name)

        # In draft or with no pack operations edited yet, ask if we can just do everything
        if self.state == 'draft' or no_quantities_done:
            view = self.env.ref('stock.view_immediate_transfer')
            wiz = self.env['stock.immediate.transfer'].create(
                {'pick_id': 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
Exemple #52
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 = []
        price_unit_prec = self.env['decimal.precision'].precision_get(
            'Product Price')

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

            move = move.with_company(move.company_id)
            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 not debit_pdiff_account:
                    continue

                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.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.with_context(
                                active_test=False).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)

                price_unit = line.price_unit * (1 -
                                                (line.discount or 0.0) / 100.0)
                if line.tax_ids:
                    # We do not want to round the price unit since :
                    # - It does not follow the currency precision
                    # - It may include a discount
                    # Since compute_all still rounds the total, we use an ugly workaround:
                    # multiply then divide the price unit.
                    price_unit *= line.quantity
                    price_unit = line.tax_ids.with_context(
                        round=False).compute_all(price_unit,
                                                 currency=move.currency_id,
                                                 quantity=1.0,
                                                 is_refund=move.move_type ==
                                                 'in_refund')['total_excluded']
                    price_unit /= line.quantity

                price_unit_val_dif = price_unit - valuation_price_unit
                price_subtotal = line.quantity * price_unit_val_dif

                # We consider there is a price difference if the subtotal is not zero. In case a
                # discount has been applied, we can't round the price unit anymore, and hence we
                # can't compare them.
                if (not move.currency_id.is_zero(price_subtotal) and
                        float_compare(line["price_unit"],
                                      line.price_unit,
                                      precision_digits=price_unit_prec) == 0):

                    # 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
Exemple #53
0
    def quants_get_reservation(self,
                               qty,
                               move,
                               pack_operation_id=False,
                               lot_id=False,
                               company_id=False,
                               domain=None,
                               preferred_domain_list=None):
        ''' This function tries to find quants for the given domain and move/ops, by trying to first limit
            the choice on the quants that match the first item of preferred_domain_list as well. But if the qty requested is not reached
            it tries to find the remaining quantity by looping on the preferred_domain_list (tries with the second item and so on).
            Make sure the quants aren't found twice => all the domains of preferred_domain_list should be orthogonal
        '''
        # TDE FIXME: clean me
        reservations = [(None, qty)]

        pack_operation = self.env['stock.pack.operation'].browse(
            pack_operation_id)
        location = pack_operation.location_id if pack_operation else move.location_id

        # don't look for quants in location that are of type production, supplier or inventory.
        if location.usage in ['inventory', 'production', 'supplier']:
            return reservations
            # return self._Reservation(reserved_quants, qty, qty, move, None)

        restrict_lot_id = lot_id if pack_operation else move.restrict_lot_id.id or lot_id
        removal_strategy = move.get_removal_strategy()

        domain = self._quants_get_reservation_domain(
            move,
            pack_operation_id=pack_operation_id,
            lot_id=lot_id,
            company_id=company_id,
            initial_domain=domain)

        if not restrict_lot_id and not preferred_domain_list:
            meta_domains = [[]]
        elif restrict_lot_id and not preferred_domain_list:
            meta_domains = [[('lot_id', '=', restrict_lot_id)],
                            [('lot_id', '=', False)]]
        elif restrict_lot_id and preferred_domain_list:
            lot_list = []
            no_lot_list = []
            for inner_domain in preferred_domain_list:
                lot_list.append(inner_domain +
                                [('lot_id', '=', restrict_lot_id)])
                no_lot_list.append(inner_domain + [('lot_id', '=', False)])
            meta_domains = lot_list + no_lot_list
        else:
            meta_domains = preferred_domain_list

        res_qty = qty
        while (float_compare(
                res_qty, 0, precision_rounding=move.product_id.uom_id.rounding)
               and meta_domains):
            additional_domain = meta_domains.pop(0)
            reservations.pop()
            new_reservations = self._quants_get_reservation(
                res_qty,
                move,
                ops=pack_operation,
                domain=domain + additional_domain,
                removal_strategy=removal_strategy)
            for quant in new_reservations:
                if quant[0]:
                    res_qty -= quant[1]
            reservations += new_reservations

        return reservations
Exemple #54
0
    def action_assign(self, no_prepare=False):
        """ Checks the product type and accordingly writes the state. """
        # TDE FIXME: remove decorator once everything is migrated
        # TDE FIXME: clean me, please
        main_domain = {}

        Quant = self.env['stock.quant']
        Uom = self.env['product.uom']
        moves_to_assign = self.env['stock.move']
        moves_to_do = self.env['stock.move']
        operations = self.env['stock.pack.operation']
        ancestors_list = {}

        # work only on in progress moves
        moves = self.filtered(
            lambda move: move.state in ['confirmed', 'waiting', 'assigned'])
        moves.filtered(lambda move: move.reserved_quant_ids).do_unreserve()
        for move in moves:
            if move.location_id.usage in ('supplier', 'inventory',
                                          'production'):
                moves_to_assign |= move
                # TDE FIXME: what ?
                # in case the move is returned, we want to try to find quants before forcing the assignment
                if not move.origin_returned_move_id:
                    continue
            # if the move is preceeded, restrict the choice of quants in the ones moved previously in original move
            ancestors = move.find_move_ancestors()
            if move.product_id.type in ['consu', 'service'] and not ancestors:
                moves_to_assign |= move
                continue
            else:
                moves_to_do |= move

                # we always search for yet unassigned quants
                main_domain[move.id] = [('reservation_id', '=', False),
                                        ('qty', '>', 0)]

                ancestors_list[move.id] = True if ancestors else False
                if move.state == 'waiting' and not ancestors:
                    # if the waiting move hasn't yet any ancestor (PO/MO not confirmed yet), don't find any quant available in stock
                    main_domain[move.id] += [('id', '=', False)]
                elif ancestors:
                    main_domain[move.id] += [('history_ids', 'in',
                                              ancestors.ids)]

                # if the move is returned from another, restrict the choice of quants to the ones that follow the returned move
                if move.origin_returned_move_id:
                    main_domain[move.id] += [('history_ids', 'in',
                                              move.origin_returned_move_id.id)]
                for link in move.linked_move_operation_ids:
                    operations |= link.operation_id

        # Check all ops and sort them: we want to process first the packages, then operations with lot then the rest
        operations = operations.sorted(
            key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) +
            (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0))
        for ops in operations:
            # TDE FIXME: this code seems to be in action_done, isn't it ?
            # first try to find quants based on specific domains given by linked operations for the case where we want to rereserve according to existing pack operations
            if not (ops.product_id and ops.pack_lot_ids):
                for record in ops.linked_move_operation_ids:
                    move = record.move_id
                    if move.id in main_domain:
                        qty = record.qty
                        domain = main_domain[move.id]
                        if qty:
                            quants = Quant.quants_get_preferred_domain(
                                qty,
                                move,
                                ops=ops,
                                domain=domain,
                                preferred_domain_list=[])
                            Quant.quants_reserve(quants, move, record)
            else:
                lot_qty = {}
                rounding = ops.product_id.uom_id.rounding
                for pack_lot in ops.pack_lot_ids:
                    lot_qty[pack_lot.lot_id.
                            id] = ops.product_uom_id._compute_quantity(
                                pack_lot.qty, ops.product_id.uom_id)
                for record in ops.linked_move_operation_ids:
                    move_qty = record.qty
                    move = record.move_id
                    domain = main_domain[move.id]
                    for lot in lot_qty:
                        if float_compare(
                                lot_qty[lot], 0, precision_rounding=rounding
                        ) > 0 and float_compare(
                                move_qty, 0, precision_rounding=rounding) > 0:
                            qty = min(lot_qty[lot], move_qty)
                            quants = Quant.quants_get_preferred_domain(
                                qty,
                                move,
                                ops=ops,
                                lot_id=lot,
                                domain=domain,
                                preferred_domain_list=[])
                            Quant.quants_reserve(quants, move, record)
                            lot_qty[lot] -= qty
                            move_qty -= qty

        # Sort moves to reserve first the ones with ancestors, in case the same product is listed in
        # different stock moves.
        for move in sorted(moves_to_do,
                           key=lambda x: -1
                           if ancestors_list.get(x.id) else 0):
            # then if the move isn't totally assigned, try to find quants without any specific domain
            if move.state != 'assigned' and not self.env.context.get(
                    'reserve_only_ops'):
                qty_already_assigned = move.reserved_availability
                qty = move.product_qty - qty_already_assigned

                quants = Quant.quants_get_preferred_domain(
                    qty,
                    move,
                    domain=main_domain[move.id],
                    preferred_domain_list=[])
                Quant.quants_reserve(quants, move)

        # force assignation of consumable products and incoming from supplier/inventory/production
        # Do not take force_assign as it would create pack operations
        if moves_to_assign:
            moves_to_assign.write({'state': 'assigned'})
        if not no_prepare:
            self.check_recompute_pack_op()
Exemple #55
0
 def _prepare_stock_moves(self, picking):
     """ Prepare the stock moves data for one order line. This function returns a list of
     dictionary ready to be used in stock.move's create()
     """
     self.ensure_one()
     res = []
     if self.product_id.type not in ['product', 'consu']:
         return res
     qty = 0.0
     price_unit = self._get_stock_move_price_unit()
     for move in self.move_ids.filtered(lambda x: x.state != 'cancel'):
         qty += move.product_qty
     template = {
         'name':
         self.name or '',
         'product_id':
         self.product_id.id,
         'product_uom':
         self.product_uom.id,
         'date':
         self.order_id.date_order,
         'date_expected':
         self.date_planned,
         'location_id':
         self.order_id.partner_id.property_stock_supplier.id,
         'location_dest_id':
         self.order_id._get_destination_location(),
         'picking_id':
         picking.id,
         'partner_id':
         self.order_id.dest_address_id.id,
         'move_dest_id':
         False,
         'state':
         'draft',
         'purchase_line_id':
         self.id,
         'company_id':
         self.order_id.company_id.id,
         'price_unit':
         price_unit,
         'picking_type_id':
         self.order_id.picking_type_id.id,
         'group_id':
         self.order_id.group_id.id,
         'procurement_id':
         False,
         'origin':
         self.order_id.name,
         'route_ids':
         self.order_id.picking_type_id.warehouse_id and [(6, 0, [
             x.id
             for x in self.order_id.picking_type_id.warehouse_id.route_ids
         ])] or [],
         'warehouse_id':
         self.order_id.picking_type_id.warehouse_id.id,
     }
     # Fullfill all related procurements with this po line
     diff_quantity = self.product_qty - qty
     for procurement in self.procurement_ids.filtered(
             lambda p: p.state != 'cancel'):
         # If the procurement has some moves already, we should deduct their quantity
         sum_existing_moves = sum(x.product_qty
                                  for x in procurement.move_ids
                                  if x.state != 'cancel')
         existing_proc_qty = procurement.product_id.uom_id._compute_quantity(
             sum_existing_moves, procurement.product_uom)
         procurement_qty = procurement.product_uom._compute_quantity(
             procurement.product_qty, self.product_uom) - existing_proc_qty
         if float_compare(
                 procurement_qty,
                 0.0,
                 precision_rounding=procurement.product_uom.rounding
         ) > 0 and float_compare(
                 diff_quantity,
                 0.0,
                 precision_rounding=self.product_uom.rounding) > 0:
             tmp = template.copy()
             tmp.update({
                 'product_uom_qty':
                 min(procurement_qty, diff_quantity),
                 'move_dest_id':
                 procurement.move_dest_id.
                 id,  # move destination is same as procurement destination
                 'procurement_id':
                 procurement.id,
                 'propagate':
                 procurement.rule_id.propagate,
             })
             res.append(tmp)
             diff_quantity -= min(procurement_qty, diff_quantity)
     if float_compare(diff_quantity,
                      0.0,
                      precision_rounding=self.product_uom.rounding) > 0:
         template['product_uom_qty'] = diff_quantity
         res.append(template)
     return res
    def _update_reserved_quantity(self,
                                  need,
                                  available_quantity,
                                  location_id,
                                  lot_id=None,
                                  package_id=None,
                                  owner_id=None,
                                  strict=True):
        """ Create or update move lines.
        """
        self.ensure_one()

        if not lot_id:
            lot_id = self.env['stock.production.lot']
        if not package_id:
            package_id = self.env['stock.quant.package']
        if not owner_id:
            owner_id = self.env['res.partner']

        taken_quantity = min(available_quantity, need)

        # `taken_quantity` is in the quants unit of measure. There's a possibility that the move's
        # unit of measure won't be respected if we blindly reserve this quantity, a common usecase
        # is if the move's unit of measure's rounding does not allow fractional reservation. We chose
        # to convert `taken_quantity` to the move's unit of measure with a down rounding method and
        # then get it back in the quants unit of measure with an half-up rounding_method. This
        # way, we'll never reserve more than allowed. We do not apply this logic if
        # `available_quantity` is brought by a chained move line. In this case, `_prepare_move_line_vals`
        # will take care of changing the UOM to the UOM of the product.
        if not strict:
            taken_quantity_move_uom = self.product_id.uom_id._compute_quantity(
                taken_quantity, self.product_uom, rounding_method='DOWN')
            taken_quantity = self.product_uom._compute_quantity(
                taken_quantity_move_uom,
                self.product_id.uom_id,
                rounding_method='HALF-UP')

        quants = []

        if self.product_id.tracking == 'serial':
            rounding = self.env['decimal.precision'].precision_get(
                'Product Unit of Measure')
            if float_compare(taken_quantity,
                             int(taken_quantity),
                             precision_digits=rounding) != 0:
                taken_quantity = 0

        # ADD filters logic to get quants
        quant_filters = []
        for specs in self.move_line_specs_ids:

            quant_filters.append(specs.create_specs_filter_values())

        try:
            if not float_is_zero(
                    taken_quantity,
                    precision_rounding=self.product_id.uom_id.rounding):
                quants = self.env['stock.quant']._update_reserved_quantity(
                    self.product_id,
                    location_id,
                    taken_quantity,
                    lot_id=lot_id,
                    package_id=package_id,
                    owner_id=owner_id,
                    strict=strict,
                    specs_filter=quant_filters)
        except UserError:
            taken_quantity = 0

        # Find a candidate move line to update or create a new one.
        _logger.info("!!!_update_reserved_quantity() UPDATE RESERVED QTY: %r",
                     quants)
        for reserved_quant, quantity in quants:
            to_update = self.move_line_ids.filtered(
                lambda ml: ml._reservation_is_updatable(
                    quantity, reserved_quant))
            if to_update:
                to_update[0].with_context(
                    bypass_reservation_update=True
                ).product_uom_qty += self.product_id.uom_id._compute_quantity(
                    quantity,
                    to_update[0].product_uom_id,
                    rounding_method='HALF-UP')
            else:
                if self.product_id.tracking == 'serial':
                    for i in range(0, int(quantity)):
                        self.env['stock.move.line'].create(
                            self._prepare_move_line_vals(
                                quantity=1, reserved_quant=reserved_quant))
                else:
                    self.env['stock.move.line'].create(
                        self._prepare_move_line_vals(
                            quantity=quantity, reserved_quant=reserved_quant))
        return taken_quantity
Exemple #57
0
    def action_done(self):
        if self.state == 'done':
            return
        if self.label_lines and len(self.label_lines) > 0:
            raise UserError(u'已生成标签明细请不要重复操作!')
        if float_compare(self.sundryin_qty, 0.0,
                         precision_rounding=0.000001) <= 0.0:
            raise UserError(u'杂入总数不能小于0!')
        if float_compare(self.everyone_qty, 0.0,
                         precision_rounding=0.000001) <= 0.0:
            raise UserError(u'每标签数量不能小于0!')
        if float_compare(self.sundryin_qty,
                         self.everyone_qty,
                         precision_rounding=0.000001) < 0.0:
            raise UserError(u"每标签数量不可以大于杂入总数!")
        temp_qty, labellines = self.sundryin_qty, []

        productlot = self.env['stock.production.lot'].action_checkout_lot(
            self.product_id.id, self.product_lot)
        for index in range(
                0, int(math.ceil(self.sundryin_qty / self.everyone_qty))):
            if float_compare(temp_qty, 0.0,
                             precision_rounding=0.000001) <= 0.0:
                break
            if float_compare(temp_qty,
                             self.everyone_qty,
                             precision_rounding=0.000001) >= 0.0:
                labelqty = self.everyone_qty
            else:
                labelqty = temp_qty
            tlabel = self.env['aas.product.label'].create({
                'product_id':
                self.product_id.id,
                'product_lot':
                productlot.id,
                'location_id':
                self.location_id.id,
                'stocked':
                True,
                'product_qty':
                labelqty
            })
            labellines.append((0, 0, {
                'label_id': tlabel.id,
                'product_qty': labelqty
            }))
            temp_qty -= labelqty
        self.write({'label_lines': labellines, 'state': 'done'})
        company_id, tproduct = self.env.user.company_id.id, self.product_id
        sundrylocation = self.env.ref('aas_wms.stock_location_sundry')
        self.env['stock.move'].create({
            'name': u'杂入%s' % tproduct.default_code,
            'product_id': tproduct.id,
            'product_uom': tproduct.uom_id.id,
            'create_date': fields.Datetime.now(),
            'restrict_lot_id': productlot.id,
            'product_uom_qty': self.sundryin_qty,
            'location_id': sundrylocation.id,
            'location_dest_id': self.location_id.id,
            'company_id': company_id
        }).action_done()
Exemple #58
0
    def _action_done(self):
        #         print ('********kakakak111111')
        #         print('***self 1',self)
        #         moves_todo = super(StockMove, self)._action_done()
        #         return moves_todo

        self.filtered(lambda move: move.state == 'draft')._action_confirm(
        )  # MRP allows scrapping draft moves
        moves = self.exists().filtered(lambda x: x.state not in
                                       ('done', 'cancel'))
        moves_todo = self.env['stock.move']  #moves#

        # Cancel moves where necessary ; we should do it before creating the extra moves because
        # this operation could trigger a merge of moves.
        for move in moves:
            if move.quantity_done <= 0:
                if float_compare(
                        move.product_uom_qty,
                        0.0,
                        precision_rounding=move.product_uom.rounding) == 0:
                    move._action_cancel()

        # Create extra moves where necessary
        for move in moves:
            if move.state == 'cancel' or move.quantity_done <= 0:
                continue
            # extra move will not be merged in mrp
            if not move.picking_id:
                moves_todo |= move
            moves_todo |= move._create_extra_move()
        print('****************moves_todo 1', moves, moves_todo)
        # Split moves where necessary and move quants
        for move in moves_todo:
            print('****************moves_todo 2', moves, moves_todo)
            # To know whether we need to create a backorder or not, round to the general product's
            # decimal precision and not the product's UOM.
            rounding = self.env['decimal.precision'].precision_get(
                'Product Unit of Measure')
            if float_compare(move.quantity_done,
                             move.product_uom_qty,
                             precision_digits=rounding) < 0:
                # Need to do some kind of conversion here
                qty_split = move.product_uom._compute_quantity(
                    move.product_uom_qty - move.quantity_done,
                    move.product_id.uom_id,
                    rounding_method='HALF-UP')
                new_move = move._split(qty_split)
                for move_line in move.move_line_ids:
                    if move_line.product_qty and move_line.qty_done:
                        # FIXME: there will be an issue if the move was partially available
                        # By decreasing `product_qty`, we free the reservation.
                        # FIXME: if qty_done > product_qty, this could raise if nothing is in stock
                        try:
                            move_line.write(
                                {'product_uom_qty': move_line.qty_done})
                        except UserError:
                            pass
                move._unreserve_initial_demand(new_move)
            move.move_line_ids._action_done()
        # Check the consistency of the result packages; there should be an unique location across
        # the contained quants.
        for result_package in moves_todo\
                .mapped('move_line_ids.result_package_id')\
                .filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
            if len(result_package.quant_ids.mapped('location_id')) > 1:
                raise UserError(
                    _('You should not put the contents of a package in different locations.'
                      ))
        picking = moves_todo and moves_todo[0].picking_id or False
        moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
        moves_todo.mapped('move_dest_ids')._action_assign()

        # We don't want to create back order for scrap moves
        # Replace by a kwarg in master
        if self.env.context.get('is_scrap'):
            return moves_todo

        if picking:
            picking._create_backorder()

        return moves_todo
Exemple #59
0
    def _action_done(self):
        """ This method is called during a move's `action_done`. It'll actually move a quant from
        the source location to the destination location, and unreserve if needed in the source
        location.

        This method is intended to be called on all the move lines of a move. This method is not
        intended to be called when editing a `done` move (that's what the override of `write` here
        is done.
        """
        Quant = self.env['stock.quant']

        # First, we loop over all the move lines to do a preliminary check: `qty_done` should not
        # be negative and, according to the presence of a picking type or a linked inventory
        # adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink
        # the line. It is mandatory in order to free the reservation and correctly apply
        # `action_done` on the next move lines.
        ml_ids_to_delete = OrderedSet()
        lot_vals_to_create = []  # lot values for batching the creation
        associate_line_lot = []  # move_line to associate to the lot
        for ml in self:
            # Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`.
            uom_qty = float_round(
                ml.qty_done,
                precision_rounding=ml.product_uom_id.rounding,
                rounding_method='HALF-UP')
            precision_digits = self.env['decimal.precision'].precision_get(
                'Product Unit of Measure')
            qty_done = float_round(ml.qty_done,
                                   precision_digits=precision_digits,
                                   rounding_method='HALF-UP')
            if float_compare(
                    uom_qty, qty_done, precision_digits=precision_digits) != 0:
                raise UserError(
                    _('The quantity done for the product "%s" doesn\'t respect the rounding precision \
                                  defined on the unit of measure "%s". Please change the quantity done or the \
                                  rounding precision of your unit of measure.')
                    % (ml.product_id.display_name, ml.product_uom_id.name))

            qty_done_float_compared = float_compare(
                ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding)
            if qty_done_float_compared > 0:
                if ml.product_id.tracking != 'none':
                    picking_type_id = ml.move_id.picking_type_id
                    if picking_type_id:
                        if picking_type_id.use_create_lots:
                            # If a picking type is linked, we may have to create a production lot on
                            # the fly before assigning it to the move line if the user checked both
                            # `use_create_lots` and `use_existing_lots`.
                            if ml.lot_name and not ml.lot_id:
                                lot_vals_to_create.append({
                                    'name':
                                    ml.lot_name,
                                    'product_id':
                                    ml.product_id.id,
                                    'company_id':
                                    ml.move_id.company_id.id
                                })
                                associate_line_lot.append(ml)
                                continue  # Avoid the raise after because not lot_id is set
                        elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots:
                            # If the user disabled both `use_create_lots` and `use_existing_lots`
                            # checkboxes on the picking type, he's allowed to enter tracked
                            # products without a `lot_id`.
                            continue
                    elif ml.move_id.inventory_id:
                        # If an inventory adjustment is linked, the user is allowed to enter
                        # tracked products without a `lot_id`.
                        continue

                    if not ml.lot_id:
                        raise UserError(
                            _('You need to supply a Lot/Serial number for product %s.'
                              ) % ml.product_id.display_name)
            elif qty_done_float_compared < 0:
                raise UserError(_('No negative quantities allowed'))
            else:
                ml_ids_to_delete.add(ml.id)

        mls_to_delete = self.env['stock.move.line'].browse(ml_ids_to_delete)
        mls_to_delete.unlink()

        # Batching the creation of lots and associated each to the right ML (order is preserve in the create)
        lots = self.env['stock.production.lot'].create(lot_vals_to_create)
        for ml, lot in zip(associate_line_lot, lots):
            ml.write({'lot_id': lot.id})

        mls_todo = (self - mls_to_delete)
        mls_todo._check_company()

        # Now, we can actually move the quant.
        ml_ids_to_ignore = OrderedSet()
        for ml in mls_todo:
            if ml.product_id.type == 'product':
                rounding = ml.product_uom_id.rounding

                # if this move line is force assigned, unreserve elsewhere if needed
                if not ml._should_bypass_reservation(
                        ml.location_id) and float_compare(
                            ml.qty_done,
                            ml.product_uom_qty,
                            precision_rounding=rounding) > 0:
                    qty_done_product_uom = ml.product_uom_id._compute_quantity(
                        ml.qty_done,
                        ml.product_id.uom_id,
                        rounding_method='HALF-UP')
                    extra_qty = qty_done_product_uom - ml.product_qty
                    ml_to_ignore = self.env['stock.move.line'].browse(
                        ml_ids_to_ignore)
                    ml._free_reservation(ml.product_id,
                                         ml.location_id,
                                         extra_qty,
                                         lot_id=ml.lot_id,
                                         package_id=ml.package_id,
                                         owner_id=ml.owner_id,
                                         ml_to_ignore=ml_to_ignore)
                # unreserve what's been reserved
                if not ml._should_bypass_reservation(
                        ml.location_id
                ) and ml.product_id.type == 'product' and ml.product_qty:
                    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)

                # move what's been actually done
                quantity = ml.product_uom_id._compute_quantity(
                    ml.qty_done,
                    ml.move_id.product_id.uom_id,
                    rounding_method='HALF-UP')
                available_qty, in_date = Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_id,
                    -quantity,
                    lot_id=ml.lot_id,
                    package_id=ml.package_id,
                    owner_id=ml.owner_id)
                if available_qty < 0 and ml.lot_id:
                    # see if we can compensate the negative quants with some untracked quants
                    untracked_qty = Quant._get_available_quantity(
                        ml.product_id,
                        ml.location_id,
                        lot_id=False,
                        package_id=ml.package_id,
                        owner_id=ml.owner_id,
                        strict=True)
                    if untracked_qty:
                        taken_from_untracked_qty = min(untracked_qty,
                                                       abs(quantity))
                        Quant._update_available_quantity(
                            ml.product_id,
                            ml.location_id,
                            -taken_from_untracked_qty,
                            lot_id=False,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id)
                        Quant._update_available_quantity(
                            ml.product_id,
                            ml.location_id,
                            taken_from_untracked_qty,
                            lot_id=ml.lot_id,
                            package_id=ml.package_id,
                            owner_id=ml.owner_id)
                Quant._update_available_quantity(
                    ml.product_id,
                    ml.location_dest_id,
                    quantity,
                    lot_id=ml.lot_id,
                    package_id=ml.result_package_id,
                    owner_id=ml.owner_id,
                    in_date=in_date)
            ml_ids_to_ignore.add(ml.id)
        # Reset the reserved quantity as we just moved it to the destination location.
        mls_todo.with_context(bypass_reservation_update=True).write({
            'product_uom_qty':
            0.00,
            'date':
            fields.Datetime.now(),
        })
    def button_validate(self):
        res = super(Picking, self).button_validate()
        self.ensure_one()
        invoice_total = 0
        payment_total = 0
        exceed_amount = 0
        customer_inv = self.env["account.invoice"].search([
            ('partner_id', '=', self.partner_id.id),
            ('state', 'not in', ['draft', 'cancel']),
            ('type', '=', 'out_invoice')
        ])
        for inv in customer_inv:
            invoice_total += inv.amount_total
        customer_payment = self.env["account.payment"].search([
            ('partner_id', '=', self.partner_id.id),
            ('payment_type', '=', 'inbound'),
            ('state', 'in', ['posted', 'reconciled'])
        ])
        for pay in customer_payment:
            payment_total += pay.amount
        sale = self.env['sale.order'].search([('name', '=', self.origin)])
        delivered_quantity = all(line.product_id.invoice_policy == 'delivery'
                                 for line in self.move_line_ids)
        if payment_total > invoice_total:
            print("else")
        elif invoice_total > payment_total:
            exceed_amount = (invoice_total + sale.amount_total) - payment_total
        if delivered_quantity:
            if exceed_amount > self.partner_id.credit_limit:
                raise UserError(_('Credit limit exceeded for this customer'))
        else:
            if not self.move_lines and not self.move_line_ids:
                raise UserError(_('Please add some lines to move'))

            # If no lots when needed, raise error
            picking_type = self.picking_type_id
            no_quantities_done = all(line.qty_done == 0.0
                                     for line in self.move_line_ids)
            no_initial_demand = all(move.product_uom_qty == 0.0
                                    for move in self.move_lines)
            if no_initial_demand and no_quantities_done:
                raise UserError(
                    _('You cannot validate a transfer if you have not processed any quantity.'
                      ))

            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' and (
                            line.qty_done == 0 or
                        (not line.lot_name and not line.lot_id)):
                        raise UserError(
                            _('You need to supply a lot/serial number for %s.')
                            % product.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