예제 #1
0
 def _start_nextworkorder(self):
     rounding = self.product_id.uom_id.rounding
     if self.next_work_order_id.state == 'pending' and (
             (self.operation_id.batch == 'no' and
              float_compare(self.qty_production, self.qty_produced, precision_rounding=rounding) <= 0) or
             (self.operation_id.batch == 'yes' and
              float_compare(self.operation_id.batch_size, self.qty_produced, precision_rounding=rounding) <= 0)):
         self.next_work_order_id.state = 'ready'
예제 #2
0
    def test_rounding_invalid(self):
        """ verify that invalid parameters are forbidden """
        with self.assertRaises(AssertionError):
            float_is_zero(0.01, precision_digits=3, precision_rounding=0.01)

        with self.assertRaises(AssertionError):
            float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01)

        with self.assertRaises(AssertionError):
            float_round(0.01, precision_digits=3, precision_rounding=0.01)
예제 #3
0
 def _check_package(self):
     default_uom = self.product_id.uom_id
     pack = self.product_packaging
     qty = self.product_uom_qty
     q = default_uom._compute_quantity(pack.qty, self.product_uom)
     # We do not use the modulo operator to check if qty is a mltiple of q. Indeed the quantity
     # per package might be a float, leading to incorrect results. For example:
     # 8 % 1.6 = 1.5999999999999996
     # 5.4 % 1.8 = 2.220446049250313e-16
     if (qty and q
             and float_compare(qty / q,
                               float_round(qty / q, precision_rounding=1.0),
                               precision_rounding=0.001) != 0):
         newqty = qty - (qty % q) + q
         return {
             'warning': {
                 'title':
                 _('Warning'),
                 'message':
                 _("This product is packaged by %.2f %s. You should sell %.2f %s."
                   ) % (pack.qty, default_uom.name, newqty,
                        self.product_uom.name),
             },
         }
     return {}
예제 #4
0
 def action_validate(self):
     inventory_lines = self.line_ids.filtered(
         lambda l: l.product_id.tracking in ['lot', 'serial'] and not l.
         prod_lot_id and l.theoretical_qty != l.product_qty)
     lines = self.line_ids.filtered(lambda l: float_compare(
         l.product_qty, 1, precision_rounding=l.product_uom_id.rounding
     ) > 0 and l.product_id.tracking == 'serial' and l.prod_lot_id)
     if inventory_lines and not lines:
         wiz_lines = [(0, 0, {
             'product_id': product.id,
             'tracking': product.tracking
         }) for product in inventory_lines.mapped('product_id')]
         wiz = self.env['stock.track.confirmation'].create({
             'inventory_id':
             self.id,
             'tracking_line_ids':
             wiz_lines
         })
         return {
             'name': _('Tracked Products in Inventory Adjustment'),
             'type': 'ir.actions.act_window',
             'view_mode': 'form',
             'res_model': 'stock.track.confirmation',
             'target': 'new',
             'res_id': wiz.id,
         }
     else:
         self._action_done()
예제 #5
0
 def write(self, values):
     if values.get('order_line') and self.state == 'sale':
         for order in self:
             pre_order_line_qty = {
                 order_line: order_line.product_uom_qty
                 for order_line in order.mapped('order_line')
             }
     res = super(SaleOrder, self).write(values)
     if values.get('order_line') and self.state == 'sale':
         for order in self:
             to_log = {}
             for order_line in order.order_line:
                 if pre_order_line_qty.get(
                         order_line, False) and float_compare(
                             order_line.product_uom_qty,
                             pre_order_line_qty[order_line],
                             order_line.product_uom.rounding) < 0:
                     to_log[order_line] = (order_line.product_uom_qty,
                                           pre_order_line_qty[order_line])
             if to_log:
                 documents = self.env[
                     'stock.picking']._log_activity_get_documents(
                         to_log, 'move_ids', 'UP')
                 order._log_decrease_ordered_quantity(documents)
     return res
예제 #6
0
 def _compute_consumed_less_than_planned(self):
     for order in self:
         order.consumed_less_than_planned = any(
             order.move_raw_ids.filtered(lambda move: float_compare(
                 move.quantity_done,
                 move.product_uom_qty,
                 precision_rounding=move.product_uom.rounding) == -1))
예제 #7
0
 def _check_main_currency_rounding(self):
     if any(precision.name == 'Account' and tools.float_compare(
             self.env.user.company_id.currency_id.rounding,
             10**-precision.digits,
             precision_digits=6) == -1 for precision in self):
         raise ValidationError(
             _("You cannot define the decimal precision of 'Account' as greater than the rounding factor of the company's main currency"
               ))
     return True
예제 #8
0
 def _onchange_qty_done(self):
     """ When the user is encoding a produce 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.product_id.tracking == 'serial' and self.qty_done:
         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
예제 #9
0
    def _onchange_product_qty(self):
        lines = []
        qty_todo = self.product_uom_id._compute_quantity(self.product_qty, self.production_id.product_uom_id, round=False)
        for move in self.production_id.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.bom_line_id):
            qty_to_consume = float_round(qty_todo * move.unit_factor, precision_rounding=move.product_uom.rounding)
            for move_line in move.move_line_ids:
                if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0:
                    break
                if move_line.lot_produced_id or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0:
                    continue
                to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty)
                lines.append({
                    'move_id': move.id,
                    'qty_to_consume': to_consume_in_line,
                    'qty_done': to_consume_in_line,
                    'lot_id': move_line.lot_id.id,
                    'product_uom_id': move.product_uom.id,
                    'product_id': move.product_id.id,
                    'qty_reserved': min(to_consume_in_line, move_line.product_uom_qty),
                })
                qty_to_consume -= to_consume_in_line
            if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0:
                if move.product_id.tracking == 'serial':
                    while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0:
                        lines.append({
                            'move_id': move.id,
                            'qty_to_consume': 1,
                            'qty_done': 1,
                            'product_uom_id': move.product_uom.id,
                            'product_id': move.product_id.id,
                        })
                        qty_to_consume -= 1
                else:
                    lines.append({
                        'move_id': move.id,
                        'qty_to_consume': qty_to_consume,
                        'qty_done': qty_to_consume,
                        'product_uom_id': move.product_uom.id,
                        'product_id': move.product_id.id,
                    })

        self.produce_line_ids = [(5,)] + [(0, 0, x) for x in lines]
예제 #10
0
    def _prepare_reconciliation(self, st_line, move_lines=None, partner=None):
        ''' Reconcile the statement line with some move lines using this reconciliation model.
        :param st_line:     An account.bank.statement.line record.
        :param move_lines:  An account.move.line recordset.
        :param partner_id:  An optional res.partner record. If not set, st_line.partner_id will be used.
        :return:            Counterpart account.moves.
        '''
        self.ensure_one()

        # Create counterpart_aml_dicts + payment_aml_rec.
        counterpart_aml_dicts = []
        payment_aml_rec = self.env['account.move.line']
        if move_lines:
            for aml in move_lines:
                if aml.account_id.internal_type == 'liquidity':
                    payment_aml_rec |= aml
                else:
                    amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual
                    counterpart_aml_dicts.append({
                        'name': aml.name if aml.name != '/' else aml.move_id.name,
                        'debit': amount < 0 and -amount or 0,
                        'credit': amount > 0 and amount or 0,
                        'move_line': aml,
                    })

        # Create new_aml_dicts.
        new_aml_dicts = self._get_write_off_move_lines_dict(st_line, move_lines=move_lines)

        line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount
        line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id
        total_residual = move_lines and sum(aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0
        total_residual -= sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts)

        # Create open_balance_dict
        open_balance_dict = None
        if float_compare(line_residual, total_residual, precision_rounding=line_currency.rounding) != 0:
            if not partner and not st_line.partner_id:
                open_balance_dict = False
            else:
                balance = total_residual - line_residual
                partner = partner or st_line.partner_id
                open_balance_dict = {
                    'name': '%s : %s' % (st_line.name, _('Open Balance')),
                    'account_id': balance < 0 and partner.property_account_payable_id.id or partner.property_account_receivable_id.id,
                    'debit': balance > 0 and balance or 0,
                    'credit': balance < 0 and -balance or 0,
                }
        return {
           'counterpart_aml_dicts': counterpart_aml_dicts,
           'payment_aml_rec': payment_aml_rec,
           'new_aml_dicts': new_aml_dicts,
           'open_balance_dict': open_balance_dict
        }
예제 #11
0
 def _onchange_product_id_check_availability(self):
     if not self.product_id or not self.product_uom_qty or not self.product_uom:
         self.product_packaging = False
         return {}
     if self.product_id.type == 'product':
         precision = self.env['decimal.precision'].precision_get(
             'Product Unit of Measure')
         product = self.product_id.with_context(
             warehouse=self.order_id.warehouse_id.id,
             lang=self.order_id.partner_id.lang or self.env.user.lang
             or 'en_US')
         product_qty = self.product_uom._compute_quantity(
             self.product_uom_qty, self.product_id.uom_id)
         if float_compare(product.virtual_available,
                          product_qty,
                          precision_digits=precision) == -1:
             is_available = self._check_routing()
             if not is_available:
                 message =  _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \
                         (self.product_uom_qty, self.product_uom.name, self.product_id.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name)
                 # We check if some products are available in other warehouses.
                 if float_compare(product.virtual_available,
                                  self.product_id.virtual_available,
                                  precision_digits=precision) == -1:
                     message += _('\nThere are %s %s available across all warehouses.\n\n') % \
                             (self.product_id.virtual_available, product.uom_id.name)
                     for warehouse in self.env['stock.warehouse'].search(
                         []):
                         quantity = self.product_id.with_context(
                             warehouse=warehouse.id).virtual_available
                         if quantity > 0:
                             message += "%s: %s %s\n" % (
                                 warehouse.name, quantity,
                                 self.product_id.uom_id.name)
                 warning_mess = {
                     'title': _('Not enough inventory!'),
                     'message': message
                 }
                 return {'warning': warning_mess}
     return {}
예제 #12
0
 def _update_line_quantity(self, values):
     precision = self.env['decimal.precision'].precision_get(
         'Product Unit of Measure')
     line_products = self.filtered(
         lambda l: l.product_id.type in ['product', 'consu'])
     if line_products.mapped('qty_delivered') and float_compare(
             values['product_uom_qty'],
             max(line_products.mapped('qty_delivered')),
             precision_digits=precision) == -1:
         raise UserError(
             _('You cannot decrease the ordered quantity below the delivered quantity.\n'
               'Create a return first.'))
     super(SaleOrderLine, self)._update_line_quantity(values)
예제 #13
0
    def write(self, values):
        increased_lines = None
        decreased_lines = None
        increased_values = {}
        decreased_values = {}
        if 'product_uom_qty' in values:
            precision = self.env['decimal.precision'].precision_get(
                'Product Unit of Measure')
            increased_lines = self.sudo().filtered(
                lambda r: r.product_id.service_to_purchase and r.
                purchase_line_count and float_compare(
                    r.product_uom_qty,
                    values['product_uom_qty'],
                    precision_digits=precision) == -1)
            decreased_lines = self.sudo().filtered(
                lambda r: r.product_id.service_to_purchase and r.
                purchase_line_count and float_compare(
                    r.product_uom_qty,
                    values['product_uom_qty'],
                    precision_digits=precision) == 1)
            increased_values = {
                line.id: line.product_uom_qty
                for line in increased_lines
            }
            decreased_values = {
                line.id: line.product_uom_qty
                for line in decreased_lines
            }

        result = super(SaleOrderLine, self).write(values)

        if increased_lines:
            increased_lines._purchase_increase_ordered_qty(
                values['product_uom_qty'], increased_values)
        if decreased_lines:
            decreased_lines._purchase_decrease_ordered_qty(
                values['product_uom_qty'], decreased_values)
        return result
예제 #14
0
    def _get_rounding_difference_vals(self, amount, amount_converted):
        if self.config_id.cash_rounding:
            partial_args = {
                'name': 'Rounding line',
                'move_id': self.move_id.id,
            }
            if float_compare(
                    0.0, amount,
                    precision_rounding=self.currency_id.rounding) > 0:  # loss
                partial_args[
                    'account_id'] = self.config_id.rounding_method._get_loss_account_id(
                    ).id
                return self._debit_amounts(partial_args, -amount,
                                           -amount_converted)

            if float_compare(
                    0.0, amount, precision_rounding=self.currency_id.rounding
            ) < 0:  # profit
                partial_args[
                    'account_id'] = self.config_id.rounding_method._get_profit_account_id(
                    ).id
                return self._credit_amounts(partial_args, amount,
                                            amount_converted)
예제 #15
0
 def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
     res = super(AccountMoveLine,
                 self).reconcile(writeoff_acc_id=writeoff_acc_id,
                                 writeoff_journal_id=writeoff_journal_id)
     account_move_ids = [
         l.move_id.id for l in self if float_compare(
             l.move_id.matched_percentage, 1, precision_digits=5) == 0
     ]
     if account_move_ids:
         expense_sheets = self.env['hr.expense.sheet'].search([
             ('account_move_id', 'in', account_move_ids),
             ('state', '!=', 'done')
         ])
         expense_sheets.set_to_paid()
     return res
예제 #16
0
 def write(self, values):
     lines = self.env['sale.order.line']
     if 'product_uom_qty' in values:
         precision = self.env['decimal.precision'].precision_get(
             'Product Unit of Measure')
         lines = self.filtered(
             lambda r: r.state == 'sale' and not r.is_expense and
             float_compare(r.product_uom_qty,
                           values['product_uom_qty'],
                           precision_digits=precision) == -1)
     previous_product_uom_qty = {
         line.id: line.product_uom_qty
         for line in lines
     }
     res = super(SaleOrderLine, self).write(values)
     if lines:
         lines.with_context(
             previous_product_uom_qty=previous_product_uom_qty
         )._action_launch_stock_rule()
     return res
예제 #17
0
 def do_produce(self):
     # Nothing to do for lots since values are created using default data (stock.move.lots)
     quantity = self.product_qty
     if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
         raise UserError(_("The production order for '%s' has no quantity specified.") % self.product_id.display_name)
     for move in self.production_id.move_finished_ids:
         if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel'):
             rounding = move.product_uom.rounding
             if move.product_id.id == self.production_id.product_id.id:
                 move.quantity_done += float_round(quantity, precision_rounding=rounding)
             elif move.unit_factor:
                 # byproducts handling
                 move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding)
     self.check_finished_move_lots()
     if self.production_id.state == 'confirmed':
         self.production_id.write({
             'state': 'progress',
             'date_start': datetime.now(),
         })
     return {'type': 'ir.actions.act_window_close'}
예제 #18
0
 def action_validate(self):
     self.ensure_one()
     if self.product_id.type != 'product':
         return self.do_scrap()
     precision = self.env['decimal.precision'].precision_get(
         'Product Unit of Measure')
     available_qty = sum(self.env['stock.quant']._gather(
         self.product_id,
         self.location_id,
         self.lot_id,
         self.package_id,
         self.owner_id,
         strict=True).mapped('quantity'))
     scrap_qty = self.product_uom_id._compute_quantity(
         self.scrap_qty, self.product_id.uom_id)
     if float_compare(available_qty, scrap_qty,
                      precision_digits=precision) >= 0:
         return self.do_scrap()
     else:
         return {
             'name':
             _('Insufficient Quantity'),
             'view_type':
             'form',
             'view_mode':
             'form',
             'res_model':
             'stock.warn.insufficient.qty.scrap',
             'view_id':
             self.env.ref(
                 'stock.stock_warn_insufficient_qty_scrap_form_view').id,
             'type':
             'ir.actions.act_window',
             'context': {
                 'default_product_id': self.product_id.id,
                 'default_location_id': self.location_id.id,
                 'default_scrap_id': self.id
             },
             'target':
             'new'
         }
예제 #19
0
 def _get_produced_qty(self):
     for production in self:
         done_moves = production.move_finished_ids.filtered(
             lambda x: x.state != 'cancel' and x.product_id.id == production
             .product_id.id)
         qty_produced = sum(done_moves.mapped('quantity_done'))
         wo_done = True
         if any([
                 x.state not in ('done', 'cancel')
                 for x in production.workorder_ids
         ]):
             wo_done = False
         production.check_to_done = (
             production.is_locked and done_moves and float_compare(
                 qty_produced,
                 production.product_qty,
                 precision_rounding=production.product_uom_id.rounding) !=
             -1 and (production.state not in ("done", "cancel"))
             and wo_done)
         production.qty_produced = qty_produced
     return True
예제 #20
0
 def action_validate(self):
     self.ensure_one()
     precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
     available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
     if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0:
         return self.action_unbuild()
     else:
         return {
             'name': _('Insufficient Quantity'),
             'view_type': 'form',
             'view_mode': 'form',
             'res_model': 'stock.warn.insufficient.qty.unbuild',
             'view_id': self.env.ref('mrp.stock_warn_insufficient_qty_unbuild_form_view').id,
             'type': 'ir.actions.act_window',
             'context': {
                 'default_product_id': self.product_id.id,
                 'default_location_id': self.location_id.id,
                 'default_unbuild_id': self.id
             },
             'target': 'new'
         }
예제 #21
0
 def _onchange_qty_producing(self):
     """ Update stock.move.lot records, according to the new qty currently
     produced. """
     moves = self.move_raw_ids.filtered(lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id.id != self.production_id.product_id.id)
     for move in moves:
         move_lots = self.active_move_line_ids.filtered(lambda move_lot: move_lot.move_id == move)
         if not move_lots:
             continue
         rounding = move.product_uom.rounding
         new_qty = float_round(move.unit_factor * self.qty_producing, precision_rounding=rounding)
         if move.product_id.tracking == 'lot':
             move_lots[0].product_qty = new_qty
             move_lots[0].qty_done = new_qty
         elif move.product_id.tracking == 'serial':
             # Create extra pseudo record
             qty_todo = float_round(new_qty - sum(move_lots.mapped('qty_done')), precision_rounding=rounding)
             if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0:
                 while float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0:
                     self.active_move_line_ids += self.env['stock.move.line'].new({
                         'move_id': move.id,
                         'product_id': move.product_id.id,
                         'lot_id': False,
                         'product_uom_qty': 0.0,
                         'product_uom_id': move.product_uom.id,
                         'qty_done': min(1.0, qty_todo),
                         'workorder_id': self.id,
                         'done_wo': False,
                         'location_id': move.location_id.id,
                         'location_dest_id': move.location_dest_id.id,
                         'date': move.date,
                     })
                     qty_todo -= 1
             elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
                 qty_todo = abs(qty_todo)
                 for move_lot in move_lots:
                     if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0:
                         break
                     if not move_lot.lot_id and float_compare(qty_todo, move_lot.qty_done, precision_rounding=rounding) >= 0:
                         qty_todo = float_round(qty_todo - move_lot.qty_done, precision_rounding=rounding)
                         self.active_move_line_ids -= move_lot  # Difference operator
                     else:
                         #move_lot.product_qty = move_lot.product_qty - qty_todo
                         if float_compare(move_lot.qty_done - qty_todo, 0, precision_rounding=rounding) == 1:
                             move_lot.qty_done = move_lot.qty_done - qty_todo
                         else:
                             move_lot.qty_done = 0
                         qty_todo = 0
예제 #22
0
    def compare_amounts(self, amount1, amount2):
        """Compare ``amount1`` and ``amount2`` after rounding them according to the
           given currency's precision..
           An amount is considered lower/greater than another amount if their rounded
           value is different. This is not the same as having a non-zero difference!

           For example 1.432 and 1.431 are equal at 2 digits precision,
           so this method would return 0.
           However 0.006 and 0.002 are considered different (returns 1) because
           they respectively round to 0.01 and 0.0, even though
           0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.

           :param float amount1: first amount to compare
           :param float amount2: second amount to compare
           :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
                    equal to, or greater than ``amount2``, according to
                    ``currency``'s rounding.

           With the new API, call it like: ``currency.compare_amounts(amount1, amount2)``.
        """
        return tools.float_compare(amount1,
                                   amount2,
                                   precision_rounding=self.rounding)
예제 #23
0
 def _generate_lot_ids(self):
     """ Generate stock move lines """
     self.ensure_one()
     MoveLine = self.env['stock.move.line']
     tracked_moves = self.move_raw_ids.filtered(
         lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id != self.production_id.product_id and move.bom_line_id)
     for move in tracked_moves:
         qty = move.unit_factor * self.qty_producing
         if move.product_id.tracking == 'serial':
             while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0:
                 MoveLine.create({
                     'move_id': move.id,
                     'product_uom_qty': 0,
                     'product_uom_id': move.product_uom.id,
                     'qty_done': min(1, qty),
                     'production_id': self.production_id.id,
                     'workorder_id': self.id,
                     'product_id': move.product_id.id,
                     'done_wo': False,
                     'location_id': move.location_id.id,
                     'location_dest_id': move.location_dest_id.id,
                 })
                 qty -= 1
         else:
             MoveLine.create({
                 'move_id': move.id,
                 'product_uom_qty': 0,
                 'product_uom_id': move.product_uom.id,
                 'qty_done': qty,
                 'product_id': move.product_id.id,
                 'production_id': self.production_id.id,
                 'workorder_id': self.id,
                 'done_wo': False,
                 'location_id': move.location_id.id,
                 'location_dest_id': move.location_dest_id.id,
                 })
예제 #24
0
    def test_sale_with_taxes(self):
        """ Test SO with taxes applied on its lines and check subtotal applied on its lines and total applied on the SO """
        # Create a tax with price included
        tax_include = self.env['account.tax'].create({
            'name': 'Tax with price include',
            'amount': 10,
            'price_include': True
        })
        # Create a tax with price not included
        tax_exclude = self.env['account.tax'].create({
            'name': 'Tax with no price include',
            'amount': 10,
        })

        # Apply taxes on the sale order lines
        self.sol_product_order.write({'tax_id': [(4, tax_include.id)]})
        self.sol_serv_deliver.write({'tax_id': [(4, tax_include.id)]})
        self.sol_serv_order.write({'tax_id': [(4, tax_exclude.id)]})
        self.sol_product_deliver.write({'tax_id': [(4, tax_exclude.id)]})

        # Trigger onchange to reset discount, unit price, subtotal, ...
        for line in self.sale_order.order_line:
            line.product_id_change()
            line._onchange_discount()

        for line in self.sale_order.order_line:
            if line.tax_id.price_include:
                price = line.price_unit * line.product_uom_qty - line.price_tax
            else:
                price = line.price_unit * line.product_uom_qty

            self.assertEquals(float_compare(line.price_subtotal, price, precision_digits=2), 0)

        self.assertEquals(self.sale_order.amount_total,
                          self.sale_order.amount_untaxed + self.sale_order.amount_tax,
                          'Taxes should be applied')
예제 #25
0
    def record_production(self):
        if not self:
            return True

        self.ensure_one()
        if self.qty_producing <= 0:
            raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))

        if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids:
            raise UserError(_('You should provide a lot/serial number for the final product.'))

        # Update quantities done on each raw material line
        # For each untracked component without any 'temporary' move lines,
        # (the new workorder tablet view allows registering consumed quantities for untracked components)
        # we assume that only the theoretical quantity was used
        for move in self.move_raw_ids:
            if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\
                        and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo):
                rounding = move.product_uom.rounding
                if self.product_id.tracking != 'none':
                    qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
                    move._generate_consumed_move_line(qty_to_add, self.final_lot_id)
                elif len(move._get_move_lines()) < 2:
                    move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
                else:
                    move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding))

        # Transfer quantities from temporary to final move lots or make them final
        for move_line in self.active_move_line_ids:
            # Check if move_line already exists
            if move_line.qty_done <= 0:  # rounding...
                move_line.sudo().unlink()
                continue
            if move_line.product_id.tracking != 'none' and not move_line.lot_id:
                raise UserError(_('You should provide a lot/serial number for a component.'))
            # Search other move_line where it could be added:
            lots = self.move_line_ids.filtered(lambda x: (x.lot_id.id == move_line.lot_id.id) and (not x.lot_produced_id) and (not x.done_move) and (x.product_id == move_line.product_id))
            if lots:
                lots[0].qty_done += move_line.qty_done
                lots[0].lot_produced_id = self.final_lot_id.id
                self._link_to_quality_check(move_line, lots[0])
                move_line.sudo().unlink()
            else:
                move_line.lot_produced_id = self.final_lot_id.id
                move_line.done_wo = True

        self.move_line_ids.filtered(
            lambda move_line: not move_line.done_move and not move_line.lot_produced_id and move_line.qty_done > 0
        ).write({
            'lot_produced_id': self.final_lot_id.id,
            'lot_produced_qty': self.qty_producing
        })

        # If last work order, then post lots used
        # TODO: should be same as checking if for every workorder something has been done?
        if not self.next_work_order_id:
            production_move = self.production_id.move_finished_ids.filtered(
                                lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
            if production_move.product_id.tracking != 'none':
                move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
                if move_line:
                    move_line.product_uom_qty += self.qty_producing
                    move_line.qty_done += self.qty_producing
                else:
                    location_dest_id = production_move.location_dest_id.get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id
                    move_line.create({'move_id': production_move.id,
                             'product_id': production_move.product_id.id,
                             'lot_id': self.final_lot_id.id,
                             'product_uom_qty': self.qty_producing,
                             'product_uom_id': production_move.product_uom.id,
                             'qty_done': self.qty_producing,
                             'workorder_id': self.id,
                             'location_id': production_move.location_id.id,
                             'location_dest_id': location_dest_id,
                    })
            else:
                production_move._set_quantity_done(self.qty_producing)

        if not self.next_work_order_id:
            for by_product_move in self._get_byproduct_move_to_update():
                    if by_product_move.has_tracking != 'serial':
                        values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor)
                        self.env['stock.move.line'].create(values)
                    elif by_product_move.has_tracking == 'serial':
                        qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id)
                        for i in range(0, int(float_round(qty_todo, precision_digits=0))):
                            values = self._get_byproduct_move_line(by_product_move, 1)
                            self.env['stock.move.line'].create(values)

        # Update workorder quantity produced
        self.qty_produced += self.qty_producing

        if self.final_lot_id:
            self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id
            self.final_lot_id = False

        # One a piece is produced, you can launch the next work order
        self._start_nextworkorder()

        # Set a qty producing
        rounding = self.production_id.product_uom_id.rounding
        if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0:
            self.qty_producing = 0
        elif self.production_id.product_id.tracking == 'serial':
            self._assign_default_final_lot_id()
            self.qty_producing = 1.0
            self._generate_lot_ids()
        else:
            self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding)
            self._generate_lot_ids()

        if self.next_work_order_id and self.next_work_order_id.state not in ['done', 'cancel'] and self.production_id.product_id.tracking != 'none':
            self.next_work_order_id._assign_default_final_lot_id()

        if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0:
            self.button_finish()
        return True
예제 #26
0
 def _compute_is_produced(self):
     rounding = self.production_id.product_uom_id.rounding
     self.is_produced = float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0
예제 #27
0
 def _prepare_account_move_and_lines(self, session=None, move=None):
     res = super(PosOrder, self)._prepare_account_move_and_lines(session, move)
     unpaid_order = self.filtered(lambda o: o.account_move.id == res['move'].id)
     if unpaid_order:
         config_id = unpaid_order[0].config_id
         if config_id.cash_rounding and config_id.rounding_method:
             difference = 0.0
             converted_amount = 0.0
             config_id = unpaid_order[0].config_id
             company_id = unpaid_order[0].company_id
             different_currency = config_id.currency_id if config_id.currency_id.id != company_id.currency_id.id else False
             for order in unpaid_order:
                 order_difference = order.amount_paid - order.amount_total
                 difference += order_difference
                 if config_id.currency_id.id != company_id.currency_id.id:
                     converted_paid = different_currency._convert(order.amount_paid,  company_id.currency_id, company_id, order.date_order)
                     converted_total = different_currency._convert(order.amount_total,  company_id.currency_id, company_id, order.date_order)
                     converted_amount += converted_paid - converted_total
                 else:
                     converted_amount += order_difference
             if difference:
                 profit_account = config_id.rounding_method._get_profit_account_id().id
                 loss_account = config_id.rounding_method._get_loss_account_id().id
                 difference_move_line = {
                     'name': 'Rounding Difference',
                     'partner_id': False,
                     'move_id': res['move'].id,
                 }
                 grouped_data_key = False
                 if float_compare(0.0, difference, precision_rounding=config_id.currency_id.rounding) > 0:
                     difference_move_line.update({
                         'account_id': loss_account,
                         'credit': 0.0,
                         'debit': -converted_amount,
                     })
                     if different_currency:
                         difference_move_line.update({
                             'currency_id': different_currency.id,
                             'amount_currency': -difference
                         })
                     grouped_data_key = ('difference_rounding',
                             False,
                             loss_account,
                             True,
                             different_currency.id if different_currency else False)
                 if float_compare(0.0, difference, precision_rounding=config_id.currency_id.rounding) < 0:
                     difference_move_line.update({
                         'account_id': profit_account,
                         'credit': converted_amount,
                         'debit': 0.0,
                     })
                     if different_currency:
                         difference_move_line.update({
                             'currency_id': different_currency.id,
                             'amount_currency': difference
                         })
                     grouped_data_key = ('difference_rounding',
                             False,
                             profit_account,
                             False,
                             different_currency.id if different_currency else False)
                 if grouped_data_key:
                     res['grouped_data'][grouped_data_key] = [difference_move_line]
     return res
예제 #28
0
    def _action_launch_stock_rule(self):
        """
        Launch procurement group run method with required/custom fields genrated by a
        sale order line. procurement group will launch '_run_pull', '_run_buy' or '_run_manufacture'
        depending on the sale order line product rule.
        """
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        errors = []
        for line in self:
            if line.state != 'sale' or not line.product_id.type in ('consu',
                                                                    'product'):
                continue
            qty = line._get_qty_procurement()
            if float_compare(qty,
                             line.product_uom_qty,
                             precision_digits=precision) >= 0:
                continue

            group_id = line.order_id.procurement_group_id
            if not group_id:
                group_id = self.env['procurement.group'].create({
                    'name':
                    line.order_id.name,
                    'move_type':
                    line.order_id.picking_policy,
                    'sale_id':
                    line.order_id.id,
                    'partner_id':
                    line.order_id.partner_shipping_id.id,
                })
                line.order_id.procurement_group_id = group_id
            else:
                # In case the procurement group is already created and the order was
                # cancelled, we need to update certain values of the group.
                updated_vals = {}
                if group_id.partner_id != line.order_id.partner_shipping_id:
                    updated_vals.update(
                        {'partner_id': line.order_id.partner_shipping_id.id})
                if group_id.move_type != line.order_id.picking_policy:
                    updated_vals.update(
                        {'move_type': line.order_id.picking_policy})
                if updated_vals:
                    group_id.write(updated_vals)

            values = line._prepare_procurement_values(group_id=group_id)
            product_qty = line.product_uom_qty - qty

            procurement_uom = line.product_uom
            quant_uom = line.product_id.uom_id
            get_param = self.env['ir.config_parameter'].sudo().get_param
            if procurement_uom.id != quant_uom.id and get_param(
                    'stock.propagate_uom') != '1':
                product_qty = line.product_uom._compute_quantity(
                    product_qty, quant_uom, rounding_method='HALF-UP')
                procurement_uom = quant_uom

            try:
                self.env['procurement.group'].run(
                    line.product_id, product_qty, procurement_uom,
                    line.order_id.partner_shipping_id.property_stock_customer,
                    line.name, line.order_id.name, values)
            except UserError as error:
                errors.append(error.name)
        if errors:
            raise UserError('\n'.join(errors))
        return True
예제 #29
0
    def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False):
        if lot:
            move_lines = self.move_line_ids.filtered(
                lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
        else:
            move_lines = self.move_line_ids.filtered(
                lambda ml: not ml.lot_id and not ml.lot_produced_id)

        # Sanity check: if the product is a serial number and `lot` is already present in the other
        # consumed move lines, raise.
        if lot and self.product_id.tracking == 'serial' and lot in self.move_line_ids.filtered(
                lambda ml: ml.qty_done).mapped('lot_id'):
            raise UserError(
                _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.'
                  ))

        for ml in move_lines:
            rounding = ml.product_uom_id.rounding
            if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0:
                break
            quantity_to_process = min(qty_to_add,
                                      ml.product_uom_qty - ml.qty_done)
            qty_to_add -= quantity_to_process

            new_quantity_done = (ml.qty_done + quantity_to_process)
            if float_compare(ml.product_uom_id._compute_quantity(
                    new_quantity_done, ml.product_id.uom_id),
                             ml.product_qty,
                             precision_rounding=rounding) >= 0:
                ml.write({
                    'qty_done': new_quantity_done,
                    'lot_produced_id': final_lot.id
                })
            else:
                new_qty_reserved = ml.product_uom_qty - new_quantity_done
                default = {
                    'product_uom_qty': new_quantity_done,
                    'qty_done': new_quantity_done,
                    'lot_produced_id': final_lot.id
                }
                ml.copy(default=default)
                ml.with_context(bypass_reservation_update=True).write({
                    'product_uom_qty':
                    new_qty_reserved,
                    'qty_done':
                    0
                })

        if float_compare(qty_to_add,
                         0,
                         precision_rounding=self.product_uom.rounding) > 0:
            # Search for a sub-location where the product is available. This might not be perfectly
            # correct if the quantity available is spread in several sub-locations, but at least
            # we should be closer to the reality. Anyway, no reservation is made, so it is still
            # possible to change it afterwards.
            quants = self.env['stock.quant']._gather(self.product_id,
                                                     self.location_id,
                                                     lot_id=lot,
                                                     strict=False)
            available_quantity = self.product_id.uom_id._compute_quantity(
                self.env['stock.quant']._get_available_quantity(
                    self.product_id,
                    self.location_id,
                    lot_id=lot,
                    strict=False), self.product_uom)
            location_id = False
            if float_compare(qty_to_add,
                             available_quantity,
                             precision_rounding=self.product_uom.rounding) < 1:
                location_id = quants.filtered(
                    lambda r: r.quantity > 0)[-1:].location_id

            vals = {
                'move_id':
                self.id,
                'product_id':
                self.product_id.id,
                'location_id':
                location_id.id if location_id else self.location_id.id,
                'production_id':
                self.raw_material_production_id.id,
                'location_dest_id':
                self.location_dest_id.id,
                'product_uom_qty':
                0,
                'product_uom_id':
                self.product_uom.id,
                'qty_done':
                qty_to_add,
                'lot_produced_id':
                final_lot.id,
            }
            if lot:
                vals.update({'lot_id': lot.id})
            self.env['stock.move.line'].create(vals)
예제 #30
0
    def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False):
        """ Create procurements based on orderpoints.
        :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing
            1000 orderpoints.
            This is appropriate for batch jobs only.
        """
        if company_id and self.env.user.company_id.id != company_id:
            # To ensure that the company_id is taken into account for
            # all the processes triggered by this method
            # i.e. If a PO is generated by the run of the procurements the
            # sequence to use is the one for the specified company not the
            # one of the user's company
            self = self.with_context(company_id=company_id, force_company=company_id)
        OrderPoint = self.env['stock.warehouse.orderpoint']
        domain = self._get_orderpoint_domain(company_id=company_id)
        orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain,
            order=self._procurement_from_orderpoint_get_order()).ids
        while orderpoints_noprefetch:
            if use_new_cursor:
                cr = registry(self._cr.dbname).cursor()
                self = self.with_env(self.env(cr=cr))
            OrderPoint = self.env['stock.warehouse.orderpoint']

            orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000])
            orderpoints_noprefetch = orderpoints_noprefetch[1000:]

            # Calculate groups that can be executed together
            location_data = OrderedDict()

            def makedefault():
                return {
                    'products': self.env['product.product'],
                    'orderpoints': self.env['stock.warehouse.orderpoint'],
                    'groups': []
                }

            for orderpoint in orderpoints:
                key = self._procurement_from_orderpoint_get_grouping_key([orderpoint.id])
                if not location_data.get(key):
                    location_data[key] = makedefault()
                location_data[key]['products'] += orderpoint.product_id
                location_data[key]['orderpoints'] += orderpoint
                location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id])

            for location_id, location_data in location_data.items():
                location_orderpoints = location_data['orderpoints']
                product_context = dict(self._context, location=location_orderpoints[0].location_id.id)
                substract_quantity = location_orderpoints._quantity_in_progress()

                for group in location_data['groups']:
                    if group.get('from_date'):
                        product_context['from_date'] = group['from_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT)
                    if group['to_date']:
                        product_context['to_date'] = group['to_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT)
                    product_quantity = location_data['products'].with_context(product_context)._product_available()
                    for orderpoint in location_orderpoints:
                        try:
                            op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available']
                            if op_product_virtual is None:
                                continue
                            if float_compare(op_product_virtual, orderpoint.product_min_qty, precision_rounding=orderpoint.product_uom.rounding) <= 0:
                                qty = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - op_product_virtual
                                remainder = orderpoint.qty_multiple > 0 and qty % orderpoint.qty_multiple or 0.0

                                if float_compare(remainder, 0.0, precision_rounding=orderpoint.product_uom.rounding) > 0:
                                    qty += orderpoint.qty_multiple - remainder

                                if float_compare(qty, 0.0, precision_rounding=orderpoint.product_uom.rounding) <= 0:
                                    continue

                                qty -= substract_quantity[orderpoint.id]
                                qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding)
                                if qty_rounded > 0:
                                    values = orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values'])
                                    try:
                                        with self._cr.savepoint():
                                            self.env['procurement.group'].run(orderpoint.product_id, qty_rounded, orderpoint.product_uom, orderpoint.location_id,
                                                                              orderpoint.name, orderpoint.name, values)
                                    except UserError as error:
                                        self.env['stock.rule']._log_next_activity(orderpoint.product_id, error.name)
                                    self._procurement_from_orderpoint_post_process([orderpoint.id])
                                if use_new_cursor:
                                    cr.commit()

                        except OperationalError:
                            if use_new_cursor:
                                orderpoints_noprefetch += [orderpoint.id]
                                cr.rollback()
                                continue
                            else:
                                raise

            try:
                if use_new_cursor:
                    cr.commit()
            except OperationalError:
                if use_new_cursor:
                    cr.rollback()
                    continue
                else:
                    raise

            if use_new_cursor:
                cr.commit()
                cr.close()

        return {}